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 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> {}