pincer_core/
error.rs

1//! Error types for pincer.
2
3use derive_more::{Display, Error, From};
4
5// ============================================================================
6// Error Decoder Trait
7// ============================================================================
8
9/// Trait for decoding HTTP error responses into typed errors.
10///
11/// Implement this trait to customize how error responses are handled.
12/// The decoder receives the HTTP status code and response body, and can
13/// optionally return a decoded error.
14///
15/// # Example
16///
17/// ```ignore
18/// use pincer::ErrorDecoder;
19///
20/// #[derive(Debug, Deserialize)]
21/// struct ApiError {
22///     code: String,
23///     message: String,
24/// }
25///
26/// struct MyErrorDecoder;
27///
28/// impl ErrorDecoder for MyErrorDecoder {
29///     type Error = ApiError;
30///
31///     fn decode(&self, status: u16, body: &bytes::Bytes) -> Option<Self::Error> {
32///         if status >= 400 {
33///             serde_json::from_slice(body).ok()
34///         } else {
35///             None
36///         }
37///     }
38/// }
39/// ```
40pub trait ErrorDecoder: Send + Sync + 'static {
41    /// The decoded error type.
42    type Error: std::fmt::Debug + Send + Sync + 'static;
43
44    /// Decode an HTTP error response into a typed error.
45    ///
46    /// Returns `Some(error)` if the response should be decoded as a custom error,
47    /// or `None` to fall back to the default `Error::Http` handling.
48    fn decode(&self, status: u16, body: &bytes::Bytes) -> Option<Self::Error>;
49}
50
51/// Default error decoder that always returns `None`.
52///
53/// This is the default decoder used when no custom decoder is specified.
54/// It simply falls back to the standard `Error::Http` handling.
55#[derive(Debug, Clone, Copy, Default)]
56pub struct DefaultErrorDecoder;
57
58impl ErrorDecoder for DefaultErrorDecoder {
59    type Error = std::convert::Infallible;
60
61    fn decode(&self, _status: u16, _body: &bytes::Bytes) -> Option<Self::Error> {
62        None
63    }
64}
65
66// ============================================================================
67// Error Type
68// ============================================================================
69
70/// Main error type for pincer operations.
71#[derive(Debug, Display, Error, From)]
72pub enum Error {
73    /// HTTP-level errors (non-2xx status codes).
74    #[display("HTTP error {status}: {message}")]
75    #[from(skip)]
76    Http {
77        /// HTTP status code.
78        status: u16,
79        /// Error message.
80        message: String,
81        /// Response body, if available.
82        #[error(not(source))]
83        body: Option<bytes::Bytes>,
84    },
85
86    /// Network/connection errors.
87    #[display("connection error: {_0}")]
88    #[from(skip)]
89    Connection(#[error(not(source))] String),
90
91    /// TLS/SSL errors.
92    #[display("TLS error: {_0}")]
93    #[from(skip)]
94    Tls(#[error(not(source))] String),
95
96    /// Request timeout.
97    #[display("request timeout")]
98    #[from(skip)]
99    Timeout,
100
101    /// Invalid request configuration.
102    #[display("invalid request: {_0}")]
103    #[from(skip)]
104    InvalidRequest(#[error(not(source))] String),
105
106    /// JSON serialization error.
107    #[display("JSON serialization error: {_0}")]
108    #[from]
109    JsonSerialization(serde_json::Error),
110
111    /// JSON deserialization error with path context.
112    #[display("JSON deserialization error at '{path}': {message}")]
113    #[from(skip)]
114    JsonDeserialization {
115        /// JSON path to the error (e.g., "user.address.city").
116        path: String,
117        /// Error message.
118        message: String,
119    },
120
121    /// Form URL-encoded serialization error.
122    #[display("form serialization error: {_0}")]
123    #[from]
124    FormSerialization(serde_urlencoded::ser::Error),
125
126    /// Query string serialization error.
127    #[display("query serialization error: {_0}")]
128    #[from]
129    QuerySerialization(serde_html_form::ser::Error),
130
131    /// URL parsing error.
132    #[display("invalid URL: {_0}")]
133    #[from]
134    InvalidUrl(url::ParseError),
135
136    /// Too many redirects.
137    #[display("too many redirects ({count} exceeded max of {max})")]
138    #[from(skip)]
139    TooManyRedirects {
140        /// Number of redirects followed.
141        count: usize,
142        /// Maximum allowed redirects.
143        max: usize,
144    },
145
146    /// Invalid redirect response.
147    #[display("invalid redirect: {_0}")]
148    #[from(skip)]
149    InvalidRedirect(#[error(not(source))] String),
150}
151
152/// Result type alias using [`crate::Error`].
153pub type Result<T> = std::result::Result<T, Error>;
154
155impl Error {
156    /// Create an HTTP error from status code and message.
157    #[must_use]
158    pub fn http(status: u16, message: impl Into<String>) -> Self {
159        Self::Http {
160            status,
161            message: message.into(),
162            body: None,
163        }
164    }
165
166    /// Create an HTTP error with body.
167    #[must_use]
168    pub fn http_with_body(status: u16, message: impl Into<String>, body: bytes::Bytes) -> Self {
169        Self::Http {
170            status,
171            message: message.into(),
172            body: Some(body),
173        }
174    }
175
176    /// Create a connection error.
177    #[must_use]
178    pub fn connection(message: impl Into<String>) -> Self {
179        Self::Connection(message.into())
180    }
181
182    /// Create a TLS error.
183    #[must_use]
184    pub fn tls(message: impl Into<String>) -> Self {
185        Self::Tls(message.into())
186    }
187
188    /// Create an invalid request error.
189    #[must_use]
190    pub fn invalid_request(message: impl Into<String>) -> Self {
191        Self::InvalidRequest(message.into())
192    }
193
194    /// Create a JSON deserialization error with path context.
195    #[must_use]
196    pub fn json_deserialization(path: impl Into<String>, message: impl Into<String>) -> Self {
197        Self::JsonDeserialization {
198            path: path.into(),
199            message: message.into(),
200        }
201    }
202
203    /// Returns `true` if this is a timeout error.
204    #[must_use]
205    pub const fn is_timeout(&self) -> bool {
206        matches!(self, Self::Timeout)
207    }
208
209    /// Returns `true` if this is a connection error.
210    #[must_use]
211    pub const fn is_connection(&self) -> bool {
212        matches!(self, Self::Connection(_))
213    }
214
215    /// Returns the HTTP status code if this is an HTTP error.
216    #[must_use]
217    pub const fn status(&self) -> Option<u16> {
218        match self {
219            Self::Http { status, .. } => Some(*status),
220            _ => None,
221        }
222    }
223
224    /// Returns `true` if this is a client error (4xx).
225    #[must_use]
226    pub fn is_client_error(&self) -> bool {
227        self.status().is_some_and(|s| (400..500).contains(&s))
228    }
229
230    /// Returns `true` if this is a server error (5xx).
231    #[must_use]
232    pub fn is_server_error(&self) -> bool {
233        self.status().is_some_and(|s| (500..600).contains(&s))
234    }
235
236    /// Returns `true` if this is a 404 Not Found error.
237    #[must_use]
238    pub fn is_not_found(&self) -> bool {
239        self.status() == Some(404)
240    }
241
242    /// Returns the response body if this is an HTTP error with a body.
243    #[must_use]
244    pub fn body(&self) -> Option<&bytes::Bytes> {
245        match self {
246            Self::Http { body, .. } => body.as_ref(),
247            _ => None,
248        }
249    }
250
251    /// Try to decode the HTTP error body as JSON.
252    ///
253    /// Returns `Some(Ok(value))` if the error has a body and it deserializes successfully,
254    /// `Some(Err(error))` if the body exists but deserialization fails,
255    /// or `None` if there is no body or this is not an HTTP error.
256    ///
257    /// # Example
258    ///
259    /// ```ignore
260    /// #[derive(Debug, Deserialize)]
261    /// struct ApiError {
262    ///     code: String,
263    ///     message: String,
264    /// }
265    ///
266    /// match client.get_user(123).await {
267    ///     Ok(user) => println!("User: {:?}", user),
268    ///     Err(e) => {
269    ///         if let Some(Ok(api_error)) = e.decode_body::<ApiError>() {
270    ///             println!("API error: {} - {}", api_error.code, api_error.message);
271    ///         } else {
272    ///             println!("Error: {}", e);
273    ///         }
274    ///     }
275    /// }
276    /// ```
277    pub fn decode_body<T: serde::de::DeserializeOwned>(&self) -> Option<Result<T>> {
278        self.body().map(|body| crate::from_json(body))
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn error_display() {
288        let err = Error::http(404, "Not Found");
289        assert_eq!(err.to_string(), "HTTP error 404: Not Found");
290
291        let err = Error::Timeout;
292        assert_eq!(err.to_string(), "request timeout");
293
294        let err = Error::connection("failed to connect");
295        assert_eq!(err.to_string(), "connection error: failed to connect");
296
297        let err = Error::json_deserialization("user.address.city", "missing field `city`");
298        assert_eq!(
299            err.to_string(),
300            "JSON deserialization error at 'user.address.city': missing field `city`"
301        );
302    }
303
304    #[test]
305    fn error_status() {
306        let err = Error::http(404, "Not Found");
307        assert_eq!(err.status(), Some(404));
308        assert!(err.is_client_error());
309        assert!(!err.is_server_error());
310
311        let err = Error::http(500, "Internal Server Error");
312        assert_eq!(err.status(), Some(500));
313        assert!(!err.is_client_error());
314        assert!(err.is_server_error());
315
316        let err = Error::Timeout;
317        assert_eq!(err.status(), None);
318        assert!(!err.is_client_error());
319        assert!(!err.is_server_error());
320    }
321
322    #[test]
323    fn error_is_timeout() {
324        assert!(Error::Timeout.is_timeout());
325        assert!(!Error::http(404, "Not Found").is_timeout());
326    }
327
328    #[test]
329    fn error_is_connection() {
330        assert!(Error::connection("failed").is_connection());
331        assert!(!Error::Timeout.is_connection());
332    }
333
334    #[test]
335    fn error_is_not_found() {
336        assert!(Error::http(404, "Not Found").is_not_found());
337        assert!(!Error::http(400, "Bad Request").is_not_found());
338        assert!(!Error::Timeout.is_not_found());
339    }
340
341    #[test]
342    fn error_body() {
343        let err = Error::http(404, "Not Found");
344        assert!(err.body().is_none());
345
346        let body = bytes::Bytes::from(r#"{"error": "not found"}"#);
347        let err = Error::http_with_body(404, "Not Found", body.clone());
348        assert_eq!(err.body(), Some(&body));
349
350        assert!(Error::Timeout.body().is_none());
351    }
352
353    #[test]
354    fn error_decode_body() {
355        #[derive(Debug, PartialEq, serde::Deserialize)]
356        struct ApiError {
357            error: String,
358        }
359
360        let body = bytes::Bytes::from(r#"{"error": "not found"}"#);
361        let err = Error::http_with_body(404, "Not Found", body);
362
363        let decoded = err.decode_body::<ApiError>();
364        assert!(decoded.is_some());
365        let result = decoded.expect("should have body");
366        assert!(result.is_ok());
367        assert_eq!(
368            result.expect("should decode"),
369            ApiError {
370                error: "not found".to_string()
371            }
372        );
373
374        // No body
375        let err = Error::http(404, "Not Found");
376        assert!(err.decode_body::<ApiError>().is_none());
377
378        // Non-HTTP error
379        assert!(Error::Timeout.decode_body::<ApiError>().is_none());
380    }
381
382    #[test]
383    fn default_error_decoder() {
384        let decoder = DefaultErrorDecoder;
385        let body = bytes::Bytes::from("test");
386        assert!(decoder.decode(404, &body).is_none());
387        assert!(decoder.decode(500, &body).is_none());
388    }
389}