Skip to main content

aonyx_api/
error.rs

1//! The API error type and its HTTP rendering.
2//!
3//! Every handler returns [`ApiResult`]; an [`ApiError`] is converted into a
4//! JSON body `{ "error": { "type": …, "message": … } }` with the matching
5//! HTTP status. [`aonyx_core::AonyxError`] maps in via [`From`] so handlers
6//! can use `?` over core calls.
7
8use aonyx_core::AonyxError;
9use axum::http::StatusCode;
10use axum::response::{IntoResponse, Response};
11use axum::Json;
12use serde_json::json;
13
14/// Convenience alias for fallible handler bodies.
15pub type ApiResult<T> = std::result::Result<T, ApiError>;
16
17/// An error surfaced by an API handler, carrying enough to pick an HTTP
18/// status and a stable machine-readable `type` tag.
19#[derive(Debug, thiserror::Error)]
20pub enum ApiError {
21    /// Missing or invalid bearer token (`401`).
22    #[error("{0}")]
23    Unauthorized(String),
24
25    /// The action is understood but not permitted — e.g. a Destructive tool
26    /// invoked while `allow_destructive` is off (`403`).
27    #[error("{0}")]
28    Forbidden(String),
29
30    /// The request was malformed or referenced something missing (`400`).
31    #[error("{0}")]
32    BadRequest(String),
33
34    /// A referenced resource does not exist (`404`).
35    #[error("{0}")]
36    NotFound(String),
37
38    /// Anything else — an internal failure (`500`).
39    #[error("{0}")]
40    Internal(String),
41}
42
43impl ApiError {
44    /// HTTP status + stable `type` tag for this error.
45    fn parts(&self) -> (StatusCode, &'static str) {
46        match self {
47            ApiError::Unauthorized(_) => (StatusCode::UNAUTHORIZED, "unauthorized"),
48            ApiError::Forbidden(_) => (StatusCode::FORBIDDEN, "forbidden"),
49            ApiError::BadRequest(_) => (StatusCode::BAD_REQUEST, "bad_request"),
50            ApiError::NotFound(_) => (StatusCode::NOT_FOUND, "not_found"),
51            ApiError::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "internal"),
52        }
53    }
54}
55
56impl IntoResponse for ApiError {
57    fn into_response(self) -> Response {
58        let (status, ty) = self.parts();
59        let body = Json(json!({ "error": { "type": ty, "message": self.to_string() } }));
60        (status, body).into_response()
61    }
62}
63
64impl From<AonyxError> for ApiError {
65    fn from(e: AonyxError) -> Self {
66        match e {
67            AonyxError::Config(m) => ApiError::BadRequest(m),
68            AonyxError::ApprovalRejected(m) => ApiError::Forbidden(m),
69            other => ApiError::Internal(other.to_string()),
70        }
71    }
72}