Skip to main content

karbon_framework/error/
app_error.rs

1use axum::http::StatusCode;
2use axum::response::{IntoResponse, Response};
3use serde::Serialize;
4
5/// Unified application error type
6#[derive(Debug, thiserror::Error)]
7pub enum AppError {
8    #[error("Not found: {0}")]
9    NotFound(String),
10
11    #[error("Bad request: {0}")]
12    BadRequest(String),
13
14    #[error("Unauthorized: {0}")]
15    Unauthorized(String),
16
17    #[error("Forbidden: {0}")]
18    Forbidden(String),
19
20    #[error("Conflict: {0}")]
21    Conflict(String),
22
23    #[error("Validation error: {0}")]
24    Validation(String),
25
26    #[error("Internal error: {0}")]
27    Internal(String),
28
29    #[error("Database error: {0}")]
30    Database(#[from] sqlx::Error),
31
32    #[error("JWT error: {0}")]
33    Jwt(#[from] jsonwebtoken::errors::Error),
34
35    #[error("JSON error: {0}")]
36    Json(#[from] serde_json::Error),
37}
38
39pub type AppResult<T> = Result<T, AppError>;
40
41#[derive(Serialize)]
42struct ErrorResponse {
43    error: String,
44    message: String,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    details: Option<serde_json::Value>,
47}
48
49impl AppError {
50    fn status_code(&self) -> StatusCode {
51        match self {
52            Self::NotFound(_) => StatusCode::NOT_FOUND,
53            Self::BadRequest(_) | Self::Validation(_) => StatusCode::BAD_REQUEST,
54            Self::Unauthorized(_) | Self::Jwt(_) => StatusCode::UNAUTHORIZED,
55            Self::Forbidden(_) => StatusCode::FORBIDDEN,
56            Self::Conflict(_) => StatusCode::CONFLICT,
57            Self::Internal(_) | Self::Database(_) | Self::Json(_) => {
58                StatusCode::INTERNAL_SERVER_ERROR
59            }
60        }
61    }
62
63    fn error_type(&self) -> &str {
64        match self {
65            Self::NotFound(_) => "not_found",
66            Self::BadRequest(_) => "bad_request",
67            Self::Unauthorized(_) | Self::Jwt(_) => "unauthorized",
68            Self::Forbidden(_) => "forbidden",
69            Self::Conflict(_) => "conflict",
70            Self::Validation(_) => "validation_error",
71            Self::Internal(_) | Self::Database(_) | Self::Json(_) => "internal_error",
72        }
73    }
74}
75
76impl IntoResponse for AppError {
77    fn into_response(self) -> Response {
78        let status = self.status_code();
79        let error_type = self.error_type().to_string();
80
81        // Log server errors
82        if status.is_server_error() {
83            tracing::error!(%status, error = %self, "Server error");
84        }
85
86        let body = ErrorResponse {
87            error: error_type,
88            message: self.to_string(),
89            details: None,
90        };
91
92        (status, axum::Json(body)).into_response()
93    }
94}