use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::Json; use serde_json::json; /// API-surface errors with their HTTP mapping. Internal causes are logged /// server-side, never echoed to clients. #[derive(Debug, thiserror::Error)] pub enum ApiError { #[error("bad request")] BadRequest, #[error("unauthorized")] Unauthorized, #[error("forbidden")] Forbidden, #[error("not found")] NotFound, #[error("conflict")] Conflict, #[error("{0}")] Quota(String), /// A dependency is temporarily refusing work and will accept it later — /// today, the Claude Code subscription's rate limit. Distinct from /// `Internal` because the operator's next action is different: wait and /// press the button again, rather than read a server log. A 500 with /// "internal error" sent them looking for a bug that was not there. #[error("{0}")] Unavailable(String), #[error("internal error")] Internal, } impl From for ApiError { fn from(err: cm_db::DbError) -> Self { match err { cm_db::DbError::NotFound => ApiError::NotFound, _ => ApiError::Internal, } } } impl From for ApiError { fn from(err: sqlx::Error) -> Self { ApiError::from(cm_db::DbError::from(err)) } } impl From for ApiError { fn from(err: cm_auth::AuthError) -> Self { match err { cm_auth::AuthError::InvalidCredentials | cm_auth::AuthError::Unauthenticated => { ApiError::Unauthorized } _ => ApiError::Internal, } } } impl IntoResponse for ApiError { fn into_response(self) -> Response { let status = match self { ApiError::BadRequest => StatusCode::BAD_REQUEST, ApiError::Unauthorized => StatusCode::UNAUTHORIZED, ApiError::Forbidden => StatusCode::FORBIDDEN, ApiError::NotFound => StatusCode::NOT_FOUND, ApiError::Conflict => StatusCode::CONFLICT, ApiError::Quota(_) => StatusCode::PAYMENT_REQUIRED, ApiError::Unavailable(_) => StatusCode::SERVICE_UNAVAILABLE, ApiError::Internal => StatusCode::INTERNAL_SERVER_ERROR, }; (status, Json(json!({ "error": self.to_string() }))).into_response() } }