explicit_error_http/
error.rs

1use crate::Error;
2use erased_serde::Serialize as DynSerialize;
3use http::StatusCode;
4use serde::{Serialize, Serializer};
5
6/// Self-sufficient container to both log an error and generate its HTTP response.
7///
8/// [Error](crate::Error) implements `From<HttpError>`, use `?` and `.into()` in functions and closures to convert to the [Error::Domain] variant.
9///
10/// Note: [HttpError] convert to [Error](crate::Error) by converting first to [DomainError](crate::DomainError).
11/// # Examples
12/// Domain errors that derive [HttpError](crate::derive::HttpError) must implement `From<&MyDomainError> for HttpError`.
13/// ```rust
14/// # use http::StatusCode;
15/// # use problem_details::ProblemDetails;
16/// # use http::Uri;
17/// use explicit_error_http::{derive::HttpError, prelude::*, HttpError};
18///
19/// #[derive(HttpError, Debug)]
20/// enum MyDomainError {
21///     Foo,
22/// }
23///
24/// impl From<&MyDomainError> for HttpError {
25///     fn from(value: &MyDomainError) -> Self {
26///         match value {
27///             MyDomainError::Foo => HttpError::new(
28///                     StatusCode::BAD_REQUEST,
29///                     ProblemDetails::new()
30///                         .with_type(Uri::from_static("/errors/my-domain/foo"))
31///                         .with_title("Foo format incorrect.")
32///                 ),
33///         }
34///     }
35/// }
36/// ```
37///
38/// Domain errors cannot require to be extracted in either a struct or enum variant (eg: middleware errors).
39/// You can generate [Error::Domain] variant with an [HttpError]
40/// ```rust
41/// # use http::StatusCode;
42/// # use problem_details::ProblemDetails;
43/// # use http::Uri;
44/// use explicit_error_http::{Error, prelude::*, HttpError};
45///
46/// fn business_logic() -> Result<(), Error> {
47///     Err(HttpError::new(
48///         StatusCode::FORBIDDEN,
49///         ProblemDetails::new()
50///             .with_type(Uri::from_static("/errors/generic#forbidden"))
51///             .with_title("Forbidden."),
52///     ))?;
53///     # Ok(())
54/// }
55/// ```
56///
57/// Usually to avoid boilerplate and having consistency in error responses web applications
58/// implement helpers for frequent http error codes.
59/// ```rust
60/// # use http::StatusCode;
61/// # use problem_details::ProblemDetails;
62/// # use http::Uri;
63/// use explicit_error_http::{prelude::*, HttpError, Error};
64///
65/// fn forbidden() -> HttpError {
66///     HttpError::new(
67///         StatusCode::FORBIDDEN,
68///         ProblemDetails::new()
69///             .with_type(Uri::from_static("/errors/generic#forbidden"))
70///             .with_title("Forbidden."),
71///     )
72/// }
73///
74/// // context can be added by the caller to add information in log
75/// fn business_logic() -> Result<(), Error> {
76///     Err(42).map_err(|e|
77///         forbidden().with_context(
78///             format!("Return a forbidden instead of 500 to avoid leaking implementation details: {e}")
79///     ))?;
80///     # Ok(())
81/// }
82/// ```
83#[derive(Serialize)]
84pub struct HttpError {
85    #[serde(skip)]
86    pub http_status_code: StatusCode,
87    #[serde(flatten)]
88    pub public: Box<dyn DynSerialize + Send + Sync>,
89    #[serde(skip)]
90    pub context: Option<String>,
91}
92
93impl HttpError {
94    /// Generate an [HttpError] without a context. To add a context
95    /// use [with_context](HttpError::with_context) afterwards.
96    /// # Examples
97    /// ```rust
98    /// # use explicit_error_http::{Result, HttpError};
99    /// # use http::StatusCode;
100    /// # use problem_details::ProblemDetails;
101    /// # use http::Uri;
102    /// fn forbidden() -> HttpError {
103    ///     HttpError::new(
104    ///         StatusCode::UNAUTHORIZED,
105    ///         ProblemDetails::new()
106    ///             .with_type(Uri::from_static("/errors/forbidden"))
107    ///             .with_title("Forbidden"),
108    ///     )
109    /// }
110    /// ```
111    pub fn new<S: Serialize + 'static + Send + Sync>(
112        http_status_code: StatusCode,
113        public: S,
114    ) -> Self {
115        Self {
116            http_status_code,
117            public: Box::new(public),
118            context: None,
119        }
120    }
121
122    /// Add a context to an [HttpError], override if one was set. The context appears in display
123    /// but not in the http response.
124    /// # Examples
125    /// ```rust
126    /// # use explicit_error_http::{Result, HttpError};
127    /// # use http::StatusCode;
128    /// # use problem_details::ProblemDetails;
129    /// # use http::Uri;
130    /// fn check_authz() -> Result<()> {
131    ///     if !false {
132    ///         Err(forbidden().with_context("Some info to help debug"))?;
133    ///     }
134    ///     Ok(())
135    /// }
136    ///
137    /// fn forbidden() -> HttpError {
138    ///     HttpError::new(
139    ///         StatusCode::UNAUTHORIZED,
140    ///         ProblemDetails::new()
141    ///             .with_type(Uri::from_static("/errors/forbidden"))
142    ///             .with_title("Forbidden"),
143    ///     )
144    /// }
145    /// ```
146    pub fn with_context(mut self, context: impl std::fmt::Display) -> Self {
147        self.context = Some(context.to_string());
148        self
149    }
150
151    /// Add a source to an [HttpError] by converting it on the fly to a [crate::DomainError]
152    /// # Example
153    /// ```rust
154    /// # use explicit_error_http::{Result, HttpError};
155    /// # use http::StatusCode;
156    /// fn check() -> Result<()> {
157    ///     Err(sqlx::Error::RowNotFound).map_err(|e|
158    ///         HttpError::new(StatusCode::NOT_FOUND, "not found")
159    ///             .with_source(e))?;      
160    /// #   Ok(())
161    /// }
162    /// ```
163    pub fn with_source<E: std::error::Error + 'static + Send + Sync>(
164        self,
165        error: E,
166    ) -> super::DomainError {
167        super::DomainError {
168            output: self,
169            source: Some(Box::new(error)),
170        }
171    }
172}
173
174impl From<HttpError> for Error {
175    fn from(value: HttpError) -> Self {
176        Error::Domain(Box::new(super::DomainError {
177            output: value,
178            source: None,
179        }))
180    }
181}
182
183impl PartialEq for HttpError {
184    fn eq(&self, other: &Self) -> bool {
185        self.context == other.context
186            && self.http_status_code == other.http_status_code
187            && serde_json::json!(self.public) == serde_json::json!(other)
188    }
189}
190
191#[derive(Serialize)]
192pub(crate) struct HttpErrorDisplay<'s> {
193    #[serde(serialize_with = "serialize_http_status_code")]
194    pub http_status_code: http::StatusCode,
195    pub public: &'s dyn DynSerialize,
196    pub context: Option<&'s str>,
197}
198
199impl<'s> From<&'s HttpError> for HttpErrorDisplay<'s> {
200    fn from(value: &'s HttpError) -> Self {
201        Self {
202            http_status_code: value.http_status_code,
203            public: value.public.as_ref(),
204            context: value.context.as_deref(),
205        }
206    }
207}
208
209impl std::fmt::Display for HttpError {
210    fn fmt<'s>(&'s self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        write!(
212            f,
213            "{}",
214            serde_json::json!(HttpErrorDisplay::<'s>::from(self))
215        )
216    }
217}
218
219impl std::fmt::Debug for HttpError {
220    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221        write!(f, "HttpError{}", self)
222    }
223}
224
225pub(crate) fn serialize_http_status_code<S>(
226    status_code: &StatusCode,
227    s: S,
228) -> Result<S::Ok, S::Error>
229where
230    S: Serializer,
231{
232    s.serialize_u16(status_code.as_u16())
233}
234
235#[cfg(test)]
236mod test;