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}