anyhow_http/
response.rs

1//! Creating responses from [`HttpError`].
2use bytes::Bytes;
3use std::marker::PhantomData;
4
5use crate::http_error::HttpError;
6
7/// A result that wraps [`HttpError`] with response formatter [`FormatResponse`].
8pub type HttpResult<T, F> = core::result::Result<T, HttpErrorResponse<F>>;
9
10/// Type representing an error response.
11#[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
51/// Trait for formatting error responses.
52pub trait FormatResponse {
53    fn format_response(http_error: &HttpError) -> Bytes;
54    fn content_type() -> mime::Mime;
55}
56
57/// A [`HttpErrorResponse`] with configured [`Json`] formatter.
58#[cfg(feature = "json")]
59#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
60pub type HttpJsonErrorResponse = HttpErrorResponse<Json>;
61
62/// A [`HttpResult`] with configured [`Json`] formatter.
63#[cfg(feature = "json")]
64#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
65pub type HttpJsonResult<T> = core::result::Result<T, HttpJsonErrorResponse>;
66
67/// A general purpose error response that formats a [`HttpError`] as Json.
68#[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}