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#[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 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 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 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 pub const fn with_status_code(mut self, status_code: StatusCode) -> Self {
103 self.status_code = status_code;
104 self
105 }
106
107 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 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 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 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 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 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 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 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 pub fn status_code(&self) -> StatusCode {
224 self.status_code
225 }
226
227 pub fn reason(&self) -> Option<Cow<'static, str>> {
229 self.reason.clone()
230 }
231
232 pub fn source(&self) -> Option<&anyhow::Error> {
234 self.source.as_ref()
235 }
236
237 pub fn headers(&self) -> Option<&HeaderMap> {
239 self.headers.as_ref()
240 }
241
242 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}