jacquard_common/
error.rs

1//! Error types for XRPC client operations
2
3use crate::xrpc::EncodeError;
4use bytes::Bytes;
5use smol_str::SmolStr;
6
7/// Boxed error type for wrapping arbitrary errors
8pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
9
10/// Client error type for all XRPC client operations
11#[derive(Debug, thiserror::Error, miette::Diagnostic)]
12#[error("{kind}")]
13pub struct ClientError {
14    #[diagnostic_source]
15    kind: ClientErrorKind,
16    #[source]
17    source: Option<BoxError>,
18    #[help]
19    help: Option<SmolStr>,
20    context: Option<SmolStr>,
21    url: Option<SmolStr>,
22    details: Option<SmolStr>,
23    location: Option<SmolStr>,
24}
25
26/// Error categories for client operations
27#[derive(Debug, thiserror::Error, miette::Diagnostic)]
28pub enum ClientErrorKind {
29    /// HTTP transport error (connection, timeout, etc.)
30    #[error("transport error")]
31    #[diagnostic(code(jacquard::client::transport))]
32    Transport,
33
34    /// Request validation/construction failed
35    #[error("invalid request: {0}")]
36    #[diagnostic(
37        code(jacquard::client::invalid_request),
38        help("check request parameters and format")
39    )]
40    InvalidRequest(SmolStr),
41
42    /// Request serialization failed
43    #[error("encode error: {0}")]
44    #[diagnostic(
45        code(jacquard::client::encode),
46        help("check request body format and encoding")
47    )]
48    Encode(SmolStr),
49
50    /// Response deserialization failed
51    #[error("decode error: {0}")]
52    #[diagnostic(
53        code(jacquard::client::decode),
54        help("check response format and encoding")
55    )]
56    Decode(SmolStr),
57
58    /// HTTP error response (non-200 status)
59    #[error("HTTP {status}")]
60    #[diagnostic(code(jacquard::client::http))]
61    Http {
62        /// HTTP status code
63        status: http::StatusCode,
64    },
65
66    /// Authentication/authorization error
67    #[error("auth error: {0}")]
68    #[diagnostic(code(jacquard::client::auth))]
69    Auth(AuthError),
70
71    /// Identity resolution error (handle→DID, DID→Doc)
72    #[error("identity resolution failed")]
73    #[diagnostic(
74        code(jacquard::client::identity_resolution),
75        help("check handle/DID is valid and network is accessible")
76    )]
77    IdentityResolution,
78
79    /// Storage/persistence error
80    #[error("storage error")]
81    #[diagnostic(
82        code(jacquard::client::storage),
83        help("check storage backend is accessible and has sufficient permissions")
84    )]
85    Storage,
86}
87
88impl ClientError {
89    /// Create a new error with the given kind and optional source
90    pub fn new(kind: ClientErrorKind, source: Option<BoxError>) -> Self {
91        Self {
92            kind,
93            source,
94            help: None,
95            context: None,
96            url: None,
97            details: None,
98            location: None,
99        }
100    }
101
102    /// Get the error kind
103    pub fn kind(&self) -> &ClientErrorKind {
104        &self.kind
105    }
106
107    /// Get the source error if present
108    pub fn source_err(&self) -> Option<&BoxError> {
109        self.source.as_ref()
110    }
111
112    /// Get the context string if present
113    pub fn context(&self) -> Option<&str> {
114        self.context.as_ref().map(|s| s.as_str())
115    }
116
117    /// Get the URL if present
118    pub fn url(&self) -> Option<&str> {
119        self.url.as_ref().map(|s| s.as_str())
120    }
121
122    /// Get the details if present
123    pub fn details(&self) -> Option<&str> {
124        self.details.as_ref().map(|s| s.as_str())
125    }
126
127    /// Get the location if present
128    pub fn location(&self) -> Option<&str> {
129        self.location.as_ref().map(|s| s.as_str())
130    }
131
132    /// Add help text to this error
133    pub fn with_help(mut self, help: impl Into<SmolStr>) -> Self {
134        self.help = Some(help.into());
135        self
136    }
137
138    /// Add context to this error
139    pub fn with_context(mut self, context: impl Into<SmolStr>) -> Self {
140        self.context = Some(context.into());
141        self
142    }
143
144    /// Add URL to this error
145    pub fn with_url(mut self, url: impl Into<SmolStr>) -> Self {
146        self.url = Some(url.into());
147        self
148    }
149
150    /// Add details to this error
151    pub fn with_details(mut self, details: impl Into<SmolStr>) -> Self {
152        self.details = Some(details.into());
153        self
154    }
155
156    /// Add location to this error
157    pub fn with_location(mut self, location: impl Into<SmolStr>) -> Self {
158        self.location = Some(location.into());
159        self
160    }
161
162    // Constructors for each kind
163
164    /// Create a transport error
165    pub fn transport(source: impl std::error::Error + Send + Sync + 'static) -> Self {
166        Self::new(ClientErrorKind::Transport, Some(Box::new(source)))
167    }
168
169    /// Create an invalid request error
170    pub fn invalid_request(msg: impl Into<SmolStr>) -> Self {
171        Self::new(ClientErrorKind::InvalidRequest(msg.into()), None)
172    }
173
174    /// Create an encode error
175    pub fn encode(msg: impl Into<SmolStr>) -> Self {
176        Self::new(ClientErrorKind::Encode(msg.into()), None)
177    }
178
179    /// Create a decode error
180    pub fn decode(msg: impl Into<SmolStr>) -> Self {
181        Self::new(ClientErrorKind::Decode(msg.into()), None)
182    }
183
184    /// Create an HTTP error with status code and optional body
185    pub fn http(status: http::StatusCode, body: Option<Bytes>) -> Self {
186        let http_err = HttpError { status, body };
187        Self::new(ClientErrorKind::Http { status }, Some(Box::new(http_err)))
188    }
189
190    /// Create an authentication error
191    pub fn auth(auth_error: AuthError) -> Self {
192        Self::new(ClientErrorKind::Auth(auth_error), None)
193    }
194
195    /// Create an identity resolution error
196    pub fn identity_resolution(source: impl std::error::Error + Send + Sync + 'static) -> Self {
197        Self::new(ClientErrorKind::IdentityResolution, Some(Box::new(source)))
198    }
199
200    /// Create a storage error
201    pub fn storage(source: impl std::error::Error + Send + Sync + 'static) -> Self {
202        Self::new(ClientErrorKind::Storage, Some(Box::new(source)))
203    }
204}
205
206/// Result type for client operations
207pub type XrpcResult<T> = std::result::Result<T, ClientError>;
208
209// ============================================================================
210// Old error types (deprecated)
211// ============================================================================
212
213/// Transport-level errors that occur during HTTP communication
214// #[deprecated(since = "0.8.0", note = "Use ClientError::transport() instead")]
215// #[derive(Debug, thiserror::Error, miette::Diagnostic)]
216// pub enum TransportError {
217//     /// Failed to establish connection to server
218//     #[error("Connection error: {0}")]
219//     Connect(String),
220
221//     /// Request timed out
222//     #[error("Request timeout")]
223//     Timeout,
224
225//     /// Request construction failed (malformed URI, headers, etc.)
226//     #[error("Invalid request: {0}")]
227//     InvalidRequest(String),
228
229//     /// Other transport error
230//     #[error("Transport error: {0}")]
231//     Other(Box<dyn std::error::Error + Send + Sync>),
232// }
233
234/// Response deserialization errors
235///
236/// Preserves detailed error information from various deserialization backends.
237/// Can be converted to string for serialization while maintaining the full error context.
238#[derive(Debug, thiserror::Error, miette::Diagnostic)]
239pub enum DecodeError {
240    /// JSON deserialization failed
241    #[error("Failed to deserialize JSON: {0}")]
242    Json(
243        #[from]
244        #[source]
245        serde_json::Error,
246    ),
247    /// CBOR deserialization failed (local I/O)
248    #[error("Failed to deserialize CBOR: {0}")]
249    CborLocal(
250        #[from]
251        #[source]
252        serde_ipld_dagcbor::DecodeError<std::io::Error>,
253    ),
254    /// CBOR deserialization failed (remote/reqwest)
255    #[error("Failed to deserialize CBOR: {0}")]
256    CborRemote(
257        #[from]
258        #[source]
259        serde_ipld_dagcbor::DecodeError<HttpError>,
260    ),
261    /// DAG-CBOR deserialization failed (in-memory, e.g., WebSocket frames)
262    #[error("Failed to deserialize DAG-CBOR: {0}")]
263    DagCborInfallible(
264        #[from]
265        #[source]
266        serde_ipld_dagcbor::DecodeError<std::convert::Infallible>,
267    ),
268    /// CBOR header deserialization failed (framed WebSocket messages)
269    #[cfg(feature = "websocket")]
270    #[error("Failed to deserialize cbor header: {0}")]
271    CborHeader(
272        #[from]
273        #[source]
274        ciborium::de::Error<std::io::Error>,
275    ),
276
277    /// Unknown event type in framed message
278    #[cfg(feature = "websocket")]
279    #[error("Unknown event type: {0}")]
280    UnknownEventType(smol_str::SmolStr),
281}
282
283/// HTTP error response (non-200 status codes outside of XRPC error handling)
284#[derive(Debug, thiserror::Error, miette::Diagnostic)]
285pub struct HttpError {
286    /// HTTP status code
287    pub status: http::StatusCode,
288    /// Response body if available
289    pub body: Option<Bytes>,
290}
291
292impl std::fmt::Display for HttpError {
293    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294        write!(f, "HTTP {}", self.status)?;
295        if let Some(body) = &self.body {
296            if let Ok(s) = std::str::from_utf8(body) {
297                write!(f, ":\n{}", s)?;
298            }
299        }
300        Ok(())
301    }
302}
303
304/// Authentication and authorization errors
305#[derive(Debug, thiserror::Error, miette::Diagnostic)]
306pub enum AuthError {
307    /// Access token has expired (use refresh token to get a new one)
308    #[error("Access token expired")]
309    TokenExpired,
310
311    /// Access token is invalid or malformed
312    #[error("Invalid access token")]
313    InvalidToken,
314
315    /// Token refresh request failed
316    #[error("Token refresh failed")]
317    RefreshFailed,
318
319    /// Request requires authentication but none was provided
320    #[error("No authentication provided, but endpoint requires auth")]
321    NotAuthenticated,
322
323    /// Other authentication error
324    #[error("Authentication error: {0:?}")]
325    Other(http::HeaderValue),
326}
327
328impl crate::IntoStatic for AuthError {
329    type Output = AuthError;
330
331    fn into_static(self) -> Self::Output {
332        match self {
333            AuthError::TokenExpired => AuthError::TokenExpired,
334            AuthError::InvalidToken => AuthError::InvalidToken,
335            AuthError::RefreshFailed => AuthError::RefreshFailed,
336            AuthError::NotAuthenticated => AuthError::NotAuthenticated,
337            AuthError::Other(header) => AuthError::Other(header),
338        }
339    }
340}
341
342// ============================================================================
343// Conversions from old to new
344// ============================================================================
345
346#[allow(deprecated)]
347// impl From<TransportError> for ClientError {
348//     fn from(e: TransportError) -> Self {
349//         Self::transport(e)
350//     }
351// }
352
353impl From<DecodeError> for ClientError {
354    fn from(e: DecodeError) -> Self {
355        let msg = smol_str::format_smolstr!("{:?}", e);
356        Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
357            .with_context("response deserialization failed")
358    }
359}
360
361impl From<HttpError> for ClientError {
362    fn from(e: HttpError) -> Self {
363        Self::http(e.status, e.body)
364    }
365}
366
367impl From<AuthError> for ClientError {
368    fn from(e: AuthError) -> Self {
369        Self::auth(e)
370    }
371}
372
373impl From<EncodeError> for ClientError {
374    fn from(e: EncodeError) -> Self {
375        let msg = smol_str::format_smolstr!("{:?}", e);
376        Self::new(ClientErrorKind::Encode(msg), Some(Box::new(e)))
377            .with_context("request encoding failed")
378    }
379}
380
381// Platform-specific conversions
382#[cfg(feature = "reqwest-client")]
383impl From<reqwest::Error> for ClientError {
384    #[cfg(not(target_arch = "wasm32"))]
385    fn from(e: reqwest::Error) -> Self {
386        Self::transport(e)
387    }
388
389    #[cfg(target_arch = "wasm32")]
390    fn from(e: reqwest::Error) -> Self {
391        Self::transport(e)
392    }
393}
394
395// Serde error conversions
396impl From<serde_json::Error> for ClientError {
397    fn from(e: serde_json::Error) -> Self {
398        let msg = smol_str::format_smolstr!("{:?}", e);
399        Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
400            .with_context("JSON deserialization failed")
401    }
402}
403
404impl From<serde_ipld_dagcbor::DecodeError<std::io::Error>> for ClientError {
405    fn from(e: serde_ipld_dagcbor::DecodeError<std::io::Error>) -> Self {
406        let msg = smol_str::format_smolstr!("{:?}", e);
407        Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
408            .with_context("DAG-CBOR deserialization failed (local I/O)")
409    }
410}
411
412impl From<serde_ipld_dagcbor::DecodeError<HttpError>> for ClientError {
413    fn from(e: serde_ipld_dagcbor::DecodeError<HttpError>) -> Self {
414        let msg = smol_str::format_smolstr!("{:?}", e);
415        Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
416            .with_context("DAG-CBOR deserialization failed (remote)")
417    }
418}
419
420impl From<serde_ipld_dagcbor::DecodeError<std::convert::Infallible>> for ClientError {
421    fn from(e: serde_ipld_dagcbor::DecodeError<std::convert::Infallible>) -> Self {
422        let msg = smol_str::format_smolstr!("{:?}", e);
423        Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
424            .with_context("DAG-CBOR deserialization failed (in-memory)")
425    }
426}
427
428#[cfg(feature = "websocket")]
429impl From<ciborium::de::Error<std::io::Error>> for ClientError {
430    fn from(e: ciborium::de::Error<std::io::Error>) -> Self {
431        let msg = smol_str::format_smolstr!("{:?}", e);
432        Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
433            .with_context("CBOR header deserialization failed")
434    }
435}
436
437// Session store errors
438impl From<crate::session::SessionStoreError> for ClientError {
439    fn from(e: crate::session::SessionStoreError) -> Self {
440        Self::storage(e)
441    }
442}
443
444// URL parse errors
445impl From<url::ParseError> for ClientError {
446    fn from(e: url::ParseError) -> Self {
447        Self::invalid_request(e.to_string())
448    }
449}