Alternative ATProto PDS implementation
1//! Error handling for the application. 2use axum::{ 3 body::Body, 4 http::StatusCode, 5 response::{IntoResponse, Response}, 6}; 7use thiserror::Error; 8use tracing::error; 9 10/// `axum`-compatible error handler. 11#[derive(Error)] 12#[expect(clippy::error_impl_error, reason = "just one")] 13pub struct Error { 14 /// The actual error that occurred. 15 err: anyhow::Error, 16 /// The error message to be returned as JSON body. 17 message: Option<ErrorMessage>, 18 /// The HTTP status code to be returned. 19 status: StatusCode, 20} 21 22#[derive(Default, serde::Serialize)] 23/// A JSON error message. 24pub(crate) struct ErrorMessage { 25 /// The error type. 26 /// This is used to identify the error in the client. 27 /// E.g. `InvalidRequest`, `ExpiredToken`, `InvalidToken`, `HandleNotFound`. 28 error: String, 29 /// The error message. 30 message: String, 31} 32impl std::fmt::Display for ErrorMessage { 33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 34 write!( 35 f, 36 r#"{{"error":"{}","message":"{}"}}"#, 37 self.error, self.message 38 ) 39 } 40} 41impl ErrorMessage { 42 /// Create a new error message to be returned as JSON body. 43 pub(crate) fn new(error: impl Into<String>, message: impl Into<String>) -> Self { 44 Self { 45 error: error.into(), 46 message: message.into(), 47 } 48 } 49} 50 51impl Error { 52 /// Returned when a route is not yet implemented. 53 pub fn unimplemented<T: Into<anyhow::Error>>(err: T) -> Self { 54 Self::with_status(StatusCode::NOT_IMPLEMENTED, err) 55 } 56 /// Returned when providing a status code and a JSON message body. 57 pub(crate) fn with_message( 58 status: StatusCode, 59 err: impl Into<anyhow::Error>, 60 message: impl Into<ErrorMessage>, 61 ) -> Self { 62 Self { 63 status, 64 err: err.into(), 65 message: Some(message.into()), 66 } 67 } 68 /// Returned when just providing a status code. 69 pub fn with_status<T: Into<anyhow::Error>>(status: StatusCode, err: T) -> Self { 70 Self { 71 status, 72 err: err.into(), 73 message: None, 74 } 75 } 76} 77 78impl From<anyhow::Error> for Error { 79 fn from(err: anyhow::Error) -> Self { 80 Self { 81 status: StatusCode::INTERNAL_SERVER_ERROR, 82 err, 83 message: None, 84 } 85 } 86} 87 88impl std::fmt::Display for Error { 89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 90 write!(f, "{}: {}", self.status, self.err) 91 } 92} 93 94impl std::fmt::Debug for Error { 95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 96 self.err.fmt(f) 97 } 98} 99 100impl IntoResponse for Error { 101 fn into_response(self) -> Response { 102 error!("{:?}", self.err); 103 104 // N.B: Forward out the error message to the requester if this is a debug build. 105 // This is insecure for production builds, so we'll return an empty body if this 106 // is a release build, unless a message was explicitly set. 107 if cfg!(debug_assertions) { 108 Response::builder() 109 .status(self.status) 110 .body(Body::new(format!("{:?}", self.err))) 111 .expect("should be a valid response") 112 } else { 113 Response::builder() 114 .status(self.status) 115 .header("Content-Type", "application/json") 116 .body(Body::new(self.message.unwrap_or_default().to_string())) 117 .expect("should be a valid response") 118 } 119 } 120}