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
152impl From<HttpError> for Error {
153 fn from(value: HttpError) -> Self {
154 Error::Domain(Box::new(super::DomainError {
155 output: value,
156 source: None,
157 }))
158 }
159}
160
161impl PartialEq for HttpError {
162 fn eq(&self, other: &Self) -> bool {
163 self.context == other.context
164 && self.http_status_code == other.http_status_code
165 && serde_json::json!(self.public) == serde_json::json!(other)
166 }
167}
168
169#[derive(Serialize)]
170pub(crate) struct HttpErrorDisplay<'s> {
171 #[serde(serialize_with = "serialize_http_status_code")]
172 pub http_status_code: http::StatusCode,
173 pub public: &'s dyn DynSerialize,
174 pub context: Option<&'s str>,
175}
176
177impl<'s> From<&'s HttpError> for HttpErrorDisplay<'s> {
178 fn from(value: &'s HttpError) -> Self {
179 Self {
180 http_status_code: value.http_status_code,
181 public: value.public.as_ref(),
182 context: value.context.as_deref(),
183 }
184 }
185}
186
187impl std::fmt::Display for HttpError {
188 fn fmt<'s>(&'s self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 write!(
190 f,
191 "{}",
192 serde_json::json!(HttpErrorDisplay::<'s>::from(self))
193 )
194 }
195}
196
197impl std::fmt::Debug for HttpError {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 write!(f, "HttpError{}", self)
200 }
201}
202
203pub(crate) fn serialize_http_status_code<S>(
204 status_code: &StatusCode,
205 s: S,
206) -> Result<S::Ok, S::Error>
207where
208 S: Serializer,
209{
210 s.serialize_u16(status_code.as_u16())
211}
212
213#[cfg(test)]
214mod test;