use axum::Json;
use axum::http::StatusCode;
use axum::http::header::WWW_AUTHENTICATE;
use axum::response::{IntoResponse, Response};
use std::collections::HashMap;
#[derive(thiserror::Error, Debug)]
pub enum AppError {
#[error("Bad Request")]
BadRequest {
error_values: Option<HashMap<String, String>>,
message: Option<String>,
description: Option<String>,
},
#[error("Authentication Required")]
Unauthorized,
#[error("Access Denied")]
Forbidden,
#[error("Not Found")]
NotFound { resource: String },
#[error("Conflict")]
Conflict { message: String },
#[error("Unprocessable Entity")]
UnprocessableEntity {
error_values: Option<HashMap<String, String>>,
message: Option<String>,
description: Option<String>,
},
#[error("Failed Dependency")]
DependencyFailed {
upstream: String,
detail: String,
retryable: bool,
},
#[error("Internal Error")]
InternalError {
error: String,
message: Option<String>,
},
#[error("Service Unavailable")]
#[allow(dead_code)]
Unavailable,
}
impl AppError {
fn status_code(&self) -> StatusCode {
match self {
Self::BadRequest { .. } => StatusCode::BAD_REQUEST,
Self::Unauthorized => StatusCode::UNAUTHORIZED,
Self::Forbidden => StatusCode::FORBIDDEN,
Self::NotFound { .. } => StatusCode::NOT_FOUND,
Self::Conflict { .. } => StatusCode::CONFLICT,
Self::UnprocessableEntity { .. } => StatusCode::UNPROCESSABLE_ENTITY,
Self::DependencyFailed { .. } => StatusCode::from_u16(424).unwrap(),
Self::InternalError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
Self::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
}
}
pub fn bad_request(
errors: Option<HashMap<String, String>>,
message: Option<String>,
description: Option<String>,
) -> Self {
Self::BadRequest {
error_values: errors,
message,
description,
}
}
pub fn not_found(resource: impl Into<String>) -> Self {
Self::NotFound {
resource: resource.into(),
}
}
pub fn conflict(message: impl Into<String>) -> Self {
Self::Conflict {
message: message.into(),
}
}
pub fn unprocessable_entity(
errors: Option<HashMap<String, String>>,
message: Option<String>,
description: Option<String>,
) -> Self {
Self::UnprocessableEntity {
error_values: errors,
message,
description,
}
}
pub fn dependency_failed(upstream: impl Into<String>, detail: impl Into<String>) -> Self {
Self::DependencyFailed {
upstream: upstream.into(),
detail: detail.into(),
retryable: true,
}
}
pub fn dependency_failed_permanent(
upstream: impl Into<String>,
detail: impl Into<String>,
) -> Self {
Self::DependencyFailed {
upstream: upstream.into(),
detail: detail.into(),
retryable: false,
}
}
pub fn internal_error(error: String, message: Option<String>) -> Self {
Self::InternalError { error, message }
}
pub fn is_dependency_error(&self) -> bool {
matches!(self, Self::DependencyFailed { .. })
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
match self {
Self::BadRequest {
error_values,
message,
description,
} => (
StatusCode::BAD_REQUEST,
Json(ErrorDetails {
error_values,
message,
description,
}),
)
.into_response(),
Self::Unauthorized => (
self.status_code(),
[(WWW_AUTHENTICATE, "Token")],
self.to_string(),
)
.into_response(),
Self::Forbidden => (StatusCode::FORBIDDEN, self.to_string()).into_response(),
Self::NotFound { resource } => (
StatusCode::NOT_FOUND,
Json(NotFoundDetails {
error: "not_found".to_string(),
resource,
}),
)
.into_response(),
Self::Conflict { message } => (
StatusCode::CONFLICT,
Json(ConflictDetails {
error: "conflict".to_string(),
message,
}),
)
.into_response(),
Self::UnprocessableEntity {
error_values,
message,
description,
} => (
StatusCode::UNPROCESSABLE_ENTITY,
Json(ErrorDetails {
error_values,
message,
description,
}),
)
.into_response(),
Self::DependencyFailed {
upstream,
detail,
retryable,
} => {
tracing::warn!(
upstream = %upstream,
detail = %detail,
retryable,
"returning 424: dependency failed"
);
(
StatusCode::from_u16(424).unwrap(),
Json(DependencyFailedDetails {
error: "dependency_failed".to_string(),
upstream,
message: "An upstream dependency is currently unavailable".to_string(),
retryable,
}),
)
.into_response()
}
Self::InternalError { error, message } => {
tracing::error!(internal_error = %error, message = ?message, "returning 500");
#[cfg(feature = "verbose-errors")]
let body = InternalErrorDetails { error, message };
#[cfg(not(feature = "verbose-errors"))]
let body = InternalErrorDetails {
error: "internal_error".to_string(),
message: Some("An unexpected error occurred".to_string()),
};
(StatusCode::INTERNAL_SERVER_ERROR, Json(body)).into_response()
}
Self::Unavailable => {
(StatusCode::SERVICE_UNAVAILABLE, self.to_string()).into_response()
}
}
}
}
#[derive(serde::Serialize)]
struct ErrorDetails {
error_values: Option<HashMap<String, String>>,
message: Option<String>,
description: Option<String>,
}
#[derive(serde::Serialize)]
struct NotFoundDetails {
error: String,
resource: String,
}
#[derive(serde::Serialize)]
struct ConflictDetails {
error: String,
message: String,
}
#[derive(serde::Serialize)]
struct DependencyFailedDetails {
error: String,
upstream: String,
message: String,
retryable: bool,
}
#[derive(serde::Serialize)]
struct InternalErrorDetails {
error: String,
message: Option<String>,
}