Alternative ATProto PDS implementation
at oauth 9.1 kB view raw
1//! Error handling for the application. 2use axum::{ 3 body::Body, 4 http::StatusCode, 5 response::{IntoResponse, Response}, 6}; 7use rsky_pds::handle::{self, errors::ErrorKind}; 8use thiserror::Error; 9use tracing::error; 10 11/// `axum`-compatible error handler. 12#[derive(Error)] 13#[expect(clippy::error_impl_error, reason = "just one")] 14pub struct Error { 15 /// The actual error that occurred. 16 err: anyhow::Error, 17 /// The error message to be returned as JSON body. 18 message: Option<ErrorMessage>, 19 /// The HTTP status code to be returned. 20 status: StatusCode, 21} 22 23#[derive(Default, serde::Serialize)] 24/// A JSON error message. 25pub(crate) struct ErrorMessage { 26 /// The error type. 27 /// This is used to identify the error in the client. 28 /// E.g. `InvalidRequest`, `ExpiredToken`, `InvalidToken`, `HandleNotFound`. 29 error: String, 30 /// The error message. 31 message: String, 32} 33impl std::fmt::Display for ErrorMessage { 34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 35 write!( 36 f, 37 r#"{{"error":"{}","message":"{}"}}"#, 38 self.error, self.message 39 ) 40 } 41} 42impl ErrorMessage { 43 /// Create a new error message to be returned as JSON body. 44 pub(crate) fn new(error: impl Into<String>, message: impl Into<String>) -> Self { 45 Self { 46 error: error.into(), 47 message: message.into(), 48 } 49 } 50} 51 52impl Error { 53 /// Returned when a route is not yet implemented. 54 pub fn unimplemented<T: Into<anyhow::Error>>(err: T) -> Self { 55 Self::with_status(StatusCode::NOT_IMPLEMENTED, err) 56 } 57 /// Returned when providing a status code and a JSON message body. 58 pub(crate) fn with_message( 59 status: StatusCode, 60 err: impl Into<anyhow::Error>, 61 message: impl Into<ErrorMessage>, 62 ) -> Self { 63 Self { 64 status, 65 err: err.into(), 66 message: Some(message.into()), 67 } 68 } 69 /// Returned when just providing a status code. 70 pub fn with_status<T: Into<anyhow::Error>>(status: StatusCode, err: T) -> Self { 71 Self { 72 status, 73 err: err.into(), 74 message: None, 75 } 76 } 77} 78 79impl From<anyhow::Error> for Error { 80 fn from(err: anyhow::Error) -> Self { 81 Self { 82 status: StatusCode::INTERNAL_SERVER_ERROR, 83 err, 84 message: None, 85 } 86 } 87} 88 89impl std::fmt::Display for Error { 90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 91 write!(f, "{}: {}", self.status, self.err) 92 } 93} 94 95impl std::fmt::Debug for Error { 96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 97 self.err.fmt(f) 98 } 99} 100 101impl IntoResponse for Error { 102 fn into_response(self) -> Response { 103 error!("{:?}", self.err); 104 105 // N.B: Forward out the error message to the requester if this is a debug build. 106 // This is insecure for production builds, so we'll return an empty body if this 107 // is a release build, unless a message was explicitly set. 108 if cfg!(debug_assertions) { 109 Response::builder() 110 .status(self.status) 111 .body(Body::new(format!("{:?}", self.err))) 112 .expect("should be a valid response") 113 } else { 114 Response::builder() 115 .status(self.status) 116 .header("Content-Type", "application/json") 117 .body(Body::new(self.message.unwrap_or_default().to_string())) 118 .expect("should be a valid response") 119 } 120 } 121} 122 123/// API error types that can be returned to clients 124#[derive(Clone, Debug)] 125pub enum ApiError { 126 RuntimeError, 127 InvalidLogin, 128 AccountTakendown, 129 InvalidRequest(String), 130 ExpiredToken, 131 InvalidToken, 132 RecordNotFound, 133 InvalidHandle, 134 InvalidEmail, 135 InvalidPassword, 136 InvalidInviteCode, 137 HandleNotAvailable, 138 EmailNotAvailable, 139 UnsupportedDomain, 140 UnresolvableDid, 141 IncompatibleDidDoc, 142 WellKnownNotFound, 143 AccountNotFound, 144 BlobNotFound, 145 BadRequest(String, String), 146 AuthRequiredError(String), 147} 148 149impl ApiError { 150 /// Get the appropriate HTTP status code for this error 151 const fn status_code(&self) -> StatusCode { 152 match self { 153 Self::RuntimeError => StatusCode::INTERNAL_SERVER_ERROR, 154 Self::InvalidLogin 155 | Self::ExpiredToken 156 | Self::InvalidToken 157 | Self::AuthRequiredError(_) => StatusCode::UNAUTHORIZED, 158 Self::AccountTakendown => StatusCode::FORBIDDEN, 159 Self::RecordNotFound 160 | Self::WellKnownNotFound 161 | Self::AccountNotFound 162 | Self::BlobNotFound => StatusCode::NOT_FOUND, 163 // All bad requests grouped together 164 _ => StatusCode::BAD_REQUEST, 165 } 166 } 167 168 /// Get the error type string for API responses 169 fn error_type(&self) -> String { 170 match self { 171 Self::RuntimeError => "InternalServerError", 172 Self::InvalidLogin => "InvalidLogin", 173 Self::AccountTakendown => "AccountTakendown", 174 Self::InvalidRequest(_) => "InvalidRequest", 175 Self::ExpiredToken => "ExpiredToken", 176 Self::InvalidToken => "InvalidToken", 177 Self::RecordNotFound => "RecordNotFound", 178 Self::InvalidHandle => "InvalidHandle", 179 Self::InvalidEmail => "InvalidEmail", 180 Self::InvalidPassword => "InvalidPassword", 181 Self::InvalidInviteCode => "InvalidInviteCode", 182 Self::HandleNotAvailable => "HandleNotAvailable", 183 Self::EmailNotAvailable => "EmailNotAvailable", 184 Self::UnsupportedDomain => "UnsupportedDomain", 185 Self::UnresolvableDid => "UnresolvableDid", 186 Self::IncompatibleDidDoc => "IncompatibleDidDoc", 187 Self::WellKnownNotFound => "WellKnownNotFound", 188 Self::AccountNotFound => "AccountNotFound", 189 Self::BlobNotFound => "BlobNotFound", 190 Self::BadRequest(error, _) => error, 191 Self::AuthRequiredError(_) => "AuthRequiredError", 192 } 193 .to_owned() 194 } 195 196 /// Get the user-facing error message 197 fn message(&self) -> String { 198 match self { 199 Self::RuntimeError => "Something went wrong", 200 Self::InvalidLogin => "Invalid identifier or password", 201 Self::AccountTakendown => "Account has been taken down", 202 Self::InvalidRequest(msg) => msg, 203 Self::ExpiredToken => "Token is expired", 204 Self::InvalidToken => "Token is invalid", 205 Self::RecordNotFound => "Record could not be found", 206 Self::InvalidHandle => "Handle is invalid", 207 Self::InvalidEmail => "Invalid email", 208 Self::InvalidPassword => "Invalid Password", 209 Self::InvalidInviteCode => "Invalid invite code", 210 Self::HandleNotAvailable => "Handle not available", 211 Self::EmailNotAvailable => "Email not available", 212 Self::UnsupportedDomain => "Unsupported domain", 213 Self::UnresolvableDid => "Unresolved Did", 214 Self::IncompatibleDidDoc => "IncompatibleDidDoc", 215 Self::WellKnownNotFound => "User not found", 216 Self::AccountNotFound => "Account could not be found", 217 Self::BlobNotFound => "Blob could not be found", 218 Self::BadRequest(_, msg) => msg, 219 Self::AuthRequiredError(msg) => msg, 220 } 221 .to_owned() 222 } 223} 224 225impl From<Error> for ApiError { 226 fn from(_value: Error) -> Self { 227 Self::RuntimeError 228 } 229} 230 231impl From<anyhow::Error> for ApiError { 232 fn from(_value: anyhow::Error) -> Self { 233 Self::RuntimeError 234 } 235} 236 237impl From<handle::errors::Error> for ApiError { 238 fn from(value: handle::errors::Error) -> Self { 239 match value.kind { 240 ErrorKind::InvalidHandle => Self::InvalidHandle, 241 ErrorKind::HandleNotAvailable => Self::HandleNotAvailable, 242 ErrorKind::UnsupportedDomain => Self::UnsupportedDomain, 243 ErrorKind::InternalError => Self::RuntimeError, 244 } 245 } 246} 247 248impl IntoResponse for ApiError { 249 fn into_response(self) -> Response { 250 let status = self.status_code(); 251 let error_type = self.error_type(); 252 let message = self.message(); 253 254 if cfg!(debug_assertions) { 255 error!("API Error: {}: {}", error_type, message); 256 } 257 258 // Create the error message and serialize to JSON 259 let error_message = ErrorMessage::new(error_type, message); 260 let body = serde_json::to_string(&error_message).unwrap_or_else(|_| { 261 r#"{"error":"InternalServerError","message":"Error serializing response"}"#.to_owned() 262 }); 263 264 // Build the response 265 Response::builder() 266 .status(status) 267 .header("Content-Type", "application/json") 268 .body(Body::new(body)) 269 .expect("should be a valid response") 270 } 271}