explicit_error_http/
error.rs

1use crate::Error;
2use erased_serde::Serialize as DynSerialize;
3use serde::Serialize;
4
5/// Self-sufficient container to both log an error and generate its HTTP response. Regarding the web framework you use, its shape can be different.
6///
7/// [Error](crate::Error) implements `From<HttpError>`, use `?` and `.into()` in functions and closures to convert to the [Error::Domain] variant.
8///
9/// Note: [HttpError] convert to [Error](crate::Error) by converting first to [DomainError](crate::DomainError).
10/// # Examples
11/// Domain errors that derive [HttpError](crate::derive::HttpError) must implement `From<&MyDomainError> for HttpError`.
12/// ```rust
13/// # use actix_web::http::StatusCode;
14/// # use problem_details::ProblemDetails;
15/// # use http::Uri;
16/// use explicit_error_http::{derive::HttpError, prelude::*, HttpError};
17///
18/// #[derive(HttpError, Debug)]
19/// enum MyDomainError {
20///     Foo,
21/// }
22///
23/// impl From<&MyDomainError> for HttpError {
24///     fn from(value: &MyDomainError) -> Self {
25///         match value {
26///             MyDomainError::Foo => HttpError::new(
27///                     StatusCode::BAD_REQUEST,
28///                     ProblemDetails::new()
29///                         .with_type(Uri::from_static("/errors/my-domain/foo"))
30///                         .with_title("Foo format incorrect.")
31///                 ),
32///         }
33///     }
34/// }
35/// ```
36///
37/// Domain errors cannot require to be extracted in either a struct or enum variant (eg: middleware errors).
38/// You can generate [Error::Domain] variant with an [HttpError]
39/// ```rust
40/// # use actix_web::http::StatusCode;
41/// # use problem_details::ProblemDetails;
42/// # use http::Uri;
43/// use explicit_error_http::{Error, prelude::*, HttpError};
44///
45/// fn business_logic() -> Result<(), Error> {
46///     Err(HttpError::new(
47///         StatusCode::FORBIDDEN,
48///         ProblemDetails::new()
49///             .with_type(Uri::from_static("/errors/generic#forbidden"))
50///             .with_title("Forbidden."),
51///     ))?;
52///     # Ok(())
53/// }
54/// ```
55///
56/// Usually to avoid boilerplate and having consistency in error responses web applications
57/// implement helpers for frequent http error codes.
58/// ```rust
59/// # use actix_web::http::StatusCode;
60/// # use problem_details::ProblemDetails;
61/// # use http::Uri;
62/// use explicit_error_http::{prelude::*, HttpError, Error};
63///
64/// fn forbidden() -> HttpError {
65///     HttpError::new(
66///         StatusCode::FORBIDDEN,
67///         ProblemDetails::new()
68///             .with_type(Uri::from_static("/errors/generic#forbidden"))
69///             .with_title("Forbidden."),
70///     )
71/// }
72///
73/// // context can be added by the caller to add information in log
74/// fn business_logic() -> Result<(), Error> {
75///     Err(42).map_err(|e|
76///         forbidden().with_context(
77///             format!("Return a forbidden instead of 500 to avoid leaking implementation details: {e}")
78///     ))?;
79///     # Ok(())
80/// }
81/// ```
82#[derive(Serialize)]
83pub struct HttpError {
84    #[cfg(feature = "actix-web")]
85    #[serde(skip)]
86    pub http_status_code: actix_web::http::StatusCode,
87    #[serde(flatten)]
88    pub public: Box<dyn DynSerialize>,
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 actix_web::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    #[cfg(feature = "actix-web")]
112    pub fn new<S: Serialize + 'static>(
113        http_status_code: actix_web::http::StatusCode,
114        public: S,
115    ) -> Self {
116        Self {
117            http_status_code,
118            public: Box::new(public),
119            context: None,
120        }
121    }
122
123    /// Add a context to an [HttpError], override if one was set. The context appears in display
124    /// but not in the http response.
125    /// # Examples
126    /// ```rust
127    /// # use explicit_error_http::{Result, HttpError};
128    /// # use actix_web::http::StatusCode;
129    /// # use problem_details::ProblemDetails;
130    /// # use http::Uri;
131    /// fn check_authz() -> Result<()> {
132    ///     if !false {
133    ///         Err(forbidden().with_context("Some info to help debug"))?;
134    ///     }
135    ///     Ok(())
136    /// }
137    ///
138    /// fn forbidden() -> HttpError {
139    ///     HttpError::new(
140    ///         StatusCode::UNAUTHORIZED,
141    ///         ProblemDetails::new()
142    ///             .with_type(Uri::from_static("/errors/forbidden"))
143    ///             .with_title("Forbidden"),
144    ///     )
145    /// }
146    /// ```
147    pub fn with_context(mut self, context: impl std::fmt::Display) -> Self {
148        self.context = Some(context.to_string());
149        self
150    }
151}
152
153impl From<HttpError> for Error {
154    fn from(value: HttpError) -> Self {
155        Error::Domain(Box::new(super::DomainError {
156            output: value,
157            source: None,
158        }))
159    }
160}
161
162#[derive(Serialize)]
163pub(crate) struct HttpErrorDisplay<'s> {
164    #[cfg(feature = "actix-web")]
165    #[serde(serialize_with = "crate::actix::serialize_http_status_code")]
166    pub http_status_code: actix_web::http::StatusCode,
167    pub public: &'s dyn DynSerialize,
168    pub context: Option<&'s str>,
169}
170
171impl<'s> From<&'s HttpError> for HttpErrorDisplay<'s> {
172    fn from(value: &'s HttpError) -> Self {
173        Self {
174            #[cfg(feature = "actix-web")]
175            http_status_code: value.http_status_code,
176            public: value.public.as_ref(),
177            context: value.context.as_deref(),
178        }
179    }
180}
181
182impl std::fmt::Display for HttpError {
183    fn fmt<'s>(&'s self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        write!(
185            f,
186            "{}",
187            serde_json::json!(HttpErrorDisplay::<'s>::from(self))
188        )
189    }
190}
191
192impl std::fmt::Debug for HttpError {
193    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194        write!(f, "HttpError{}", self)
195    }
196}
197
198#[cfg(test)]
199mod test;