use thiserror::Error;
use actix_web::HttpResponse;
use serde::Serialize;
use std::borrow::Cow;
pub type Result<T> = std::result::Result<T, AppError>;
#[derive(Debug, Serialize)]
struct ErrorBody {
error: ErrorDetail,
}
#[derive(Debug, Serialize)]
struct ErrorDetail {
code: String,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
request_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
details: Option<Vec<FieldError>>,
}
#[derive(Debug, Serialize)]
struct FieldError {
field: String,
message: String,
}
#[derive(Debug, Error, serde::Serialize, serde::Deserialize)]
pub enum AppError {
#[error("Not found: {0}")]
NotFound(Cow<'static, str>),
#[error("Validation failed: {0}")]
Validation(Cow<'static, str>),
#[error("Unauthorized: {0}")]
Unauthorized(Cow<'static, str>),
#[error("Forbidden")]
Forbidden,
#[error("Conflict: {0}")]
Conflict(Cow<'static, str>),
#[error("Method not allowed: {0}")]
MethodNotAllowed(Cow<'static, str>),
#[error("Request timeout")]
Timeout,
#[error("Payload too large: {0}")]
PayloadTooLarge(Cow<'static, str>),
#[error("Too many requests: {0}")]
TooManyRequests(Cow<'static, str>),
#[error("Internal server error: {0}")]
Internal(Cow<'static, str>),
#[error("Service unavailable: {0}")]
Unavailable(Cow<'static, str>),
#[error("Gateway timeout: {0}")]
GatewayTimeout(Cow<'static, str>),
#[error(transparent)]
#[serde(skip)]
Database(#[from] sqlx::Error),
#[error(transparent)]
#[serde(skip)]
Io(#[from] std::io::Error),
#[error(transparent)]
#[serde(skip)]
Serde(#[from] serde_json::Error),
}
impl AppError {
pub fn not_found(msg: impl Into<Cow<'static, str>>) -> Self {
AppError::NotFound(msg.into())
}
pub fn validation(msg: impl Into<Cow<'static, str>>) -> Self {
AppError::Validation(msg.into())
}
pub fn unauthorized(msg: impl Into<Cow<'static, str>>) -> Self {
AppError::Unauthorized(msg.into())
}
pub fn internal(msg: impl Into<Cow<'static, str>>) -> Self {
AppError::Internal(msg.into())
}
pub fn status_code(&self) -> u16 {
match self {
AppError::NotFound(_) => 404,
AppError::Validation(_) => 400,
AppError::Unauthorized(_) => 401,
AppError::Forbidden => 403,
AppError::Conflict(_) => 409,
AppError::MethodNotAllowed(_) => 405,
AppError::Timeout => 408,
AppError::PayloadTooLarge(_) => 413,
AppError::TooManyRequests(_) => 429,
AppError::Internal(_) => 500,
AppError::Unavailable(_) => 503,
AppError::GatewayTimeout(_) => 504,
AppError::Database(_) | AppError::Io(_) | AppError::Serde(_) => 500,
}
}
pub fn code(&self) -> &'static str {
match self {
AppError::NotFound(_) => "NOT_FOUND",
AppError::Validation(_) => "VALIDATION_ERROR",
AppError::Unauthorized(_) => "UNAUTHORIZED",
AppError::Forbidden => "FORBIDDEN",
AppError::Conflict(_) => "CONFLICT",
AppError::MethodNotAllowed(_) => "METHOD_NOT_ALLOWED",
AppError::Timeout => "REQUEST_TIMEOUT",
AppError::PayloadTooLarge(_) => "PAYLOAD_TOO_LARGE",
AppError::TooManyRequests(_) => "TOO_MANY_REQUESTS",
AppError::Internal(_) => "INTERNAL_ERROR",
AppError::Unavailable(_) => "SERVICE_UNAVAILABLE",
AppError::GatewayTimeout(_) => "GATEWAY_TIMEOUT",
AppError::Database(_) => "DATABASE_ERROR",
AppError::Io(_) => "IO_ERROR",
AppError::Serde(_) => "SERIALIZATION_ERROR",
}
}
pub fn is_client_error(&self) -> bool {
matches!(self,
AppError::NotFound(_)
| AppError::Validation(_)
| AppError::Unauthorized(_)
| AppError::Forbidden
| AppError::Conflict(_)
| AppError::MethodNotAllowed(_)
| AppError::Timeout
| AppError::PayloadTooLarge(_)
| AppError::TooManyRequests(_)
)
}
pub fn is_server_error(&self) -> bool {
matches!(self,
AppError::Internal(_)
| AppError::Unavailable(_)
| AppError::GatewayTimeout(_)
| AppError::Database(_)
| AppError::Io(_)
| AppError::Serde(_)
)
}
pub fn to_response(&self, request_id: Option<&str>) -> HttpResponse {
let body = ErrorBody {
error: ErrorDetail {
code: self.code().to_string(),
message: self.to_string(),
request_id: request_id.map(String::from),
details: None,
},
};
let status = actix_web::http::StatusCode::from_u16(self.status_code())
.unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR);
HttpResponse::build(status).json(&body)
}
}
impl actix_web::ResponseError for AppError {
fn error_response(&self) -> HttpResponse {
self.to_response(None)
}
fn status_code(&self) -> actix_web::http::StatusCode {
actix_web::http::StatusCode::from_u16(self.status_code())
.unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR)
}
}