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