Skip to main content

shared/
response.rs

1use crate::{ApiError, database::DatabaseError};
2use accept_header::Accept;
3use axum::response::IntoResponse;
4use std::{
5    borrow::Cow,
6    fmt::{Debug, Display},
7    str::FromStr,
8};
9
10pub type ApiResponseResult = Result<ApiResponse, ApiResponse>;
11
12tokio::task_local! {
13    pub static ACCEPT_HEADER: Option<Accept>;
14    pub static APP_DEBUG: bool;
15}
16
17pub fn accept_from_headers(headers: &axum::http::HeaderMap) -> Option<Accept> {
18    let header_value = headers.get(axum::http::header::ACCEPT)?;
19    let header_str = header_value.to_str().ok()?;
20
21    Accept::from_str(header_str).ok()
22}
23
24#[derive(Debug)]
25pub struct ApiResponse {
26    pub body: axum::body::Body,
27    pub status: axum::http::StatusCode,
28    pub headers: axum::http::HeaderMap,
29}
30
31impl ApiResponse {
32    #[inline]
33    pub fn new(body: axum::body::Body) -> Self {
34        Self {
35            body,
36            status: axum::http::StatusCode::OK,
37            headers: axum::http::HeaderMap::new(),
38        }
39    }
40
41    #[inline]
42    pub fn new_response(response: impl IntoResponse) -> Self {
43        let (parts, body) = response.into_response().into_parts();
44
45        Self {
46            body,
47            status: parts.status,
48            headers: parts.headers,
49        }
50    }
51
52    #[inline]
53    pub fn new_stream(stream: impl tokio::io::AsyncRead + Send + 'static) -> Self {
54        Self {
55            body: axum::body::Body::from_stream(tokio_util::io::ReaderStream::with_capacity(
56                stream,
57                crate::BUFFER_SIZE,
58            )),
59            status: axum::http::StatusCode::OK,
60            headers: axum::http::HeaderMap::new(),
61        }
62    }
63
64    /// Create a new API response with content negotiation based on the `Accept` header.
65    pub fn new_serialized(body: impl serde::Serialize) -> Self {
66        let accept_header = ACCEPT_HEADER.try_with(|h| h.clone()).ok().flatten();
67
68        static AVAILABLE_SERIALIZERS: &[mime::Mime] = &[
69            mime::APPLICATION_JSON,
70            mime::APPLICATION_MSGPACK,
71            mime::TEXT_XML,
72        ];
73
74        let negotiated = accept_header
75            .as_ref()
76            .and_then(|accept| accept.negotiate(AVAILABLE_SERIALIZERS).ok())
77            .unwrap_or(mime::APPLICATION_JSON);
78
79        let (content_type, body) = match negotiated {
80            m if m.essence_str() == mime::APPLICATION_MSGPACK.essence_str() => {
81                let mut bytes = Vec::new();
82                let mut se = rmp_serde::Serializer::new(&mut bytes)
83                    .with_struct_map()
84                    .with_human_readable();
85                if let Err(err) = body.serialize(&mut se) {
86                    tracing::error!(
87                        "failed to serialize response body to MessagePack: {:?}",
88                        err
89                    );
90
91                    (
92                        axum::http::HeaderValue::from_static("application/json"),
93                        axum::body::Body::from("{}"),
94                    )
95                } else {
96                    (
97                        axum::http::HeaderValue::from_static("application/msgpack"),
98                        axum::body::Body::from(bytes),
99                    )
100                }
101            }
102            m if m.essence_str() == mime::TEXT_XML.essence_str() => {
103                let string = quick_xml::se::to_string(&body).unwrap_or_else(|err| {
104                    tracing::error!("failed to serialize response body to XML: {:?}", err);
105                    "<error>serialization failed</error>".to_string()
106                });
107
108                (
109                    axum::http::HeaderValue::from_static("text/xml"),
110                    axum::body::Body::from(string),
111                )
112            }
113            _ => {
114                let bytes = serde_json::to_vec(&body).unwrap_or_else(|err| {
115                    tracing::error!("failed to serialize response body to JSON: {:?}", err);
116                    b"{}".to_vec()
117                });
118
119                (
120                    axum::http::HeaderValue::from_static("application/json"),
121                    axum::body::Body::from(bytes),
122                )
123            }
124        };
125
126        Self {
127            body,
128            status: axum::http::StatusCode::OK,
129            headers: axum::http::HeaderMap::from_iter([
130                (axum::http::header::CONTENT_TYPE, content_type),
131                (
132                    axum::http::header::VARY,
133                    axum::http::HeaderValue::from_static("Accept"),
134                ),
135            ]),
136        }
137    }
138
139    #[inline]
140    pub fn error(err: impl AsRef<str>) -> Self {
141        Self::new_serialized(ApiError::new_value(&[err.as_ref()]))
142            .with_status(axum::http::StatusCode::BAD_REQUEST)
143    }
144
145    #[inline]
146    pub fn with_status(mut self, status: axum::http::StatusCode) -> Self {
147        self.status = status;
148        self
149    }
150
151    #[inline]
152    pub fn with_header(mut self, key: &'static str, value: impl AsRef<str>) -> Self {
153        if let Ok(header_value) = axum::http::HeaderValue::from_str(value.as_ref()) {
154            self.headers.insert(key, header_value);
155        }
156
157        self
158    }
159
160    #[inline]
161    pub fn with_optional_header(
162        mut self,
163        key: &'static str,
164        value: Option<impl AsRef<str>>,
165    ) -> Self {
166        let value = match value {
167            Some(value) => value,
168            None => return self,
169        };
170
171        if let Ok(header_value) = axum::http::HeaderValue::from_str(value.as_ref()) {
172            self.headers.insert(key, header_value);
173        }
174
175        self
176    }
177
178    #[inline]
179    pub fn with_headers(mut self, headers: &axum::http::HeaderMap) -> Self {
180        for (key, value) in headers.iter() {
181            self.headers.insert(key, value.clone());
182        }
183
184        self
185    }
186
187    #[inline]
188    pub fn ok(self) -> ApiResponseResult {
189        Ok(self)
190    }
191}
192
193impl<T> From<T> for ApiResponse
194where
195    T: Into<anyhow::Error>,
196{
197    fn from(err: T) -> Self {
198        let err: anyhow::Error = err.into();
199
200        if let Some((message, status)) = extract_readable_error(&err) {
201            return ApiResponse::error(message).with_status(status);
202        }
203
204        tracing::error!("a request error occurred: {:?}", err);
205        sentry_anyhow::capture_anyhow(&err);
206
207        let debug = APP_DEBUG.try_get().unwrap_or_default();
208
209        ApiResponse::error(if debug {
210            Cow::Owned(err.to_string())
211        } else {
212            "internal server error".into()
213        })
214        .with_status(axum::http::StatusCode::INTERNAL_SERVER_ERROR)
215    }
216}
217
218impl IntoResponse for ApiResponse {
219    #[inline]
220    fn into_response(self) -> axum::response::Response {
221        let mut response = axum::response::Response::new(self.body);
222        *response.status_mut() = self.status;
223        *response.headers_mut() = self.headers;
224
225        response
226    }
227}
228
229pub fn extract_readable_error(err: &anyhow::Error) -> Option<(String, axum::http::StatusCode)> {
230    if let Some(error) = err.downcast_ref::<DisplayError>() {
231        return Some((error.message.to_string(), error.status));
232    } else if let Some(DatabaseError::Validation(error)) = err.downcast_ref::<DatabaseError>() {
233        let error_messages = crate::utils::flatten_validation_errors(error);
234
235        return Some((
236            ApiError::new_strings_value(error_messages).to_string(),
237            axum::http::StatusCode::BAD_REQUEST,
238        ));
239    } else if let Some(DatabaseError::InvalidRelation(error)) = err.downcast_ref::<DatabaseError>()
240    {
241        return Some((error.to_string(), axum::http::StatusCode::BAD_REQUEST));
242    }
243
244    None
245}
246
247#[derive(Debug)]
248pub struct DisplayError<'a> {
249    status: axum::http::StatusCode,
250    message: Cow<'a, str>,
251}
252
253impl<'a> DisplayError<'a> {
254    pub fn new(message: impl Into<Cow<'a, str>>) -> Self {
255        Self {
256            status: axum::http::StatusCode::BAD_REQUEST,
257            message: message.into(),
258        }
259    }
260
261    pub fn with_status(mut self, status: axum::http::StatusCode) -> Self {
262        self.status = status;
263
264        self
265    }
266}
267
268impl<'a> Display for DisplayError<'a> {
269    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270        f.debug_struct("DisplayError")
271            .field("status", &self.status)
272            .field("message", &self.message)
273            .finish()
274    }
275}
276
277impl<'a> std::error::Error for DisplayError<'a> {}