1use bytes::Bytes;
3use std::marker::PhantomData;
4
5use crate::http_error::HttpError;
6
7pub type HttpResult<T, F> = core::result::Result<T, HttpErrorResponse<F>>;
9
10#[derive(Debug)]
12pub struct HttpErrorResponse<F: FormatResponse> {
13 pub http_error: Box<HttpError>,
14 _formatter: PhantomData<F>,
15}
16
17impl<E, F> From<E> for HttpErrorResponse<F>
18where
19 F: FormatResponse,
20 E: Into<anyhow::Error>,
21{
22 fn from(e: E) -> Self {
23 Self {
24 http_error: Box::new(HttpError::from_err(e)),
25 _formatter: PhantomData,
26 }
27 }
28}
29
30#[cfg(feature = "axum")]
31#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
32impl<F: FormatResponse> axum::response::IntoResponse for HttpErrorResponse<F> {
33 fn into_response(self) -> axum::response::Response {
34 let mut headers = self.http_error.headers.clone().unwrap_or_default();
35 headers.insert(
36 http::header::CONTENT_TYPE,
37 http::HeaderValue::from_str(F::content_type().as_ref()).unwrap(),
38 );
39 let mut resp = (
40 self.http_error.status_code,
41 headers,
42 F::format_response(&self.http_error),
43 )
44 .into_response();
45 resp.extensions_mut()
46 .insert(std::sync::Arc::new(self.http_error));
47 resp
48 }
49}
50
51pub trait FormatResponse {
53 fn format_response(http_error: &HttpError) -> Bytes;
54 fn content_type() -> mime::Mime;
55}
56
57#[cfg(feature = "json")]
59#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
60pub type HttpJsonErrorResponse = HttpErrorResponse<Json>;
61
62#[cfg(feature = "json")]
64#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
65pub type HttpJsonResult<T> = core::result::Result<T, HttpJsonErrorResponse>;
66
67#[cfg(feature = "json")]
69#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
70#[derive(Debug)]
71pub struct Json;
72
73#[cfg(feature = "json")]
74#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
75impl FormatResponse for Json {
76 fn format_response(http_error: &HttpError) -> Bytes {
77 use bytes::BufMut;
78 let error_reason = http_error
79 .reason()
80 .as_deref()
81 .or_else(|| http_error.status_code().canonical_reason())
82 .map(String::from);
83
84 let mut resp = serde_json::json!({
85 "error": {
86 "message": error_reason,
87 },
88 });
89 if let Some(data) = &http_error.data {
90 for (k, v) in data {
91 resp["error"][k] = v.clone();
92 }
93 }
94
95 let mut buf = bytes::BytesMut::with_capacity(128).writer();
96 if let Err(err) = serde_json::to_writer(&mut buf, &resp) {
97 return err.to_string().into();
98 }
99
100 buf.into_inner().freeze()
101 }
102
103 fn content_type() -> mime::Mime {
104 mime::APPLICATION_JSON
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::http_error;
112 use http::StatusCode;
113
114 #[test]
115 #[cfg(feature = "json")]
116 fn http_error_response_json() {
117 let resp: HttpErrorResponse<Json> = http_error!(BAD_REQUEST).into();
118 assert_eq!(resp.http_error.status_code, StatusCode::BAD_REQUEST);
119 }
120
121 #[test]
122 #[cfg(all(feature = "axum", feature = "json"))]
123 fn http_error_resonse_axum_into_response() {
124 use axum::response::IntoResponse;
125 let resp: HttpErrorResponse<Json> = HttpError::from_status_code(StatusCode::BAD_REQUEST)
126 .with_header("x-custom-header", 42.into())
127 .into();
128 let resp = resp.into_response();
129 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
130 assert_eq!(
131 resp.headers().get("content-type"),
132 Some(&"application/json".parse().unwrap())
133 );
134 assert_eq!(resp.headers().get("x-custom-header"), Some(&42.into()));
135 }
136
137 #[test]
138 #[cfg(feature = "json")]
139 fn http_error_json_response() {
140 let e: HttpError = http_error!(BAD_REQUEST, "invalid param",).into();
141 let e = e.with_key_value("ctx", "some context");
142 let e = e.with_key_value("code", 1234);
143 let body = Json::format_response(&e);
144 let content_type = Json::content_type();
145 assert_eq!(
146 body,
147 Bytes::from_static(
148 b"{\"error\":{\"code\":1234,\"ctx\":\"some context\",\"message\":\"invalid param\"}}"
149 )
150 );
151 assert_eq!(content_type, mime::APPLICATION_JSON);
152 }
153
154 #[test]
155 #[cfg(feature = "json")]
156 fn http_error_response_from_anyhow_downcast() {
157 let res: HttpResult<(), Json> = (|| {
158 let e = http_error!(BAD_REQUEST);
159 Err(e)?;
160 unreachable!()
161 })();
162 let e = res.unwrap_err().http_error;
163 assert_eq!(e.status_code(), StatusCode::BAD_REQUEST)
164 }
165}