Skip to main content

awa_ui/
error.rs

1use axum::http::StatusCode;
2use axum::response::{IntoResponse, Response};
3use serde_json::json;
4
5/// API error wrapper around AwaError, mapping to HTTP status codes.
6#[derive(Debug)]
7pub enum ApiError {
8    Awa(awa_model::AwaError),
9    ReadOnly,
10    NotFound(String),
11    Unauthorized(String),
12}
13
14impl From<awa_model::AwaError> for ApiError {
15    fn from(err: awa_model::AwaError) -> Self {
16        ApiError::Awa(err)
17    }
18}
19
20impl From<sqlx::Error> for ApiError {
21    fn from(err: sqlx::Error) -> Self {
22        ApiError::Awa(err.into())
23    }
24}
25
26impl From<serde_json::Error> for ApiError {
27    fn from(err: serde_json::Error) -> Self {
28        ApiError::Awa(awa_model::AwaError::Validation(err.to_string()))
29    }
30}
31
32impl From<std::sync::Arc<crate::cache::CacheError>> for ApiError {
33    fn from(err: std::sync::Arc<crate::cache::CacheError>) -> Self {
34        // Moka wraps try_get_with errors in Arc. Extract the inner AwaError
35        // if we're the only holder, otherwise create a database-level error
36        // from the description.
37        let cache_err = std::sync::Arc::try_unwrap(err).unwrap_or_else(|arc| (*arc).clone());
38        ApiError::Awa(
39            std::sync::Arc::try_unwrap(cache_err.0).unwrap_or_else(|arc| {
40                awa_model::AwaError::Database(sqlx::Error::Protocol(arc.to_string()))
41            }),
42        )
43    }
44}
45
46impl IntoResponse for ApiError {
47    fn into_response(self) -> Response {
48        let (status, message) = match self {
49            ApiError::ReadOnly => (
50                StatusCode::SERVICE_UNAVAILABLE,
51                "awa serve is connected to a read-only database; admin actions are disabled"
52                    .to_string(),
53            ),
54            ApiError::NotFound(message) => (StatusCode::NOT_FOUND, message),
55            ApiError::Unauthorized(message) => (StatusCode::UNAUTHORIZED, message),
56            ApiError::Awa(err) => match &err {
57                awa_model::AwaError::JobNotFound { .. } => (StatusCode::NOT_FOUND, err.to_string()),
58                awa_model::AwaError::CallbackNotFound { .. } => {
59                    (StatusCode::NOT_FOUND, err.to_string())
60                }
61                awa_model::AwaError::Validation(_) => (StatusCode::BAD_REQUEST, err.to_string()),
62                awa_model::AwaError::UniqueConflict { .. } => {
63                    (StatusCode::CONFLICT, err.to_string())
64                }
65                awa_model::AwaError::Database(_) | awa_model::AwaError::Serialization(_) => {
66                    tracing::error!(error = %err, "internal error");
67                    (
68                        StatusCode::INTERNAL_SERVER_ERROR,
69                        "internal server error".to_string(),
70                    )
71                }
72                _ => {
73                    tracing::error!(error = %err, "internal error");
74                    (
75                        StatusCode::INTERNAL_SERVER_ERROR,
76                        "internal server error".to_string(),
77                    )
78                }
79            },
80        };
81
82        let body = axum::Json(json!({ "error": message }));
83        (status, body).into_response()
84    }
85}
86
87impl ApiError {
88    pub fn read_only() -> Self {
89        ApiError::ReadOnly
90    }
91
92    pub fn unauthorized(message: impl Into<String>) -> Self {
93        ApiError::Unauthorized(message.into())
94    }
95
96    pub fn not_found(message: impl Into<String>) -> Self {
97        ApiError::NotFound(message.into())
98    }
99}