Skip to main content

filigree/errors/
http_error.rs

1use std::{borrow::Cow, fmt::Debug, ops::Deref, sync::Arc};
2
3use axum::{
4    http::StatusCode,
5    response::{IntoResponse, Response},
6    Json,
7};
8use error_stack::Report;
9use serde::Serialize;
10use tracing::{event, Level};
11
12/// An error that can be returned from an HTTP endpoint
13pub trait HttpError: ToString + std::fmt::Debug {
14    /// The type of the error detail. Can be [()] if there is no detail for this error.
15    type Detail: Serialize + Debug + Send + Sync + 'static;
16
17    /// The status code that the error should return.
18    fn status_code(&self) -> StatusCode;
19    /// An error code that may provide additional information to clients on how to behave in
20    /// response to the error.
21    fn error_kind(&self) -> &'static str;
22
23    /// Extra detail about this error
24    fn error_detail(&self) -> Self::Detail;
25
26    /// The status code and data for this error. Most implementors of this trait will not
27    /// need to override the default implementation.
28    fn response_tuple(&self) -> (StatusCode, ErrorResponseData<Self::Detail>) {
29        (
30            self.status_code(),
31            ErrorResponseData::new(self.error_kind(), self.to_string(), self.error_detail()),
32        )
33    }
34
35    /// Return a value to force the [ObfuscateErrorLayer] to obfuscate this error's response in production, even if
36    /// it would not otherwise do so.
37    fn obfuscate(&self) -> Option<ForceObfuscate> {
38        None
39    }
40
41    /// Convert the error into a [Response]. Most implementors of this trait will not
42    /// need to override the default implementation.
43    fn to_response(&self) -> Response {
44        let (code, err) = self.response_tuple();
45        event!(Level::ERROR, error.code=%code, error.kind=%err.error.kind, error=%err.error.message, error.details=?err.error.details);
46
47        let form = err.form.clone();
48        let mut response = (code, Json(err)).into_response();
49
50        if let Some(mut obfuscate) = self.obfuscate() {
51            // Attach form to the obfuscated data if present, since we want to pass it through.
52            if obfuscate.form.is_none() {
53                obfuscate = if let Some(form) = form {
54                    obfuscate.with_form(form)
55                } else {
56                    obfuscate
57                };
58            }
59
60            response.extensions_mut().insert(obfuscate);
61        }
62
63        response
64    }
65}
66
67/// Error kind codes to return to the client in an error response.
68pub enum ErrorKind {
69    /// Invalid API key format
70    ApiKeyFormat,
71    /// A generic ErrorKind when obfuscating a bad request error
72    BadRequest,
73    /// Error communicating with the database
74    Database,
75    /// Error initializing the database connection
76    DatabaseInit,
77    /// User or organization is inactive
78    Disabled,
79    /// Error from the email sending service
80    EmailSendFailure,
81    /// A permissions predicate failed
82    FailedPredicate,
83    /// An OAuth login seemed to work, but fetching the user's details failed.
84    FetchOAuthUserDetails,
85    /// The password was incorrect. In production this should be obfuscated
86    IncorrectPassword,
87    /// The API key provided in a request was invalid or disabled
88    InvalidApiKey,
89    /// The Host header supplied in a request did not match an expected host
90    InvalidHostHeader,
91    /// The token provided in a reset request was invalid or expired
92    InvalidToken,
93    /// I/O error
94    IO,
95    /// The requested operation requires a permission that the client does not have
96    MissingPermission,
97    /// The requested object was not found
98    NotFound,
99    /// The user's account has not yet been verified
100    NotVerified,
101    /// An error roccurred exchanging an OAuith refresh token for a new access token
102    OAuthExchangeError,
103    /// The requested OAuth provider is not supported
104    OAuthProviderNotSupported,
105    /// The OAuth session provided in a request was invalid or expired
106    OAuthSessionExpired,
107    /// The OAuth session provided in a request was not found
108    OAuthSessionNotFound,
109    /// The client requested a sort order that is not supported for the given model
110    OrderBy,
111    /// The password and confirmation fields supplied by the client do not match.
112    PasswordConfirmMismatch,
113    /// Internal error with the password hashing mechanism
114    PasswordHasherError,
115    /// Error reading the request body
116    RequestRead,
117    /// Failed to start the server
118    ServerStart,
119    /// Internal error with the session backend
120    SessionBackend,
121    /// Error while shutting down the server
122    Shutdown,
123    /// Creation of new user accounts is disabled
124    SignupDisabled,
125    /// Error communicating with storage
126    Storage,
127    /// The user is not logged in
128    Unauthenticated,
129    /// An upload failed in some way not covered by a more specific error message
130    UploadFailed,
131    /// An uploaded file was larger than the configured limit
132    UploadTooLarge,
133    /// Internal error while creating a user
134    UserCreationError,
135    /// The requested user does not exist
136    UserNotFound,
137}
138
139impl ErrorKind {
140    /// Return the string form of the error kind code
141    pub fn as_str(&self) -> &'static str {
142        match self {
143            Self::ApiKeyFormat => "invalid_api_key",
144            Self::BadRequest => "bad_request",
145            Self::Database => "database",
146            Self::DatabaseInit => "db_init",
147            Self::Disabled => "disabled",
148            Self::EmailSendFailure => "email_send_failure",
149            Self::FailedPredicate => "failed_authz_condition",
150            Self::FetchOAuthUserDetails => "fetch_oauth_user_details",
151            Self::IncorrectPassword => "incorrect_password",
152            Self::InvalidApiKey => "invalid_api_key",
153            Self::InvalidHostHeader => "invalid_host_header",
154            Self::InvalidToken => "invalid_token",
155            Self::IO => "io_error",
156            Self::MissingPermission => "missing_permission",
157            Self::NotFound => "not_found",
158            Self::NotVerified => "not_verified",
159            Self::OAuthExchangeError => "oauth_exchange_error",
160            Self::OAuthProviderNotSupported => "oauth_provider_not_supported",
161            Self::OAuthSessionExpired => "oauth_session_expired",
162            Self::OAuthSessionNotFound => "oauth_session_not_found",
163            Self::OrderBy => "order_by",
164            Self::PasswordConfirmMismatch => "password_mismatch",
165            Self::PasswordHasherError => "password_hash_internal",
166            Self::RequestRead => "request_read",
167            Self::ServerStart => "server",
168            Self::SessionBackend => "session_backend",
169            Self::Shutdown => "shutdown",
170            Self::SignupDisabled => "signup_disabled",
171            Self::Storage => "storage",
172            Self::Unauthenticated => "unauthenticated",
173            Self::UploadFailed => "upload_failed",
174            Self::UploadTooLarge => "upload_too_large",
175            Self::UserCreationError => "user_creation_error",
176            Self::UserNotFound => "user_not_found",
177        }
178    }
179}
180
181impl Into<Cow<'static, str>> for ErrorKind {
182    fn into(self) -> Cow<'static, str> {
183        Cow::Borrowed(self.as_str())
184    }
185}
186
187/// Force error obfuscation and customize the values returned to the user.
188#[derive(Clone, Debug, Default)]
189pub struct ForceObfuscate {
190    /// The code to return in the error
191    pub kind: Cow<'static, str>,
192    /// The message to return to in the error
193    pub message: Cow<'static, str>,
194    /// Form data which should be returned even if the error is obfuscated
195    pub form: Option<Arc<serde_json::Value>>,
196}
197
198impl ForceObfuscate {
199    /// Create a new ForceObfuscate
200    pub fn new(kind: impl Into<Cow<'static, str>>, message: impl Into<Cow<'static, str>>) -> Self {
201        Self {
202            kind: kind.into(),
203            message: message.into(),
204            form: None,
205        }
206    }
207
208    /// Attach form data to the obfuscated error
209    pub fn with_form(self, form: Arc<serde_json::Value>) -> Self {
210        Self {
211            form: Some(form),
212            ..self
213        }
214    }
215
216    /// A generic "Unauthenticated" error to return when the details of an authentication failure
217    /// should be obfuscated.
218    pub fn unauthenticated() -> Self {
219        Self::new(ErrorKind::Unauthenticated, "Unauthenticated")
220    }
221}
222
223/// Attach this to a [Report] to include form data when rendering the error response.
224#[derive(Debug)]
225pub struct FormDataResponse(pub Arc<serde_json::Value>);
226
227impl FormDataResponse {
228    /// Create a new FormDataResponse
229    pub fn new(form: Arc<serde_json::Value>) -> Self {
230        Self(form)
231    }
232}
233
234impl<T> HttpError for error_stack::Report<T>
235where
236    T: HttpError + Send + Sync + 'static,
237{
238    type Detail = String;
239
240    fn response_tuple(&self) -> (StatusCode, ErrorResponseData<Self::Detail>) {
241        let err = ErrorResponseData::new(self.error_kind(), self.to_string(), self.error_detail());
242        let err = if let Some(form_data) = self
243            .frames()
244            .find_map(|frame| frame.downcast_ref::<FormDataResponse>())
245        {
246            err.with_form(Some(form_data.0.clone()))
247        } else {
248            err
249        };
250
251        (self.status_code(), err)
252    }
253
254    fn obfuscate(&self) -> Option<ForceObfuscate> {
255        self.current_context().obfuscate()
256    }
257
258    fn status_code(&self) -> StatusCode {
259        self.current_context().status_code()
260    }
261
262    fn error_kind(&self) -> &'static str {
263        self.current_context().error_kind()
264    }
265
266    /// Send the entire report detail as the detail
267    fn error_detail(&self) -> String {
268        format!("{self:?}")
269    }
270}
271
272/// A body to be returned in an error response
273#[derive(Debug, Serialize)]
274pub struct ErrorResponseData<T: Debug + Serialize> {
275    #[serde(skip_serializing_if = "Option::is_none")]
276    form: Option<Arc<serde_json::Value>>,
277    error: ErrorDetails<T>,
278}
279
280/// An error code and additional details.
281#[derive(Debug, Serialize)]
282pub struct ErrorDetails<T: Debug + Serialize> {
283    kind: Cow<'static, str>,
284    message: Cow<'static, str>,
285    details: T,
286}
287
288impl<T: Debug + Serialize> ErrorResponseData<T> {
289    /// Create a new [ErrorResponseData] with the given error code and message.
290    pub fn new(
291        kind: impl Into<Cow<'static, str>>,
292        message: impl Into<Cow<'static, str>>,
293        details: T,
294    ) -> ErrorResponseData<T> {
295        let ret = ErrorResponseData {
296            form: None,
297            error: ErrorDetails {
298                kind: kind.into(),
299                message: message.into(),
300                details: details.into(),
301            },
302        };
303
304        event!(Level::ERROR, kind=%ret.error.kind, message=%ret.error.message, details=?ret.error.details);
305
306        ret
307    }
308
309    /// Attach form details to the error response
310    pub fn with_form(self, form: Option<Arc<serde_json::Value>>) -> Self {
311        Self {
312            form,
313            error: self.error,
314        }
315    }
316}
317
318/// Wraps an error_stack::Report and implements IntoResponse, allowing easy return of a Report<T>
319/// from an Axum endpoint.
320pub struct WrapReport<T: HttpError + Sync + Send + 'static>(error_stack::Report<T>);
321
322impl<T: HttpError + Sync + Send + 'static> IntoResponse for WrapReport<T> {
323    fn into_response(self) -> Response {
324        self.0.to_response()
325    }
326}
327
328impl<T: HttpError + Sync + Send + 'static> From<Report<T>> for WrapReport<T> {
329    fn from(value: Report<T>) -> Self {
330        WrapReport(value)
331    }
332}
333
334impl<T: HttpError + std::error::Error + Sync + Send + 'static> From<T> for WrapReport<T> {
335    fn from(value: T) -> Self {
336        WrapReport(Report::from(value))
337    }
338}
339
340impl<T: HttpError + Sync + Send + 'static> Deref for WrapReport<T> {
341    type Target = error_stack::Report<T>;
342
343    fn deref(&self) -> &Self::Target {
344        &self.0
345    }
346}
347
348#[cfg(test)]
349mod test {
350    use serde_json::json;
351
352    use super::*;
353    use crate::auth::AuthError;
354
355    #[test]
356    fn report_form_data_attachment() {
357        let err = Report::new(AuthError::Unauthenticated).attach(FormDataResponse::new(Arc::new(
358            json!({ "email": "abc@example.com" }),
359        )));
360
361        let (_, data) = err.response_tuple();
362        let form = data.form.unwrap();
363        assert_eq!(form.as_ref(), &json!({ "email": "abc@example.com" }));
364    }
365}