anyhow_http/
http_error.rs

1use anyhow::anyhow;
2use core::fmt;
3use http::header::IntoHeaderName;
4use serde::de::DeserializeOwned;
5use serde::Serialize;
6use std::error::Error as StdError;
7use std::{borrow::Cow, collections::HashMap};
8
9use http::{HeaderMap, HeaderValue, StatusCode};
10
11/// [`HttpError`] is an error that encapsulates data to generate Http error responses.
12pub struct HttpError {
13    pub(crate) status_code: StatusCode,
14    pub(crate) reason: Option<Cow<'static, str>>,
15    pub(crate) source: Option<anyhow::Error>,
16    pub(crate) data: Option<HashMap<String, serde_json::Value>>,
17    pub(crate) headers: Option<HeaderMap>,
18}
19
20impl fmt::Debug for HttpError {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        write!(
23            f,
24            "HttpError\nStatus: {status_code}\nReason: {reason:?}\nData: {data:?}\nHeaders: {headers:?}\n\nSource: {source:?}",
25            status_code = self.status_code,
26            reason = self.reason,
27            data = self.data,
28            headers = self.headers,
29            source = self.source
30        )
31    }
32}
33
34impl fmt::Display for HttpError {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        match (&self.reason, &self.source) {
37            (None, None) => write!(f, "HttpError({})", self.status_code),
38            (Some(r), None) => write!(f, "HttpError({}): {r}", self.status_code),
39            (None, Some(s)) if f.alternate() => {
40                write!(f, "HttpError({}): source: {s:#}", self.status_code)
41            }
42            (None, Some(s)) => write!(f, "HttpError({}): source: {s}", self.status_code),
43            (Some(r), Some(s)) if f.alternate() => {
44                write!(f, "HttpError({}): {r}, source: {s:#}", self.status_code)
45            }
46            (Some(r), Some(s)) => write!(f, "HttpError({}): {r}, source: {s}", self.status_code),
47        }
48    }
49}
50
51impl StdError for HttpError {
52    fn source(&self) -> Option<&(dyn StdError + 'static)> {
53        self.source
54            .as_deref()
55            .map(|e| e as &(dyn StdError + 'static))
56    }
57}
58
59#[allow(clippy::derivable_impls)]
60impl Default for HttpError {
61    fn default() -> Self {
62        Self { ..Self::new() }
63    }
64}
65
66impl PartialEq for HttpError {
67    fn eq(&self, other: &Self) -> bool {
68        self.status_code == other.status_code
69            && self.reason == other.reason
70            && self.data == other.data
71    }
72}
73
74impl HttpError {
75    /// Creates an empty [`HttpError`] with status 500.
76    pub const fn new() -> Self {
77        Self {
78            status_code: StatusCode::INTERNAL_SERVER_ERROR,
79            reason: None,
80            source: None,
81            data: None,
82            headers: None,
83        }
84    }
85
86    /// Creates a [`HttpError`] with status code and reason. This constructor can be evaluated at
87    /// compile time.
88    ///
89    /// ```
90    /// use anyhow_http::HttpError;
91    ///
92    /// const BAD_REQUEST: HttpError =
93    ///     HttpError::from_static(http::StatusCode::BAD_REQUEST, "invalid request");
94    /// ```
95    pub const fn from_static(status_code: StatusCode, reason: &'static str) -> Self {
96        Self {
97            status_code,
98            reason: Some(Cow::Borrowed(reason)),
99            source: None,
100            data: None,
101            headers: None,
102        }
103    }
104
105    /// Creates a [`HttpError`] from a status code.
106    pub const fn from_status_code(status_code: StatusCode) -> Self {
107        let mut http_err = Self::new();
108        http_err.status_code = status_code;
109        http_err
110    }
111
112    /// Sets the status code.
113    pub const fn with_status_code(mut self, status_code: StatusCode) -> Self {
114        self.status_code = status_code;
115        self
116    }
117
118    /// Sets the error reason.
119    pub fn with_reason<S: Into<Cow<'static, str>>>(mut self, reason: S) -> Self {
120        self.reason = Some(reason.into());
121        self
122    }
123
124    /// Adds context to the source error. If no source is availabe a new [`anyhow::Error`] is
125    /// created in its place.
126    pub fn with_source_context<C>(mut self, context: C) -> Self
127    where
128        C: fmt::Display + Send + Sync + 'static,
129    {
130        let source = match self.source {
131            Some(s) => s.context(context),
132            None => anyhow!("{context}"),
133        };
134        self.source = Some(source);
135        self
136    }
137
138    /// Set the source error from a generic error trait object.
139    pub fn with_boxed_source_err(mut self, err: Box<dyn StdError + Send + Sync + 'static>) -> Self {
140        self.source = Some(anyhow!("{err}"));
141        self
142    }
143
144    /// Set the source error from a generic error.
145    pub fn with_source_err<E>(mut self, err: E) -> Self
146    where
147        E: Into<anyhow::Error>,
148    {
149        self.source = Some(err.into());
150        self
151    }
152
153    /// Append to to the inner data based on one or more key-value pairs.
154    ///
155    /// ```
156    /// use anyhow_http::HttpError;
157    ///
158    /// let err: HttpError = HttpError::default()
159    ///     .with_data([("key1", 1234), ("key2", 5678)])
160    ///     .unwrap();
161    /// ```
162    pub fn with_data<I, K, V>(mut self, values: I) -> Option<Self>
163    where
164        I: IntoIterator<Item = (K, V)>,
165        K: Into<String>,
166        V: Serialize + Sync + Send + 'static,
167    {
168        let iter = values
169            .into_iter()
170            .map(|(k, v)| Some((k.into(), serde_json::to_value(v).ok()?)));
171
172        self.data = self
173            .data
174            .get_or_insert_with(HashMap::new)
175            .clone()
176            .into_iter()
177            .map(Option::Some)
178            .chain(iter)
179            .collect();
180
181        Some(self)
182    }
183
184    /// Adds a key-pair value to the inner data.
185    pub fn with_key_value<K, V>(mut self, key: K, value: V) -> Self
186    where
187        K: Into<String>,
188        V: Serialize + Sync + Send + 'static,
189    {
190        let Ok(value) = serde_json::to_value(value) else {
191            return self;
192        };
193        self.data
194            .get_or_insert_with(HashMap::new)
195            .insert(key.into(), value);
196        self
197    }
198
199    pub fn with_header<K>(mut self, header_key: K, header_value: HeaderValue) -> Self
200    where
201        K: IntoHeaderName,
202    {
203        self.headers
204            .get_or_insert_with(HeaderMap::new)
205            .insert(header_key, header_value);
206        self
207    }
208
209    /// Retrieves a key-pair value from the inner data.
210    pub fn get<V>(&self, key: impl AsRef<str>) -> Option<V>
211    where
212        V: DeserializeOwned + Send + Sync,
213    {
214        self.data
215            .as_ref()
216            .and_then(|d| d.get(key.as_ref()))
217            .and_then(|v| serde_json::from_value(v.clone()).ok())
218    }
219
220    /// Sets a key-pair value pair into the inner data.
221    pub fn set<K, V>(&mut self, key: K, value: V) -> serde_json::Result<()>
222    where
223        K: Into<String>,
224        V: Serialize + Sync + Send + 'static,
225    {
226        let value = serde_json::to_value(value)?;
227        self.data
228            .get_or_insert_with(HashMap::new)
229            .insert(key.into(), value);
230        Ok(())
231    }
232
233    /// Returns the status code.
234    pub fn status_code(&self) -> StatusCode {
235        self.status_code
236    }
237
238    /// Returns the error reason if any.
239    pub fn reason(&self) -> Option<Cow<'static, str>> {
240        self.reason.clone()
241    }
242
243    /// Returns the source error if any.
244    pub fn source(&self) -> Option<&anyhow::Error> {
245        self.source.as_ref()
246    }
247
248    /// Returns the error response headers if any.
249    pub fn headers(&self) -> Option<&HeaderMap> {
250        self.headers.as_ref()
251    }
252
253    /// Creates a [`HttpError`] from a generic error. It attempts to downcast to an underlying
254    /// [`HttpError`].
255    pub fn from_err<E>(err: E) -> Self
256    where
257        E: Into<anyhow::Error>,
258    {
259        let err = err.into();
260        match err.downcast::<HttpError>() {
261            Ok(http_error) => http_error,
262            Err(err) => Self {
263                source: Some(err),
264                ..Self::default()
265            },
266        }
267    }
268
269    pub fn into_boxed(self) -> Box<dyn StdError + Send + Sync + 'static> {
270        self.into()
271    }
272}
273
274impl From<anyhow::Error> for HttpError {
275    fn from(err: anyhow::Error) -> Self {
276        HttpError::from_err(err)
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use anyhow::anyhow;
283
284    use super::*;
285
286    #[test]
287    fn http_error_display() {
288        let e: HttpError = HttpError::default();
289        assert_eq!(e.to_string(), "HttpError(500 Internal Server Error)");
290
291        let e: HttpError = HttpError::default().with_reason("reason");
292        assert_eq!(
293            e.to_string(),
294            "HttpError(500 Internal Server Error): reason"
295        );
296
297        let e: HttpError = HttpError::default().with_source_err(anyhow!("error"));
298        assert_eq!(
299            e.to_string(),
300            "HttpError(500 Internal Server Error): source: error"
301        );
302
303        let e: HttpError = HttpError::default()
304            .with_reason("reason")
305            .with_source_err(anyhow!("error"));
306        assert_eq!(
307            e.to_string(),
308            "HttpError(500 Internal Server Error): reason, source: error"
309        );
310    }
311
312    #[test]
313    fn http_error_new_const() {
314        const ERR: HttpError = HttpError::new();
315        assert_eq!(ERR.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
316    }
317
318    #[test]
319    fn http_error_from_static() {
320        const ERR: HttpError = HttpError::from_static(StatusCode::BAD_REQUEST, "invalid request");
321        assert_eq!(ERR.status_code(), StatusCode::BAD_REQUEST);
322        assert_eq!(ERR.reason(), Some("invalid request".into()));
323    }
324
325    #[test]
326    fn http_error_default() {
327        let e: HttpError = HttpError::default();
328        assert_eq!(e.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
329    }
330
331    #[test]
332    fn http_error_from_status_code() {
333        let e: HttpError = HttpError::from_status_code(StatusCode::BAD_REQUEST);
334        assert_eq!(e.status_code(), StatusCode::BAD_REQUEST);
335    }
336
337    #[test]
338    fn http_error_from_err() {
339        let e: HttpError = HttpError::from_err(anyhow!("error"));
340        assert_eq!(e.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
341        assert_eq!(e.source().unwrap().to_string(), "error");
342
343        let e: HttpError = HttpError::from_err(fmt::Error);
344        assert_eq!(e.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
345        assert_eq!(e.source().unwrap().to_string(), fmt::Error.to_string());
346    }
347
348    #[derive(Debug)]
349    struct GenericError;
350    impl std::fmt::Display for GenericError {
351        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
352            write!(f, "CustomError")
353        }
354    }
355
356    impl From<GenericError> for HttpError {
357        fn from(_: GenericError) -> Self {
358            Self::default().with_status_code(StatusCode::BAD_REQUEST)
359        }
360    }
361
362    impl From<GenericError> for anyhow::Error {
363        fn from(_: GenericError) -> Self {
364            HttpError::default()
365                .with_status_code(StatusCode::BAD_REQUEST)
366                .into()
367        }
368    }
369
370    #[test]
371    fn http_error_from_custom_impl_try() {
372        let res: std::result::Result<(), HttpError> = (|| {
373            Err(GenericError)?;
374            unreachable!()
375        })();
376        let e = res.unwrap_err();
377        assert_eq!(e.status_code(), StatusCode::BAD_REQUEST);
378    }
379
380    #[test]
381    fn http_error_into_anyhow() {
382        let res: anyhow::Result<()> = (|| {
383            Err(GenericError)?;
384            unreachable!()
385        })();
386        let e = res.unwrap_err();
387        assert_eq!(
388            HttpError::from_err(e).status_code(),
389            StatusCode::BAD_REQUEST
390        );
391    }
392
393    #[test]
394    fn http_error_with_status_code() {
395        let e: HttpError = HttpError::default().with_status_code(StatusCode::BAD_REQUEST);
396        assert_eq!(e.status_code(), StatusCode::BAD_REQUEST);
397    }
398
399    #[test]
400    fn http_error_with_reason() {
401        let e: HttpError = HttpError::default().with_reason("reason");
402        assert_eq!(e.reason(), Some("reason".into()));
403    }
404
405    #[test]
406    fn http_error_with_source_context() {
407        let e: HttpError = HttpError::default().with_source_context("context");
408        assert_eq!(e.source().map(ToString::to_string), Some("context".into()));
409
410        let e: HttpError = HttpError::default()
411            .with_source_err(anyhow!("source"))
412            .with_source_context("context");
413        assert_eq!(format!("{:#}", e.source().unwrap()), "context: source");
414    }
415
416    #[test]
417    fn http_error_with_dyn_source_error() {
418        let dyn_err = Box::new(fmt::Error) as Box<dyn StdError + Send + Sync + 'static>;
419        let e: HttpError = HttpError::default().with_boxed_source_err(dyn_err);
420        assert_eq!(e.source().unwrap().to_string(), fmt::Error.to_string());
421    }
422
423    #[test]
424    fn http_error_with_source_error() {
425        let e: HttpError = HttpError::default().with_source_err(fmt::Error);
426        assert_eq!(e.source().unwrap().to_string(), fmt::Error.to_string());
427    }
428
429    #[test]
430    fn http_error_data() {
431        let e: HttpError = HttpError::default().with_key_value("key", 1234);
432        assert_eq!(e.get::<i32>("key"), Some(1234));
433        assert_eq!(e.get::<String>("key"), None);
434    }
435
436    #[test]
437    fn http_error_with_data() {
438        let e: HttpError = HttpError::default()
439            .with_data([("key1", 1234), ("key2", 5678)])
440            .unwrap();
441        assert_eq!(e.get::<i32>("key1"), Some(1234));
442        assert_eq!(e.get::<i32>("key2"), Some(5678));
443    }
444
445    #[test]
446    fn http_error_set() {
447        let mut e: HttpError = HttpError::default();
448        e.set("key1", 1234).unwrap();
449        assert_eq!(e.get::<i32>("key1"), Some(1234));
450    }
451
452    #[test]
453    fn http_error_with_headers() {
454        let e: HttpError = HttpError::default()
455            .with_header(
456                http::header::CONTENT_TYPE,
457                "application/json".parse().unwrap(),
458            )
459            .with_header("x-custom-header", "42".parse().unwrap());
460        let hdrs = e.headers().unwrap();
461        assert_eq!(
462            hdrs.get(http::header::CONTENT_TYPE).unwrap(),
463            "application/json"
464        );
465        assert_eq!(hdrs.get("x-custom-header").unwrap(), "42");
466    }
467
468    #[test]
469    fn http_error_anyhow_downcast() {
470        let outer: anyhow::Error = HttpError::from_status_code(StatusCode::BAD_REQUEST).into();
471        let e = HttpError::from(outer);
472        assert_eq!(e.status_code(), StatusCode::BAD_REQUEST);
473    }
474}