Skip to main content

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