Skip to main content

prospero_api/
error.rs

1//! Mapping `CoreError` to HTTP responses. Typed errors become precise status
2//! codes (404/409/503) — never an opaque 500 for an expected condition.
3
4use axum::Json;
5use axum::http::StatusCode;
6use axum::response::{IntoResponse, Response};
7use prospero_core::CoreError;
8use serde::Serialize;
9
10/// An API error that renders as `{ "error": "...", "kind": "..." }`.
11pub enum ApiError {
12    /// A typed core error mapped to a precise status code.
13    Core(CoreError),
14    /// The operation exists but the active fleet backend does not support it
15    /// (e.g. workspace-registry ops under k8s) → 405. (#76)
16    MethodNotAllowed(String),
17}
18
19impl ApiError {
20    /// The requested operation is real but not served by the active backend —
21    /// e.g. the workspace registry/config plane under k8s, where workspaces are
22    /// `CalibanTask`/namespace-driven rather than a prospero registry. (#76)
23    #[must_use]
24    pub fn unsupported_on_backend() -> Self {
25        ApiError::MethodNotAllowed(
26            "not supported by the active fleet backend (k8s workspaces are \
27             CalibanTask/namespace-driven, not a prospero registry)"
28                .to_string(),
29        )
30    }
31}
32
33#[derive(Serialize)]
34struct ErrorBody {
35    error: String,
36    kind: &'static str,
37}
38
39impl From<CoreError> for ApiError {
40    fn from(e: CoreError) -> Self {
41        ApiError::Core(e)
42    }
43}
44
45impl IntoResponse for ApiError {
46    fn into_response(self) -> Response {
47        let (status, kind, error) = match self {
48            ApiError::MethodNotAllowed(msg) => {
49                (StatusCode::METHOD_NOT_ALLOWED, "method_not_allowed", msg)
50            }
51            ApiError::Core(e) => {
52                let (status, kind) = match &e {
53                    CoreError::AgentNotFound(_) | CoreError::WorkspaceNotFound(_) => {
54                        (StatusCode::NOT_FOUND, "not_found")
55                    }
56                    CoreError::InvalidState { .. } => (StatusCode::CONFLICT, "invalid_state"),
57                    CoreError::Conflict(_) => (StatusCode::CONFLICT, "conflict"),
58                    CoreError::ProviderMisconfigured(_) => {
59                        (StatusCode::BAD_REQUEST, "provider_misconfigured")
60                    }
61                    CoreError::InvalidConfig(_) => (StatusCode::BAD_REQUEST, "invalid_config"),
62                    CoreError::CalibandUnreachable { .. }
63                    | CoreError::Discovery(_)
64                    | CoreError::Fleet(_) => (StatusCode::SERVICE_UNAVAILABLE, "unreachable"),
65                    CoreError::Protocol(_) => (StatusCode::BAD_GATEWAY, "protocol"),
66                    CoreError::Store(_)
67                    | CoreError::SeqConflict
68                    | CoreError::Io(_)
69                    | CoreError::Json(_) => (StatusCode::INTERNAL_SERVER_ERROR, "internal"),
70                };
71                (status, kind, e.to_string())
72            }
73        };
74        let body = ErrorBody { error, kind };
75        (status, Json(body)).into_response()
76    }
77}