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
11pub struct HttpError {
13 pub(crate) status_code: StatusCode,
14 pub(crate) reason: Option<Cow<'static, str>>,
15 pub(crate) source: Option<anyhow::Error>,
16 pub(crate) data: Option<HashMap<String, serde_json::Value>>,
17 pub(crate) headers: Option<HeaderMap>,
18}
19
20impl fmt::Debug for HttpError {
21 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22 write!(
23 f,
24 "HttpError\nStatus: {status_code}\nReason: {reason:?}\nData: {data:?}\nHeaders: {headers:?}\n\nSource: {source:?}",
25 status_code = self.status_code,
26 reason = self.reason,
27 data = self.data,
28 headers = self.headers,
29 source = self.source
30 )
31 }
32}
33
34impl fmt::Display for HttpError {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match (&self.reason, &self.source) {
37 (None, None) => write!(f, "HttpError({})", self.status_code),
38 (Some(r), None) => write!(f, "HttpError({}): {r}", self.status_code),
39 (None, Some(s)) if f.alternate() => {
40 write!(f, "HttpError({}): source: {s:#}", self.status_code)
41 }
42 (None, Some(s)) => write!(f, "HttpError({}): source: {s}", self.status_code),
43 (Some(r), Some(s)) if f.alternate() => {
44 write!(f, "HttpError({}): {r}, source: {s:#}", self.status_code)
45 }
46 (Some(r), Some(s)) => write!(f, "HttpError({}): {r}, source: {s}", self.status_code),
47 }
48 }
49}
50
51impl StdError for HttpError {
52 fn source(&self) -> Option<&(dyn StdError + 'static)> {
53 self.source
54 .as_deref()
55 .map(|e| e as &(dyn StdError + 'static))
56 }
57}
58
59#[allow(clippy::derivable_impls)]
60impl Default for HttpError {
61 fn default() -> Self {
62 Self { ..Self::new() }
63 }
64}
65
66impl PartialEq for HttpError {
67 fn eq(&self, other: &Self) -> bool {
68 self.status_code == other.status_code
69 && self.reason == other.reason
70 && self.data == other.data
71 }
72}
73
74impl HttpError {
75 pub const fn new() -> Self {
77 Self {
78 status_code: StatusCode::INTERNAL_SERVER_ERROR,
79 reason: None,
80 source: None,
81 data: None,
82 headers: None,
83 }
84 }
85
86 pub const fn from_static(status_code: StatusCode, reason: &'static str) -> Self {
96 Self {
97 status_code,
98 reason: Some(Cow::Borrowed(reason)),
99 source: None,
100 data: None,
101 headers: None,
102 }
103 }
104
105 pub const fn from_status_code(status_code: StatusCode) -> Self {
107 let mut http_err = Self::new();
108 http_err.status_code = status_code;
109 http_err
110 }
111
112 pub const fn with_status_code(mut self, status_code: StatusCode) -> Self {
114 self.status_code = status_code;
115 self
116 }
117
118 pub fn with_reason<S: Into<Cow<'static, str>>>(mut self, reason: S) -> Self {
120 self.reason = Some(reason.into());
121 self
122 }
123
124 pub fn with_source_context<C>(mut self, context: C) -> Self
127 where
128 C: fmt::Display + Send + Sync + 'static,
129 {
130 let source = match self.source {
131 Some(s) => s.context(context),
132 None => anyhow!("{context}"),
133 };
134 self.source = Some(source);
135 self
136 }
137
138 pub fn with_boxed_source_err(mut self, err: Box<dyn StdError + Send + Sync + 'static>) -> Self {
140 self.source = Some(anyhow!("{err}"));
141 self
142 }
143
144 pub fn with_source_err<E>(mut self, err: E) -> Self
146 where
147 E: Into<anyhow::Error>,
148 {
149 self.source = Some(err.into());
150 self
151 }
152
153 pub fn with_data<I, K, V>(mut self, values: I) -> Option<Self>
163 where
164 I: IntoIterator<Item = (K, V)>,
165 K: Into<String>,
166 V: Serialize + Sync + Send + 'static,
167 {
168 let iter = values
169 .into_iter()
170 .map(|(k, v)| Some((k.into(), serde_json::to_value(v).ok()?)));
171
172 self.data = self
173 .data
174 .get_or_insert_with(HashMap::new)
175 .clone()
176 .into_iter()
177 .map(Option::Some)
178 .chain(iter)
179 .collect();
180
181 Some(self)
182 }
183
184 pub fn with_key_value<K, V>(mut self, key: K, value: V) -> Self
186 where
187 K: Into<String>,
188 V: Serialize + Sync + Send + 'static,
189 {
190 let Ok(value) = serde_json::to_value(value) else {
191 return self;
192 };
193 self.data
194 .get_or_insert_with(HashMap::new)
195 .insert(key.into(), value);
196 self
197 }
198
199 pub fn with_header<K>(mut self, header_key: K, header_value: HeaderValue) -> Self
200 where
201 K: IntoHeaderName,
202 {
203 self.headers
204 .get_or_insert_with(HeaderMap::new)
205 .insert(header_key, header_value);
206 self
207 }
208
209 pub fn get<V>(&self, key: impl AsRef<str>) -> Option<V>
211 where
212 V: DeserializeOwned + Send + Sync,
213 {
214 self.data
215 .as_ref()
216 .and_then(|d| d.get(key.as_ref()))
217 .and_then(|v| serde_json::from_value(v.clone()).ok())
218 }
219
220 pub fn set<K, V>(&mut self, key: K, value: V) -> serde_json::Result<()>
222 where
223 K: Into<String>,
224 V: Serialize + Sync + Send + 'static,
225 {
226 let value = serde_json::to_value(value)?;
227 self.data
228 .get_or_insert_with(HashMap::new)
229 .insert(key.into(), value);
230 Ok(())
231 }
232
233 pub fn status_code(&self) -> StatusCode {
235 self.status_code
236 }
237
238 pub fn reason(&self) -> Option<Cow<'static, str>> {
240 self.reason.clone()
241 }
242
243 pub fn source(&self) -> Option<&anyhow::Error> {
245 self.source.as_ref()
246 }
247
248 pub fn headers(&self) -> Option<&HeaderMap> {
250 self.headers.as_ref()
251 }
252
253 pub fn from_err<E>(err: E) -> Self
256 where
257 E: Into<anyhow::Error>,
258 {
259 let err = err.into();
260 match err.downcast::<HttpError>() {
261 Ok(http_error) => http_error,
262 Err(err) => Self {
263 source: Some(err),
264 ..Self::default()
265 },
266 }
267 }
268
269 pub fn into_boxed(self) -> Box<dyn StdError + Send + Sync + 'static> {
270 self.into()
271 }
272}
273
274impl From<anyhow::Error> for HttpError {
275 fn from(err: anyhow::Error) -> Self {
276 HttpError::from_err(err)
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use anyhow::anyhow;
283
284 use super::*;
285
286 #[test]
287 fn http_error_display() {
288 let e: HttpError = HttpError::default();
289 assert_eq!(e.to_string(), "HttpError(500 Internal Server Error)");
290
291 let e: HttpError = HttpError::default().with_reason("reason");
292 assert_eq!(
293 e.to_string(),
294 "HttpError(500 Internal Server Error): reason"
295 );
296
297 let e: HttpError = HttpError::default().with_source_err(anyhow!("error"));
298 assert_eq!(
299 e.to_string(),
300 "HttpError(500 Internal Server Error): source: error"
301 );
302
303 let e: HttpError = HttpError::default()
304 .with_reason("reason")
305 .with_source_err(anyhow!("error"));
306 assert_eq!(
307 e.to_string(),
308 "HttpError(500 Internal Server Error): reason, source: error"
309 );
310 }
311
312 #[test]
313 fn http_error_new_const() {
314 const ERR: HttpError = HttpError::new();
315 assert_eq!(ERR.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
316 }
317
318 #[test]
319 fn http_error_from_static() {
320 const ERR: HttpError = HttpError::from_static(StatusCode::BAD_REQUEST, "invalid request");
321 assert_eq!(ERR.status_code(), StatusCode::BAD_REQUEST);
322 assert_eq!(ERR.reason(), Some("invalid request".into()));
323 }
324
325 #[test]
326 fn http_error_default() {
327 let e: HttpError = HttpError::default();
328 assert_eq!(e.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
329 }
330
331 #[test]
332 fn http_error_from_status_code() {
333 let e: HttpError = HttpError::from_status_code(StatusCode::BAD_REQUEST);
334 assert_eq!(e.status_code(), StatusCode::BAD_REQUEST);
335 }
336
337 #[test]
338 fn http_error_from_err() {
339 let e: HttpError = HttpError::from_err(anyhow!("error"));
340 assert_eq!(e.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
341 assert_eq!(e.source().unwrap().to_string(), "error");
342
343 let e: HttpError = HttpError::from_err(fmt::Error);
344 assert_eq!(e.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
345 assert_eq!(e.source().unwrap().to_string(), fmt::Error.to_string());
346 }
347
348 #[derive(Debug)]
349 struct GenericError;
350 impl std::fmt::Display for GenericError {
351 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
352 write!(f, "CustomError")
353 }
354 }
355
356 impl From<GenericError> for HttpError {
357 fn from(_: GenericError) -> Self {
358 Self::default().with_status_code(StatusCode::BAD_REQUEST)
359 }
360 }
361
362 impl From<GenericError> for anyhow::Error {
363 fn from(_: GenericError) -> Self {
364 HttpError::default()
365 .with_status_code(StatusCode::BAD_REQUEST)
366 .into()
367 }
368 }
369
370 #[test]
371 fn http_error_from_custom_impl_try() {
372 let res: std::result::Result<(), HttpError> = (|| {
373 Err(GenericError)?;
374 unreachable!()
375 })();
376 let e = res.unwrap_err();
377 assert_eq!(e.status_code(), StatusCode::BAD_REQUEST);
378 }
379
380 #[test]
381 fn http_error_into_anyhow() {
382 let res: anyhow::Result<()> = (|| {
383 Err(GenericError)?;
384 unreachable!()
385 })();
386 let e = res.unwrap_err();
387 assert_eq!(
388 HttpError::from_err(e).status_code(),
389 StatusCode::BAD_REQUEST
390 );
391 }
392
393 #[test]
394 fn http_error_with_status_code() {
395 let e: HttpError = HttpError::default().with_status_code(StatusCode::BAD_REQUEST);
396 assert_eq!(e.status_code(), StatusCode::BAD_REQUEST);
397 }
398
399 #[test]
400 fn http_error_with_reason() {
401 let e: HttpError = HttpError::default().with_reason("reason");
402 assert_eq!(e.reason(), Some("reason".into()));
403 }
404
405 #[test]
406 fn http_error_with_source_context() {
407 let e: HttpError = HttpError::default().with_source_context("context");
408 assert_eq!(e.source().map(ToString::to_string), Some("context".into()));
409
410 let e: HttpError = HttpError::default()
411 .with_source_err(anyhow!("source"))
412 .with_source_context("context");
413 assert_eq!(format!("{:#}", e.source().unwrap()), "context: source");
414 }
415
416 #[test]
417 fn http_error_with_dyn_source_error() {
418 let dyn_err = Box::new(fmt::Error) as Box<dyn StdError + Send + Sync + 'static>;
419 let e: HttpError = HttpError::default().with_boxed_source_err(dyn_err);
420 assert_eq!(e.source().unwrap().to_string(), fmt::Error.to_string());
421 }
422
423 #[test]
424 fn http_error_with_source_error() {
425 let e: HttpError = HttpError::default().with_source_err(fmt::Error);
426 assert_eq!(e.source().unwrap().to_string(), fmt::Error.to_string());
427 }
428
429 #[test]
430 fn http_error_data() {
431 let e: HttpError = HttpError::default().with_key_value("key", 1234);
432 assert_eq!(e.get::<i32>("key"), Some(1234));
433 assert_eq!(e.get::<String>("key"), None);
434 }
435
436 #[test]
437 fn http_error_with_data() {
438 let e: HttpError = HttpError::default()
439 .with_data([("key1", 1234), ("key2", 5678)])
440 .unwrap();
441 assert_eq!(e.get::<i32>("key1"), Some(1234));
442 assert_eq!(e.get::<i32>("key2"), Some(5678));
443 }
444
445 #[test]
446 fn http_error_set() {
447 let mut e: HttpError = HttpError::default();
448 e.set("key1", 1234).unwrap();
449 assert_eq!(e.get::<i32>("key1"), Some(1234));
450 }
451
452 #[test]
453 fn http_error_with_headers() {
454 let e: HttpError = HttpError::default()
455 .with_header(
456 http::header::CONTENT_TYPE,
457 "application/json".parse().unwrap(),
458 )
459 .with_header("x-custom-header", "42".parse().unwrap());
460 let hdrs = e.headers().unwrap();
461 assert_eq!(
462 hdrs.get(http::header::CONTENT_TYPE).unwrap(),
463 "application/json"
464 );
465 assert_eq!(hdrs.get("x-custom-header").unwrap(), "42");
466 }
467
468 #[test]
469 fn http_error_anyhow_downcast() {
470 let outer: anyhow::Error = HttpError::from_status_code(StatusCode::BAD_REQUEST).into();
471 let e = HttpError::from(outer);
472 assert_eq!(e.status_code(), StatusCode::BAD_REQUEST);
473 }
474}