1use axum::Json;
5use axum::http::StatusCode;
6use axum::response::{IntoResponse, Response};
7use prospero_core::CoreError;
8use serde::Serialize;
9
10pub enum ApiError {
12 Core(CoreError),
14 MethodNotAllowed(String),
17 Unauthorized,
19 Forbidden(String),
21 NotFound(String),
23 BadRequest(String),
26}
27
28impl ApiError {
29 #[must_use]
33 pub fn unsupported_on_backend() -> Self {
34 ApiError::MethodNotAllowed(
35 "not supported by the active fleet backend (k8s workspaces are \
36 CalibanTask/namespace-driven, not a prospero registry)"
37 .to_string(),
38 )
39 }
40}
41
42#[derive(Serialize)]
43struct ErrorBody {
44 error: String,
45 kind: &'static str,
46}
47
48impl From<CoreError> for ApiError {
49 fn from(e: CoreError) -> Self {
50 ApiError::Core(e)
51 }
52}
53
54impl IntoResponse for ApiError {
55 fn into_response(self) -> Response {
56 let www_authenticate = matches!(self, ApiError::Unauthorized);
57 let (status, kind, error) = match self {
58 ApiError::MethodNotAllowed(msg) => {
59 (StatusCode::METHOD_NOT_ALLOWED, "method_not_allowed", msg)
60 }
61 ApiError::Unauthorized => (
62 StatusCode::UNAUTHORIZED,
63 "unauthorized",
64 "unauthorized".to_string(),
65 ),
66 ApiError::Forbidden(msg) => (StatusCode::FORBIDDEN, "forbidden", msg),
67 ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, "not_found", msg),
68 ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, "bad_request", msg),
69 ApiError::Core(e) => {
70 let (status, kind) = match &e {
71 CoreError::AgentNotFound(_)
72 | CoreError::WorkspaceNotFound(_)
73 | CoreError::AutomationNotFound(_) => (StatusCode::NOT_FOUND, "not_found"),
74 CoreError::Unauthorized(_) => (StatusCode::UNAUTHORIZED, "unauthorized"),
75 CoreError::InvalidState { .. } => (StatusCode::CONFLICT, "invalid_state"),
76 CoreError::Conflict(_) => (StatusCode::CONFLICT, "conflict"),
77 CoreError::ProviderMisconfigured(_) => {
78 (StatusCode::BAD_REQUEST, "provider_misconfigured")
79 }
80 CoreError::InvalidConfig(_) => (StatusCode::BAD_REQUEST, "invalid_config"),
81 CoreError::CalibandUnreachable { .. }
82 | CoreError::Discovery(_)
83 | CoreError::Fleet(_) => (StatusCode::SERVICE_UNAVAILABLE, "unreachable"),
84 CoreError::Protocol(_) => (StatusCode::BAD_GATEWAY, "protocol"),
85 CoreError::Store(_)
86 | CoreError::SeqConflict
87 | CoreError::Io(_)
88 | CoreError::Json(_) => (StatusCode::INTERNAL_SERVER_ERROR, "internal"),
89 };
90 (status, kind, e.to_string())
91 }
92 };
93 let body = ErrorBody { error, kind };
94 let mut response = (status, Json(body)).into_response();
95 if www_authenticate {
96 response.headers_mut().insert(
97 axum::http::header::WWW_AUTHENTICATE,
98 axum::http::HeaderValue::from_static("Bearer realm=\"prospero\""),
99 );
100 }
101 response
102 }
103}