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    /// DAG-CBOR deserialization failed (in-memory, e.g., WebSocket frames)
95    #[error("Failed to deserialize DAG-CBOR: {0}")]
96    DagCborInfallible(
97        #[from]
98        #[source]
99        serde_ipld_dagcbor::DecodeError<std::convert::Infallible>,
100    ),
101    /// CBOR header deserialization failed (framed WebSocket messages)
102    #[cfg(feature = "websocket")]
103    #[error("Failed to deserialize cbor header: {0}")]
104    CborHeader(
105        #[from]
106        #[source]
107        ciborium::de::Error<std::io::Error>,
108    ),
109
110    /// Unknown event type in framed message
111    #[cfg(feature = "websocket")]
112    #[error("Unknown event type: {0}")]
113    UnknownEventType(smol_str::SmolStr),
114}
115
116/// HTTP error response (non-200 status codes outside of XRPC error handling)
117#[derive(Debug, thiserror::Error, miette::Diagnostic)]
118pub struct HttpError {
119    /// HTTP status code
120    pub status: http::StatusCode,
121    /// Response body if available
122    pub body: Option<Bytes>,
123}
124
125impl std::fmt::Display for HttpError {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        write!(f, "HTTP {}", self.status)?;
128        if let Some(body) = &self.body {
129            if let Ok(s) = std::str::from_utf8(body) {
130                write!(f, ":\n{}", s)?;
131            }
132        }
133        Ok(())
134    }
135}
136
137/// Result type for client operations
138pub type XrpcResult<T> = std::result::Result<T, ClientError>;
139
140#[cfg(feature = "reqwest-client")]
141impl From<reqwest::Error> for TransportError {
142    #[cfg(not(target_arch = "wasm32"))]
143    fn from(e: reqwest::Error) -> Self {
144        if e.is_timeout() {
145            Self::Timeout
146        } else if e.is_connect() {
147            Self::Connect(e.to_string())
148        } else if e.is_builder() || e.is_request() {
149            Self::InvalidRequest(e.to_string())
150        } else {
151            Self::Other(Box::new(e))
152        }
153    }
154    #[cfg(target_arch = "wasm32")]
155    fn from(e: reqwest::Error) -> Self {
156        if e.is_timeout() {
157            Self::Timeout
158        } else if e.is_builder() || e.is_request() {
159            Self::InvalidRequest(e.to_string())
160        } else {
161            Self::Other(Box::new(e))
162        }
163    }
164}
165
166/// Authentication and authorization errors
167#[derive(Debug, thiserror::Error, miette::Diagnostic)]
168pub enum AuthError {
169    /// Access token has expired (use refresh token to get a new one)
170    #[error("Access token expired")]
171    TokenExpired,
172
173    /// Access token is invalid or malformed
174    #[error("Invalid access token")]
175    InvalidToken,
176
177    /// Token refresh request failed
178    #[error("Token refresh failed")]
179    RefreshFailed,
180
181    /// Request requires authentication but none was provided
182    #[error("No authentication provided, but endpoint requires auth")]
183    NotAuthenticated,
184
185    /// Other authentication error
186    #[error("Authentication error: {0:?}")]
187    Other(http::HeaderValue),
188}
189
190impl crate::IntoStatic for AuthError {
191    type Output = AuthError;
192
193    fn into_static(self) -> Self::Output {
194        match self {
195            AuthError::TokenExpired => AuthError::TokenExpired,
196            AuthError::InvalidToken => AuthError::InvalidToken,
197            AuthError::RefreshFailed => AuthError::RefreshFailed,
198            AuthError::NotAuthenticated => AuthError::NotAuthenticated,
199            AuthError::Other(header) => AuthError::Other(header),
200        }
201    }
202}