Skip to main content

aiclient_api/util/
error.rs

1use axum::http::StatusCode;
2use axum::response::{IntoResponse, Response};
3use serde_json::json;
4
5use crate::providers::OutputFormat;
6
7#[derive(Debug, thiserror::Error)]
8pub enum AppError {
9    #[error("Provider error: {0}")]
10    Provider(#[from] anyhow::Error),
11
12    #[error("Authentication required: {0}")]
13    Unauthorized(String),
14
15    #[error("Provider unavailable: {0}")]
16    Unavailable(String),
17
18    #[error("Bad request: {0}")]
19    BadRequest(String),
20
21    #[error("Rate limited")]
22    RateLimited,
23
24    #[error("Upstream error: {status} {body}")]
25    Upstream { status: u16, body: String },
26}
27
28impl AppError {
29    /// Extract the HTTP status code and message from this error.
30    pub fn status_and_message(&self) -> (StatusCode, String) {
31        match self {
32            AppError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg.clone()),
33            AppError::Unavailable(msg) => (StatusCode::SERVICE_UNAVAILABLE, msg.clone()),
34            AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
35            AppError::RateLimited => (StatusCode::TOO_MANY_REQUESTS, "Rate limit exceeded".into()),
36            AppError::Upstream { status, body } => {
37                let code = StatusCode::from_u16(*status).unwrap_or(StatusCode::BAD_GATEWAY);
38                (code, body.clone())
39            }
40            AppError::Provider(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
41        }
42    }
43
44    /// Build an error response in OpenAI format.
45    pub fn openai_error(status: StatusCode, message: &str) -> Response {
46        let error_type = match status {
47            StatusCode::UNAUTHORIZED => "authentication_error",
48            StatusCode::BAD_REQUEST => "invalid_request_error",
49            StatusCode::TOO_MANY_REQUESTS => "rate_limit_error",
50            StatusCode::NOT_FOUND => "not_found_error",
51            _ => "server_error",
52        };
53        let body = json!({
54            "error": {
55                "message": message,
56                "type": error_type,
57                "code": serde_json::Value::Null,
58            }
59        });
60        (status, axum::Json(body)).into_response()
61    }
62
63    /// Build an error response in Anthropic format.
64    pub fn anthropic_error(status: StatusCode, message: &str) -> Response {
65        let error_type = match status {
66            StatusCode::UNAUTHORIZED => "authentication_error",
67            StatusCode::BAD_REQUEST => "invalid_request_error",
68            StatusCode::TOO_MANY_REQUESTS => "rate_limit_error",
69            StatusCode::NOT_FOUND => "not_found_error",
70            StatusCode::SERVICE_UNAVAILABLE => "api_error",
71            StatusCode::FORBIDDEN => "permission_error",
72            _ => "api_error",
73        };
74        let body = json!({
75            "type": "error",
76            "error": {
77                "type": error_type,
78                "message": message,
79            }
80        });
81        (status, axum::Json(body)).into_response()
82    }
83
84    /// Build an error response matching the given output format.
85    pub fn format_error(status: StatusCode, message: &str, format: OutputFormat) -> Response {
86        match format {
87            OutputFormat::OpenAI => Self::openai_error(status, message),
88            OutputFormat::Anthropic => Self::anthropic_error(status, message),
89        }
90    }
91}
92
93impl IntoResponse for AppError {
94    fn into_response(self) -> Response {
95        let (status, message) = match &self {
96            AppError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg.clone()),
97            AppError::Unavailable(msg) => (StatusCode::SERVICE_UNAVAILABLE, msg.clone()),
98            AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
99            AppError::RateLimited => (StatusCode::TOO_MANY_REQUESTS, "Rate limit exceeded".into()),
100            AppError::Upstream { status, body } => {
101                let code = StatusCode::from_u16(*status).unwrap_or(StatusCode::BAD_GATEWAY);
102                return (code, body.clone()).into_response();
103            }
104            AppError::Provider(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
105        };
106        // Default to OpenAI format when format is unknown (e.g. middleware errors)
107        Self::openai_error(status, &message)
108    }
109}