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
12pub trait HttpError: ToString + std::fmt::Debug {
14 type Detail: Serialize + Debug + Send + Sync + 'static;
16
17 fn status_code(&self) -> StatusCode;
19 fn error_kind(&self) -> &'static str;
22
23 fn error_detail(&self) -> Self::Detail;
25
26 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 fn obfuscate(&self) -> Option<ForceObfuscate> {
38 None
39 }
40
41 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 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
67pub enum ErrorKind {
69 ApiKeyFormat,
71 BadRequest,
73 Database,
75 DatabaseInit,
77 Disabled,
79 EmailSendFailure,
81 FailedPredicate,
83 FetchOAuthUserDetails,
85 IncorrectPassword,
87 InvalidApiKey,
89 InvalidHostHeader,
91 InvalidToken,
93 IO,
95 MissingPermission,
97 NotFound,
99 NotVerified,
101 OAuthExchangeError,
103 OAuthProviderNotSupported,
105 OAuthSessionExpired,
107 OAuthSessionNotFound,
109 OrderBy,
111 PasswordConfirmMismatch,
113 PasswordHasherError,
115 RequestRead,
117 ServerStart,
119 SessionBackend,
121 Shutdown,
123 SignupDisabled,
125 Storage,
127 Unauthenticated,
129 UploadFailed,
131 UploadTooLarge,
133 UserCreationError,
135 UserNotFound,
137}
138
139impl ErrorKind {
140 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#[derive(Clone, Debug, Default)]
189pub struct ForceObfuscate {
190 pub kind: Cow<'static, str>,
192 pub message: Cow<'static, str>,
194 pub form: Option<Arc<serde_json::Value>>,
196}
197
198impl ForceObfuscate {
199 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 pub fn with_form(self, form: Arc<serde_json::Value>) -> Self {
210 Self {
211 form: Some(form),
212 ..self
213 }
214 }
215
216 pub fn unauthenticated() -> Self {
219 Self::new(ErrorKind::Unauthenticated, "Unauthenticated")
220 }
221}
222
223#[derive(Debug)]
225pub struct FormDataResponse(pub Arc<serde_json::Value>);
226
227impl FormDataResponse {
228 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 fn error_detail(&self) -> String {
268 format!("{self:?}")
269 }
270}
271
272#[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#[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 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 pub fn with_form(self, form: Option<Arc<serde_json::Value>>) -> Self {
311 Self {
312 form,
313 error: self.error,
314 }
315 }
316}
317
318pub 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}