use crate::response::{ApiErrorDetail, ApiResponse};
use thiserror::Error;
#[derive(Debug, Clone, Copy)]
pub enum ErrorCode {
Validation,
Unauthorized,
Forbidden,
NotFound,
Conflict,
RateLimited,
Internal,
External,
ServiceUnavailable,
}
impl ErrorCode {
pub fn as_str(self) -> &'static str {
match self {
ErrorCode::Validation => "VALIDATION_ERROR",
ErrorCode::Unauthorized => "UNAUTHORIZED",
ErrorCode::Forbidden => "FORBIDDEN",
ErrorCode::NotFound => "NOT_FOUND",
ErrorCode::Conflict => "CONFLICT",
ErrorCode::RateLimited => "RATE_LIMITED",
ErrorCode::Internal => "INTERNAL_ERROR",
ErrorCode::External => "EXTERNAL_ERROR",
ErrorCode::ServiceUnavailable => "SERVICE_UNAVAILABLE",
}
}
}
#[derive(Error, Debug)]
pub enum AppError {
#[error("Validation failed")]
Validation {
message: String,
#[source]
source: Option<anyhow::Error>,
details: Vec<ApiErrorDetail>,
},
#[error("Unauthorized: {message}")]
Unauthorized { message: String },
#[error("Forbidden: {message}")]
Forbidden { message: String },
#[error("Not found: {message}")]
NotFound { message: String },
#[error("Conflict: {message}")]
Conflict { message: String },
#[error("Rate limited: {message}")]
RateLimited { message: String },
#[error("Internal error")]
Internal {
message: String,
#[source]
source: Option<anyhow::Error>,
},
#[error("External dependency error")]
External {
message: String,
#[source]
source: Option<anyhow::Error>,
},
#[error("Service unavailable: {message}")]
ServiceUnavailable { message: String },
}
impl AppError {
pub fn code(&self) -> ErrorCode {
match self {
AppError::Validation { .. } => ErrorCode::Validation,
AppError::Unauthorized { .. } => ErrorCode::Unauthorized,
AppError::Forbidden { .. } => ErrorCode::Forbidden,
AppError::NotFound { .. } => ErrorCode::NotFound,
AppError::Conflict { .. } => ErrorCode::Conflict,
AppError::RateLimited { .. } => ErrorCode::RateLimited,
AppError::Internal { .. } => ErrorCode::Internal,
AppError::External { .. } => ErrorCode::External,
AppError::ServiceUnavailable { .. } => ErrorCode::ServiceUnavailable,
}
}
pub fn public_message(&self) -> String {
match self {
AppError::Validation { message, .. } => message.clone(),
AppError::Unauthorized { message } => message.clone(),
AppError::Forbidden { message } => message.clone(),
AppError::NotFound { message } => message.clone(),
AppError::Conflict { message } => message.clone(),
AppError::RateLimited { message } => message.clone(),
AppError::Internal { .. } => "Internal server error".to_string(),
AppError::External { .. } => "Upstream service error".to_string(),
AppError::ServiceUnavailable { message } => message.clone(),
}
}
pub fn validation(message: impl Into<String>, details: Vec<ApiErrorDetail>) -> Self {
Self::Validation {
message: message.into(),
source: None,
details,
}
}
pub fn internal(message: impl Into<String>, source: Option<anyhow::Error>) -> Self {
Self::Internal {
message: message.into(),
source,
}
}
pub fn external(message: impl Into<String>, source: Option<anyhow::Error>) -> Self {
Self::External {
message: message.into(),
source,
}
}
pub fn to_api_response(&self, request_id: impl Into<String>) -> ApiResponse<()> {
let rid = request_id.into();
match self {
AppError::Validation { details, .. } => {
let mut errs = details.clone();
if errs.is_empty() {
errs.push(ApiErrorDetail::new(self.code().as_str(), self.public_message()));
}
ApiResponse::err(errs, rid)
}
_ => ApiResponse::err(
vec![ApiErrorDetail::new(self.code().as_str(), self.public_message())],
rid,
),
}
}
}
#[cfg(feature = "web")]
mod web_impl {
use super::*;
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
impl AppError {
pub fn http_status(&self) -> StatusCode {
match self {
AppError::Validation { .. } => StatusCode::BAD_REQUEST,
AppError::Unauthorized { .. } => StatusCode::UNAUTHORIZED,
AppError::Forbidden { .. } => StatusCode::FORBIDDEN,
AppError::NotFound { .. } => StatusCode::NOT_FOUND,
AppError::Conflict { .. } => StatusCode::CONFLICT,
AppError::RateLimited { .. } => StatusCode::TOO_MANY_REQUESTS,
AppError::Internal { .. } => StatusCode::INTERNAL_SERVER_ERROR,
AppError::External { .. } => StatusCode::BAD_GATEWAY,
AppError::ServiceUnavailable { .. } => StatusCode::SERVICE_UNAVAILABLE,
}
}
}
impl ResponseError for AppError {
fn status_code(&self) -> StatusCode {
self.http_status()
}
fn error_response(&self) -> HttpResponse {
let request_id = uuid::Uuid::new_v4().to_string();
#[cfg(feature = "observability")]
tracing::error!(error_code = %self.code().as_str(), error = ?self, "request failed");
let body = self.to_api_response(request_id);
HttpResponse::build(self.status_code()).json(body)
}
}
}