1use std::borrow::Cow;
68use std::fmt;
69
70use axum_core::response::{IntoResponse, Response};
71use http::StatusCode;
72use http::header::{self, HeaderName, HeaderValue};
73use serde::Serialize;
74
75pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
77
78pub type Result<T, E = Error> = std::result::Result<T, E>;
80
81pub const INTERNAL_ERROR_MESSAGE: &str = "An internal server error occurred";
83
84pub struct Error {
93 inner: Box<Inner>,
94}
95
96struct Inner {
97 status: StatusCode,
98 code: Cow<'static, str>,
99 message: Cow<'static, str>,
100 details: Option<serde_json::Value>,
101 headers: Vec<(HeaderName, HeaderValue)>,
102 source: Option<BoxError>,
103}
104
105macro_rules! constructors {
106 ($( $(#[$doc:meta])* $name:ident => $status:ident, $code:literal; )*) => {
107 $(
108 $(#[$doc])*
109 pub fn $name(message: impl Into<Cow<'static, str>>) -> Self {
110 Self::new(StatusCode::$status, $code, message)
111 }
112 )*
113 };
114}
115
116impl Error {
117 pub fn new(
120 status: StatusCode,
121 code: impl Into<Cow<'static, str>>,
122 message: impl Into<Cow<'static, str>>,
123 ) -> Self {
124 Self {
125 inner: Box::new(Inner {
126 status,
127 code: code.into(),
128 message: message.into(),
129 details: None,
130 headers: Vec::new(),
131 source: None,
132 }),
133 }
134 }
135
136 constructors! {
137 bad_request => BAD_REQUEST, "BAD_REQUEST";
139 unauthorized => UNAUTHORIZED, "UNAUTHORIZED";
141 forbidden => FORBIDDEN, "FORBIDDEN";
143 not_found => NOT_FOUND, "NOT_FOUND";
145 method_not_allowed => METHOD_NOT_ALLOWED, "METHOD_NOT_ALLOWED";
147 timeout => REQUEST_TIMEOUT, "REQUEST_TIMEOUT";
149 conflict => CONFLICT, "CONFLICT";
151 payload_too_large => PAYLOAD_TOO_LARGE, "PAYLOAD_TOO_LARGE";
153 unsupported_media_type => UNSUPPORTED_MEDIA_TYPE, "UNSUPPORTED_MEDIA_TYPE";
155 unprocessable => UNPROCESSABLE_ENTITY, "UNPROCESSABLE_ENTITY";
157 too_many_requests => TOO_MANY_REQUESTS, "TOO_MANY_REQUESTS";
159 service_unavailable => SERVICE_UNAVAILABLE, "SERVICE_UNAVAILABLE";
161 }
162
163 pub fn validation(details: impl Serialize) -> Self {
166 Self::new(
167 StatusCode::UNPROCESSABLE_ENTITY,
168 "VALIDATION_FAILED",
169 "The request failed validation",
170 )
171 .with_details(details)
172 }
173
174 pub fn internal(source: impl Into<BoxError>) -> Self {
179 Self::new(
180 StatusCode::INTERNAL_SERVER_ERROR,
181 "INTERNAL_ERROR",
182 INTERNAL_ERROR_MESSAGE,
183 )
184 .with_source(source)
185 }
186
187 #[must_use]
189 pub fn with_code(mut self, code: impl Into<Cow<'static, str>>) -> Self {
190 self.inner.code = code.into();
191 self
192 }
193
194 #[must_use]
196 pub fn with_message(mut self, message: impl Into<Cow<'static, str>>) -> Self {
197 self.inner.message = message.into();
198 self
199 }
200
201 #[must_use]
206 pub fn with_details(mut self, details: impl Serialize) -> Self {
207 match serde_json::to_value(details) {
208 Ok(value) => self.inner.details = Some(value),
209 Err(err) => tracing::warn!(error = %err, "failed to serialize error details"),
210 }
211 self
212 }
213
214 #[must_use]
216 pub fn with_source(mut self, source: impl Into<BoxError>) -> Self {
217 self.inner.source = Some(source.into());
218 self
219 }
220
221 #[must_use]
223 pub fn with_header(mut self, name: HeaderName, value: HeaderValue) -> Self {
224 self.inner.headers.push((name, value));
225 self
226 }
227
228 pub fn status(&self) -> StatusCode {
230 self.inner.status
231 }
232
233 pub fn code(&self) -> &str {
235 &self.inner.code
236 }
237
238 pub fn message(&self) -> &str {
240 &self.inner.message
241 }
242
243 pub fn details(&self) -> Option<&serde_json::Value> {
245 self.inner.details.as_ref()
246 }
247
248 pub fn headers(&self) -> &[(HeaderName, HeaderValue)] {
250 &self.inner.headers
251 }
252
253 pub fn source_ref(&self) -> Option<&(dyn std::error::Error + Send + Sync + 'static)> {
255 self.inner.source.as_deref()
256 }
257
258 pub fn to_json_body(&self) -> Vec<u8> {
260 #[derive(Serialize)]
261 struct Envelope<'a> {
262 error: Body<'a>,
263 }
264 #[derive(Serialize)]
265 struct Body<'a> {
266 code: &'a str,
267 message: &'a str,
268 #[serde(skip_serializing_if = "Option::is_none")]
269 details: Option<&'a serde_json::Value>,
270 }
271 let envelope = Envelope {
272 error: Body {
273 code: &self.inner.code,
274 message: &self.inner.message,
275 details: self.inner.details.as_ref(),
276 },
277 };
278 serde_json::to_vec(&envelope).unwrap_or_default()
280 }
281
282 fn log(&self) {
283 let status = self.inner.status.as_u16();
284 let code: &str = &self.inner.code;
285 match (&self.inner.source, self.inner.status.is_server_error()) {
286 (Some(source), true) => {
287 tracing::error!(status, code, error = %ErrorChain(source.as_ref()), "request failed")
288 }
289 (None, true) => {
290 tracing::error!(status, code, message = %self.inner.message, "request failed")
291 }
292 (Some(source), false) => {
293 tracing::debug!(status, code, error = %ErrorChain(source.as_ref()), "request rejected")
294 }
295 (None, false) => tracing::debug!(status, code, "request rejected"),
296 }
297 }
298}
299
300impl IntoResponse for Error {
301 fn into_response(self) -> Response {
302 self.log();
303 let body = self.to_json_body();
304 let Inner {
305 status, headers, ..
306 } = *self.inner;
307 let mut response = (status, body).into_response();
308 let map = response.headers_mut();
309 map.insert(
310 header::CONTENT_TYPE,
311 HeaderValue::from_static("application/json"),
312 );
313 for (name, value) in headers {
314 map.append(name, value);
315 }
316 response
317 }
318}
319
320impl fmt::Display for Error {
321 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322 write!(
323 f,
324 "{} {}: {}",
325 self.inner.status.as_u16(),
326 self.inner.code,
327 self.inner.message
328 )?;
329 if let Some(source) = &self.inner.source {
330 write!(f, ": {}", ErrorChain(source.as_ref()))?;
331 }
332 Ok(())
333 }
334}
335
336impl fmt::Debug for Error {
337 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338 if f.alternate() {
340 f.debug_struct("Error")
341 .field("status", &self.inner.status)
342 .field("code", &self.inner.code)
343 .field("message", &self.inner.message)
344 .field("details", &self.inner.details)
345 .field("source", &self.inner.source)
346 .finish()
347 } else {
348 fmt::Display::fmt(self, f)
349 }
350 }
351}
352
353impl std::error::Error for Error {
354 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
355 self.inner
356 .source
357 .as_ref()
358 .map(|s| s.as_ref() as &(dyn std::error::Error + 'static))
359 }
360}
361
362impl From<StatusCode> for Error {
363 fn from(status: StatusCode) -> Self {
366 let reason = status.canonical_reason().unwrap_or("Error");
367 let code = reason.to_ascii_uppercase().replace([' ', '-'], "_");
368 Self::new(status, code, reason)
369 }
370}
371
372impl From<std::io::Error> for Error {
373 fn from(err: std::io::Error) -> Self {
374 Self::internal(err)
375 }
376}
377
378impl From<serde_json::Error> for Error {
379 fn from(err: serde_json::Error) -> Self {
380 Self::internal(err)
381 }
382}
383
384impl From<http::Error> for Error {
385 fn from(err: http::Error) -> Self {
386 Self::internal(err)
387 }
388}
389
390impl From<crate::ServerError> for Error {
391 fn from(err: crate::ServerError) -> Self {
392 Self::new(
393 StatusCode::INTERNAL_SERVER_ERROR,
394 "SERVER_ERROR",
395 INTERNAL_ERROR_MESSAGE,
396 )
397 .with_source(err)
398 }
399}
400
401impl From<crate::ConfigError> for Error {
402 fn from(err: crate::ConfigError) -> Self {
403 Self::new(
404 StatusCode::INTERNAL_SERVER_ERROR,
405 "CONFIG_ERROR",
406 INTERNAL_ERROR_MESSAGE,
407 )
408 .with_source(err)
409 }
410}
411
412struct ErrorChain<'a>(&'a (dyn std::error::Error + 'static));
414
415impl fmt::Display for ErrorChain<'_> {
416 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
417 write!(f, "{}", self.0)?;
418 let mut current = self.0.source();
419 while let Some(source) = current {
420 write!(f, ": {source}")?;
421 current = source.source();
422 }
423 Ok(())
424 }
425}
426
427pub trait ResultExt<T> {
429 fn or_internal(self) -> Result<T>;
432
433 fn or_error(self, error: Error) -> Result<T>;
435}
436
437impl<T, E> ResultExt<T> for std::result::Result<T, E>
438where
439 E: Into<BoxError>,
440{
441 fn or_internal(self) -> Result<T> {
442 self.map_err(Error::internal)
443 }
444
445 fn or_error(self, error: Error) -> Result<T> {
446 self.map_err(|source| error.with_source(source))
447 }
448}
449
450pub trait OptionExt<T> {
452 fn or_not_found(self, message: impl Into<Cow<'static, str>>) -> Result<T>;
454
455 fn or_error(self, error: Error) -> Result<T>;
457}
458
459impl<T> OptionExt<T> for Option<T> {
460 fn or_not_found(self, message: impl Into<Cow<'static, str>>) -> Result<T> {
461 self.ok_or_else(|| Error::not_found(message))
462 }
463
464 fn or_error(self, error: Error) -> Result<T> {
465 self.ok_or(error)
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472 use http_body_util::BodyExt;
473
474 async fn body_json(response: Response) -> serde_json::Value {
475 let bytes = response.into_body().collect().await.unwrap().to_bytes();
476 serde_json::from_slice(&bytes).unwrap()
477 }
478
479 #[test]
480 fn error_is_pointer_sized() {
481 assert_eq!(std::mem::size_of::<Error>(), std::mem::size_of::<usize>());
482 }
483
484 #[tokio::test]
485 async fn renders_standard_envelope() {
486 let err = Error::not_found("The requested user does not exist").with_code("USER_NOT_FOUND");
487 let response = err.into_response();
488 assert_eq!(response.status(), StatusCode::NOT_FOUND);
489 assert_eq!(response.headers()[header::CONTENT_TYPE], "application/json");
490 let body = body_json(response).await;
491 assert_eq!(
492 body,
493 serde_json::json!({"error": {"code": "USER_NOT_FOUND", "message": "The requested user does not exist"}})
494 );
495 }
496
497 #[tokio::test]
498 async fn internal_errors_never_expose_their_source() {
499 let source = std::io::Error::other("connection to db.internal:5432 refused");
500 let response = Error::internal(source).into_response();
501 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
502 let text = body_json(response).await.to_string();
503 assert!(!text.contains("db.internal"));
504 assert!(text.contains(INTERNAL_ERROR_MESSAGE));
505 }
506
507 #[tokio::test]
508 async fn validation_details_and_headers() {
509 let response = Error::validation(serde_json::json!({"email": ["invalid"]}))
510 .with_header(header::RETRY_AFTER, HeaderValue::from_static("1"))
511 .into_response();
512 assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
513 assert_eq!(response.headers()[header::RETRY_AFTER], "1");
514 let body = body_json(response).await;
515 assert_eq!(body["error"]["code"], "VALIDATION_FAILED");
516 assert_eq!(body["error"]["details"]["email"][0], "invalid");
517 }
518
519 #[test]
520 fn from_status_code() {
521 let err = Error::from(StatusCode::IM_A_TEAPOT);
522 assert_eq!(err.code(), "I'M_A_TEAPOT");
523 assert_eq!(err.status(), StatusCode::IM_A_TEAPOT);
524 let err = Error::from(StatusCode::GATEWAY_TIMEOUT);
525 assert_eq!(err.code(), "GATEWAY_TIMEOUT");
526 }
527
528 #[test]
529 fn result_and_option_extensions() {
530 let r: std::result::Result<(), std::io::Error> = Err(std::io::Error::other("boom"));
531 let err = r.or_internal().unwrap_err();
532 assert_eq!(err.status(), StatusCode::INTERNAL_SERVER_ERROR);
533 assert!(err.to_string().contains("boom"));
534
535 let err = None::<u8>.or_not_found("missing").unwrap_err();
536 assert_eq!(err.status(), StatusCode::NOT_FOUND);
537 assert_eq!(err.message(), "missing");
538 }
539
540 #[test]
541 fn domain_error_conversion_with_question_mark() {
542 #[derive(Debug)]
543 struct Missing;
544 impl From<Missing> for Error {
545 fn from(_: Missing) -> Self {
546 Error::not_found("gone").with_code("GONE")
547 }
548 }
549 fn handler() -> Result<()> {
550 Err(Missing)?;
551 Ok(())
552 }
553 assert_eq!(handler().unwrap_err().code(), "GONE");
554 }
555}