use std::{borrow::Cow, fmt::Debug, ops::Deref, sync::Arc};
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use error_stack::Report;
use serde::Serialize;
use tracing::{event, Level};
pub trait HttpError: ToString + std::fmt::Debug {
type Detail: Serialize + Debug + Send + Sync + 'static;
fn status_code(&self) -> StatusCode;
fn error_kind(&self) -> &'static str;
fn error_detail(&self) -> Self::Detail;
fn response_tuple(&self) -> (StatusCode, ErrorResponseData<Self::Detail>) {
(
self.status_code(),
ErrorResponseData::new(self.error_kind(), self.to_string(), self.error_detail()),
)
}
fn obfuscate(&self) -> Option<ForceObfuscate> {
None
}
fn to_response(&self) -> Response {
let (code, err) = self.response_tuple();
event!(Level::ERROR, error.code=%code, error.kind=%err.error.kind, error=%err.error.message, error.details=?err.error.details);
let form = err.form.clone();
let mut response = (code, Json(err)).into_response();
if let Some(mut obfuscate) = self.obfuscate() {
if obfuscate.form.is_none() {
obfuscate = if let Some(form) = form {
obfuscate.with_form(form)
} else {
obfuscate
};
}
response.extensions_mut().insert(obfuscate);
}
response
}
}
pub enum ErrorKind {
ApiKeyFormat,
BadRequest,
Database,
DatabaseInit,
Disabled,
EmailSendFailure,
FailedPredicate,
FetchOAuthUserDetails,
IncorrectPassword,
InvalidApiKey,
InvalidHostHeader,
InvalidToken,
IO,
MissingPermission,
NotFound,
NotVerified,
OAuthExchangeError,
OAuthProviderNotSupported,
OAuthSessionExpired,
OAuthSessionNotFound,
OrderBy,
PasswordConfirmMismatch,
PasswordHasherError,
RequestRead,
ServerStart,
SessionBackend,
Shutdown,
SignupDisabled,
Storage,
Unauthenticated,
UploadFailed,
UploadTooLarge,
UserCreationError,
UserNotFound,
}
impl ErrorKind {
pub fn as_str(&self) -> &'static str {
match self {
Self::ApiKeyFormat => "invalid_api_key",
Self::BadRequest => "bad_request",
Self::Database => "database",
Self::DatabaseInit => "db_init",
Self::Disabled => "disabled",
Self::EmailSendFailure => "email_send_failure",
Self::FailedPredicate => "failed_authz_condition",
Self::FetchOAuthUserDetails => "fetch_oauth_user_details",
Self::IncorrectPassword => "incorrect_password",
Self::InvalidApiKey => "invalid_api_key",
Self::InvalidHostHeader => "invalid_host_header",
Self::InvalidToken => "invalid_token",
Self::IO => "io_error",
Self::MissingPermission => "missing_permission",
Self::NotFound => "not_found",
Self::NotVerified => "not_verified",
Self::OAuthExchangeError => "oauth_exchange_error",
Self::OAuthProviderNotSupported => "oauth_provider_not_supported",
Self::OAuthSessionExpired => "oauth_session_expired",
Self::OAuthSessionNotFound => "oauth_session_not_found",
Self::OrderBy => "order_by",
Self::PasswordConfirmMismatch => "password_mismatch",
Self::PasswordHasherError => "password_hash_internal",
Self::RequestRead => "request_read",
Self::ServerStart => "server",
Self::SessionBackend => "session_backend",
Self::Shutdown => "shutdown",
Self::SignupDisabled => "signup_disabled",
Self::Storage => "storage",
Self::Unauthenticated => "unauthenticated",
Self::UploadFailed => "upload_failed",
Self::UploadTooLarge => "upload_too_large",
Self::UserCreationError => "user_creation_error",
Self::UserNotFound => "user_not_found",
}
}
}
impl Into<Cow<'static, str>> for ErrorKind {
fn into(self) -> Cow<'static, str> {
Cow::Borrowed(self.as_str())
}
}
#[derive(Clone, Debug, Default)]
pub struct ForceObfuscate {
pub kind: Cow<'static, str>,
pub message: Cow<'static, str>,
pub form: Option<Arc<serde_json::Value>>,
}
impl ForceObfuscate {
pub fn new(kind: impl Into<Cow<'static, str>>, message: impl Into<Cow<'static, str>>) -> Self {
Self {
kind: kind.into(),
message: message.into(),
form: None,
}
}
pub fn with_form(self, form: Arc<serde_json::Value>) -> Self {
Self {
form: Some(form),
..self
}
}
pub fn unauthenticated() -> Self {
Self::new(ErrorKind::Unauthenticated, "Unauthenticated")
}
}
#[derive(Debug)]
pub struct FormDataResponse(pub Arc<serde_json::Value>);
impl FormDataResponse {
pub fn new(form: Arc<serde_json::Value>) -> Self {
Self(form)
}
}
impl<T> HttpError for error_stack::Report<T>
where
T: HttpError + Send + Sync + 'static,
{
type Detail = String;
fn response_tuple(&self) -> (StatusCode, ErrorResponseData<Self::Detail>) {
let err = ErrorResponseData::new(self.error_kind(), self.to_string(), self.error_detail());
let err = if let Some(form_data) = self
.frames()
.find_map(|frame| frame.downcast_ref::<FormDataResponse>())
{
err.with_form(Some(form_data.0.clone()))
} else {
err
};
(self.status_code(), err)
}
fn obfuscate(&self) -> Option<ForceObfuscate> {
self.current_context().obfuscate()
}
fn status_code(&self) -> StatusCode {
self.current_context().status_code()
}
fn error_kind(&self) -> &'static str {
self.current_context().error_kind()
}
fn error_detail(&self) -> String {
format!("{self:?}")
}
}
#[derive(Debug, Serialize)]
pub struct ErrorResponseData<T: Debug + Serialize> {
#[serde(skip_serializing_if = "Option::is_none")]
form: Option<Arc<serde_json::Value>>,
error: ErrorDetails<T>,
}
#[derive(Debug, Serialize)]
pub struct ErrorDetails<T: Debug + Serialize> {
kind: Cow<'static, str>,
message: Cow<'static, str>,
details: T,
}
impl<T: Debug + Serialize> ErrorResponseData<T> {
pub fn new(
kind: impl Into<Cow<'static, str>>,
message: impl Into<Cow<'static, str>>,
details: T,
) -> ErrorResponseData<T> {
let ret = ErrorResponseData {
form: None,
error: ErrorDetails {
kind: kind.into(),
message: message.into(),
details: details.into(),
},
};
event!(Level::ERROR, kind=%ret.error.kind, message=%ret.error.message, details=?ret.error.details);
ret
}
pub fn with_form(self, form: Option<Arc<serde_json::Value>>) -> Self {
Self {
form,
error: self.error,
}
}
}
pub struct WrapReport<T: HttpError + Sync + Send + 'static>(error_stack::Report<T>);
impl<T: HttpError + Sync + Send + 'static> IntoResponse for WrapReport<T> {
fn into_response(self) -> Response {
self.0.to_response()
}
}
impl<T: HttpError + Sync + Send + 'static> From<Report<T>> for WrapReport<T> {
fn from(value: Report<T>) -> Self {
WrapReport(value)
}
}
impl<T: HttpError + std::error::Error + Sync + Send + 'static> From<T> for WrapReport<T> {
fn from(value: T) -> Self {
WrapReport(Report::from(value))
}
}
impl<T: HttpError + Sync + Send + 'static> Deref for WrapReport<T> {
type Target = error_stack::Report<T>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[cfg(test)]
mod test {
use serde_json::json;
use super::*;
use crate::auth::AuthError;
#[test]
fn report_form_data_attachment() {
let err = Report::new(AuthError::Unauthenticated).attach(FormDataResponse::new(Arc::new(
json!({ "email": "abc@example.com" }),
)));
let (_, data) = err.response_tuple();
let form = data.form.unwrap();
assert_eq!(form.as_ref(), &json!({ "email": "abc@example.com" }));
}
}