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    #[inline]
66    pub fn new_serialized(body: impl serde::Serialize) -> Self {
67        Self::new_serialized_with_capacity(body, 128)
68    }
69
70    /// The same as [`ApiResponse::new_serialized`], preallocating `capacity` bytes for the
71    /// serialized JSON body.
72    pub fn new_serialized_with_capacity(body: impl serde::Serialize, capacity: usize) -> Self {
73        let accept_header = ACCEPT_HEADER.try_with(|h| h.clone()).ok().flatten();
74
75        static AVAILABLE_SERIALIZERS: &[mime::Mime] = &[
76            mime::APPLICATION_JSON,
77            mime::APPLICATION_MSGPACK,
78            mime::TEXT_XML,
79        ];
80
81        let negotiated = accept_header
82            .as_ref()
83            .and_then(|accept| accept.negotiate(AVAILABLE_SERIALIZERS).ok())
84            .unwrap_or(mime::APPLICATION_JSON);
85
86        let (content_type, body) = match negotiated {
87            m if m.essence_str() == mime::APPLICATION_MSGPACK.essence_str() => {
88                let mut bytes = Vec::new();
89                let mut se = rmp_serde::Serializer::new(&mut bytes)
90                    .with_struct_map()
91                    .with_human_readable();
92                if let Err(err) = body.serialize(&mut se) {
93                    tracing::error!(
94                        "failed to serialize response body to MessagePack: {:?}",
95                        err
96                    );
97
98                    (
99                        axum::http::HeaderValue::from_static("application/json"),
100                        axum::body::Body::from("{}"),
101                    )
102                } else {
103                    (
104                        axum::http::HeaderValue::from_static("application/msgpack"),
105                        axum::body::Body::from(bytes),
106                    )
107                }
108            }
109            m if m.essence_str() == mime::TEXT_XML.essence_str() => {
110                let string = quick_xml::se::to_string(&body).unwrap_or_else(|err| {
111                    tracing::error!("failed to serialize response body to XML: {:?}", err);
112                    "<error>serialization failed</error>".to_string()
113                });
114
115                (
116                    axum::http::HeaderValue::from_static("text/xml"),
117                    axum::body::Body::from(string),
118                )
119            }
120            _ => {
121                let mut bytes = Vec::with_capacity(capacity);
122                if let Err(err) = serde_json::to_writer(&mut bytes, &body) {
123                    tracing::error!("failed to serialize response body to JSON: {:?}", err);
124                    bytes = b"{}".to_vec();
125                }
126
127                (
128                    axum::http::HeaderValue::from_static("application/json"),
129                    axum::body::Body::from(bytes),
130                )
131            }
132        };
133
134        Self {
135            body,
136            status: axum::http::StatusCode::OK,
137            headers: axum::http::HeaderMap::from_iter([
138                (axum::http::header::CONTENT_TYPE, content_type),
139                (
140                    axum::http::header::VARY,
141                    axum::http::HeaderValue::from_static("Accept"),
142                ),
143            ]),
144        }
145    }
146
147    #[inline]
148    pub fn error(err: impl AsRef<str>) -> Self {
149        Self::new_serialized(ApiError::new_value(&[err.as_ref()]))
150            .with_status(axum::http::StatusCode::BAD_REQUEST)
151    }
152
153    #[inline]
154    pub fn errors(errors: Vec<String>) -> Self {
155        Self::new_serialized(ApiError::new_strings_value(errors))
156            .with_status(axum::http::StatusCode::BAD_REQUEST)
157    }
158
159    #[inline]
160    pub fn with_status(mut self, status: axum::http::StatusCode) -> Self {
161        self.status = status;
162        self
163    }
164
165    #[inline]
166    pub fn with_header(mut self, key: &'static str, value: impl AsRef<str>) -> Self {
167        if let Ok(header_value) = axum::http::HeaderValue::from_str(value.as_ref()) {
168            self.headers.insert(key, header_value);
169        }
170
171        self
172    }
173
174    #[inline]
175    pub fn with_optional_header(
176        mut self,
177        key: &'static str,
178        value: Option<impl AsRef<str>>,
179    ) -> Self {
180        let value = match value {
181            Some(value) => value,
182            None => return self,
183        };
184
185        if let Ok(header_value) = axum::http::HeaderValue::from_str(value.as_ref()) {
186            self.headers.insert(key, header_value);
187        }
188
189        self
190    }
191
192    #[inline]
193    pub fn with_headers(mut self, headers: &axum::http::HeaderMap) -> Self {
194        for (key, value) in headers.iter() {
195            self.headers.insert(key, value.clone());
196        }
197
198        self
199    }
200
201    #[inline]
202    pub fn ok(self) -> ApiResponseResult {
203        Ok(self)
204    }
205}
206
207impl<T> From<T> for ApiResponse
208where
209    T: Into<anyhow::Error>,
210{
211    fn from(err: T) -> Self {
212        let err: anyhow::Error = err.into();
213
214        if let Some((errors, status)) = extract_readable_error(&err) {
215            return ApiResponse::errors(errors).with_status(status);
216        }
217
218        tracing::error!("a request error occurred: {:?}", err);
219        sentry_anyhow::capture_anyhow(&err);
220
221        let debug = APP_DEBUG.try_get().unwrap_or_default();
222
223        ApiResponse::error(if debug {
224            Cow::Owned(err.to_string())
225        } else {
226            "internal server error".into()
227        })
228        .with_status(axum::http::StatusCode::INTERNAL_SERVER_ERROR)
229    }
230}
231
232impl IntoResponse for ApiResponse {
233    #[inline]
234    fn into_response(self) -> axum::response::Response {
235        let mut response = axum::response::Response::new(self.body);
236        *response.status_mut() = self.status;
237        *response.headers_mut() = self.headers;
238
239        response
240    }
241}
242
243pub fn extract_readable_error(
244    err: &anyhow::Error,
245) -> Option<(Vec<String>, axum::http::StatusCode)> {
246    if let Some(error) = err.downcast_ref::<DisplayError>() {
247        return Some((vec![error.message.to_string()], error.status));
248    } else if let Some(DatabaseError::Validation(error)) = err.downcast_ref::<DatabaseError>() {
249        return Some((
250            crate::utils::flatten_validation_errors(error),
251            axum::http::StatusCode::BAD_REQUEST,
252        ));
253    } else if let Some(DatabaseError::InvalidRelation(error)) = err.downcast_ref::<DatabaseError>()
254    {
255        return Some((vec![error.to_string()], axum::http::StatusCode::BAD_REQUEST));
256    } else if let Some(DatabaseError::Any(error)) = err.downcast_ref::<DatabaseError>() {
257        return extract_readable_error(error);
258    } else if let Some(error) = err.downcast_ref::<crate::cache::SharedComputeError>() {
259        return extract_readable_error(&error.0);
260    }
261
262    None
263}
264
265pub fn extract_readable_message(err: &anyhow::Error) -> Option<(String, axum::http::StatusCode)> {
266    extract_readable_error(err).map(|(errors, status)| (errors.join(", "), status))
267}
268
269#[derive(Debug)]
270pub struct DisplayError<'a> {
271    status: axum::http::StatusCode,
272    message: Cow<'a, str>,
273}
274
275impl<'a> DisplayError<'a> {
276    pub fn new(message: impl Into<Cow<'a, str>>) -> Self {
277        Self {
278            status: axum::http::StatusCode::BAD_REQUEST,
279            message: message.into(),
280        }
281    }
282
283    pub fn with_status(mut self, status: axum::http::StatusCode) -> Self {
284        self.status = status;
285
286        self
287    }
288}
289
290impl<'a> Display for DisplayError<'a> {
291    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292        f.debug_struct("DisplayError")
293            .field("status", &self.status)
294            .field("message", &self.message)
295            .finish()
296    }
297}
298
299impl<'a> std::error::Error for DisplayError<'a> {}