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;