Skip to main content

heldar_kernel/
error.rs

1use axum::http::StatusCode;
2use axum::response::{IntoResponse, Response};
3use axum::Json;
4use serde_json::json;
5
6/// Application error type, convertible into an HTTP response.
7#[derive(Debug, thiserror::Error)]
8pub enum AppError {
9    #[error("{0}")]
10    NotFound(String),
11    #[error("{0}")]
12    BadRequest(String),
13    #[error("{0}")]
14    Conflict(String),
15    #[error("{0}")]
16    Unauthorized(String),
17    #[error("{0}")]
18    Forbidden(String),
19    #[error(transparent)]
20    Db(#[from] sqlx::Error),
21    #[error(transparent)]
22    Other(#[from] anyhow::Error),
23}
24
25pub type AppResult<T> = Result<T, AppError>;
26
27impl IntoResponse for AppError {
28    fn into_response(self) -> Response {
29        let (status, msg) = match self {
30            AppError::NotFound(m) => (StatusCode::NOT_FOUND, m),
31            AppError::BadRequest(m) => (StatusCode::BAD_REQUEST, m),
32            AppError::Conflict(m) => (StatusCode::CONFLICT, m),
33            AppError::Unauthorized(m) => (StatusCode::UNAUTHORIZED, m),
34            AppError::Forbidden(m) => (StatusCode::FORBIDDEN, m),
35            AppError::Db(sqlx::Error::RowNotFound) => {
36                (StatusCode::NOT_FOUND, "resource not found".to_string())
37            }
38            // Map common constraint violations to 4xx instead of 500 (e.g. duplicate id,
39            // or a site_id/foreign key that does not exist).
40            AppError::Db(sqlx::Error::Database(ref dbe)) => {
41                use sqlx::error::ErrorKind;
42                match dbe.kind() {
43                    ErrorKind::UniqueViolation => {
44                        (StatusCode::CONFLICT, "resource already exists".to_string())
45                    }
46                    ErrorKind::ForeignKeyViolation => (
47                        StatusCode::BAD_REQUEST,
48                        "referenced resource does not exist (check site_id)".to_string(),
49                    ),
50                    _ => {
51                        tracing::error!(error = %dbe, "database error");
52                        (
53                            StatusCode::INTERNAL_SERVER_ERROR,
54                            "database error".to_string(),
55                        )
56                    }
57                }
58            }
59            AppError::Db(e) => {
60                tracing::error!(error = %e, "database error");
61                (
62                    StatusCode::INTERNAL_SERVER_ERROR,
63                    "database error".to_string(),
64                )
65            }
66            AppError::Other(e) => {
67                tracing::error!(error = ?e, "internal error");
68                (
69                    StatusCode::INTERNAL_SERVER_ERROR,
70                    "internal error".to_string(),
71                )
72            }
73        };
74        (status, Json(json!({ "error": msg }))).into_response()
75    }
76}