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 #[inline]
66 pub fn new_serialized(body: impl serde::Serialize) -> Self {
67 Self::new_serialized_with_capacity(body, 128)
68 }
69
70 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> {}