jacquard_common/
error.rs

1//! Error types for XRPC client operations
2
3use crate::xrpc::EncodeError;
4use bytes::Bytes;
5
6/// Client error type wrapping all possible error conditions
7#[derive(Debug, thiserror::Error, miette::Diagnostic)]
8pub enum ClientError {
9    /// HTTP transport error
10    #[error("HTTP transport error: {0}")]
11    Transport(
12        #[from]
13        #[diagnostic_source]
14        TransportError,
15    ),
16
17    /// Request serialization failed
18    #[error("{0}")]
19    Encode(
20        #[from]
21        #[diagnostic_source]
22        EncodeError,
23    ),
24
25    /// Response deserialization failed
26    #[error("{0}")]
27    Decode(
28        #[from]
29        #[diagnostic_source]
30        DecodeError,
31    ),
32
33    /// HTTP error response
34    #[error("HTTP {0}")]
35    Http(
36        #[from]
37        #[diagnostic_source]
38        HttpError,
39    ),
40
41    /// Authentication error
42    #[error("Authentication error: {0}")]
43    Auth(
44        #[from]
45        #[diagnostic_source]
46        AuthError,
47    ),
48}
49
50/// Transport-level errors that occur during HTTP communication
51#[derive(Debug, thiserror::Error, miette::Diagnostic)]
52pub enum TransportError {
53    /// Failed to establish connection to server
54    #[error("Connection error: {0}")]
55    Connect(String),
56
57    /// Request timed out
58    #[error("Request timeout")]
59    Timeout,
60
61    /// Request construction failed (malformed URI, headers, etc.)
62    #[error("Invalid request: {0}")]
63    InvalidRequest(String),
64
65    /// Other transport error
66    #[error("Transport error: {0}")]
67    Other(Box<dyn std::error::Error + Send + Sync>),
68}
69
70/// Response deserialization errors
71#[derive(Debug, thiserror::Error, miette::Diagnostic)]
72pub enum DecodeError {
73    /// JSON deserialization failed
74    #[error("Failed to deserialize JSON: {0}")]
75    Json(
76        #[from]
77        #[source]
78        serde_json::Error,
79    ),
80    /// CBOR deserialization failed (local I/O)
81    #[error("Failed to deserialize CBOR: {0}")]
82    CborLocal(
83        #[from]
84        #[source]
85        serde_ipld_dagcbor::DecodeError<std::io::Error>,
86    ),
87    /// CBOR deserialization failed (remote/reqwest)
88    #[error("Failed to deserialize CBOR: {0}")]
89    CborRemote(
90        #[from]
91        #[source]
92        serde_ipld_dagcbor::DecodeError<HttpError>,
93    ),
94}
95
96/// HTTP error response (non-200 status codes outside of XRPC error handling)
97#[derive(Debug, thiserror::Error, miette::Diagnostic)]
98pub struct HttpError {
99    /// HTTP status code
100    pub status: http::StatusCode,
101    /// Response body if available
102    pub body: Option<Bytes>,
103}
104
105impl std::fmt::Display for HttpError {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        write!(f, "HTTP {}", self.status)?;
108        if let Some(body) = &self.body {
109            if let Ok(s) = std::str::from_utf8(body) {
110                write!(f, ":\n{}", s)?;
111            }
112        }
113        Ok(())
114    }
115}
116
117/// Result type for client operations
118pub type XrpcResult<T> = std::result::Result<T, ClientError>;
119
120#[cfg(feature = "reqwest-client")]
121impl From<reqwest::Error> for TransportError {
122    fn from(e: reqwest::Error) -> Self {
123        if e.is_timeout() {
124            Self::Timeout
125        } else if e.is_connect() {
126            Self::Connect(e.to_string())
127        } else if e.is_builder() || e.is_request() {
128            Self::InvalidRequest(e.to_string())
129        } else {
130            Self::Other(Box::new(e))
131        }
132    }
133}
134
135/// Authentication and authorization errors
136#[derive(Debug, thiserror::Error, miette::Diagnostic)]
137pub enum AuthError {
138    /// Access token has expired (use refresh token to get a new one)
139    #[error("Access token expired")]
140    TokenExpired,
141
142    /// Access token is invalid or malformed
143    #[error("Invalid access token")]
144    InvalidToken,
145
146    /// Token refresh request failed
147    #[error("Token refresh failed")]
148    RefreshFailed,
149
150    /// Request requires authentication but none was provided
151    #[error("No authentication provided, but endpoint requires auth")]
152    NotAuthenticated,
153
154    /// Other authentication error
155    #[error("Authentication error: {0:?}")]
156    Other(http::HeaderValue),
157}
158
159impl crate::IntoStatic for AuthError {
160    type Output = AuthError;
161
162    fn into_static(self) -> Self::Output {
163        match self {
164            AuthError::TokenExpired => AuthError::TokenExpired,
165            AuthError::InvalidToken => AuthError::InvalidToken,
166            AuthError::RefreshFailed => AuthError::RefreshFailed,
167            AuthError::NotAuthenticated => AuthError::NotAuthenticated,
168            AuthError::Other(header) => AuthError::Other(header),
169        }
170    }
171}