Skip to main content

st/proxy/claude/
error.rs

1//! Claude API Error Types
2//!
3//! Typed errors matching every HTTP status code the Claude API returns.
4//! Each variant carries the error message from the API response body.
5//!
6//! Retryable errors: RateLimited (429), InternalError (500), Overloaded (529)
7//! Non-retryable: everything else (fix the request before retrying)
8
9use serde::Deserialize;
10use std::fmt;
11
12// ---------------------------------------------------------------------------
13// Typed API error enum
14// ---------------------------------------------------------------------------
15
16/// Every error the Claude Messages API can return, plus network/parse errors.
17///
18/// # Example
19/// ```rust,no_run
20/// match err {
21///     ClaudeApiError::RateLimited { message } => { /* back off and retry */ }
22///     ClaudeApiError::InvalidRequest { message } => { /* fix request */ }
23///     _ => { /* log and bail */ }
24/// }
25/// ```
26#[derive(Debug, thiserror::Error)]
27pub enum ClaudeApiError {
28    /// 400 - Malformed JSON, missing params, invalid values
29    #[error("Invalid request (400): {message}")]
30    InvalidRequest { message: String },
31
32    /// 401 - Missing or invalid API key
33    #[error("Authentication failed (401): {message}")]
34    AuthenticationError { message: String },
35
36    /// 403 - API key lacks permission for the requested model/feature
37    #[error("Permission denied (403): {message}")]
38    PermissionDenied { message: String },
39
40    /// 404 - Bad endpoint or invalid model ID
41    #[error("Not found (404): {message}")]
42    NotFound { message: String },
43
44    /// 413 - Request body too large (too many tokens, huge images, etc.)
45    #[error("Request too large (413): {message}")]
46    RequestTooLarge { message: String },
47
48    /// 429 - Rate limited (retryable - check `retry-after` header)
49    #[error("Rate limited (429): {message}")]
50    RateLimited { message: String },
51
52    /// 500 - Anthropic server issue (retryable)
53    #[error("Internal server error (500): {message}")]
54    InternalError { message: String },
55
56    /// 529 - API overloaded (retryable)
57    #[error("API overloaded (529): {message}")]
58    Overloaded { message: String },
59
60    /// Network-level failure (DNS, TLS, connection refused, etc.)
61    #[error("Network error: {0}")]
62    Network(#[from] reqwest::Error),
63
64    /// Failed to parse the API response JSON
65    #[error("JSON parse error: {0}")]
66    JsonParse(#[from] serde_json::Error),
67
68    /// SSE stream delivered an error event or broke mid-stream
69    #[error("Stream error: {message}")]
70    StreamError { message: String },
71
72    /// Catch-all for unexpected HTTP status codes
73    #[error("Unexpected API error ({status}): {message}")]
74    Unknown { status: u16, message: String },
75}
76
77impl ClaudeApiError {
78    /// Returns true if the error is safe to retry with backoff.
79    pub fn is_retryable(&self) -> bool {
80        matches!(
81            self,
82            ClaudeApiError::RateLimited { .. }
83                | ClaudeApiError::InternalError { .. }
84                | ClaudeApiError::Overloaded { .. }
85        )
86    }
87}
88
89// ---------------------------------------------------------------------------
90// Raw API error response (for deserializing the JSON body)
91// ---------------------------------------------------------------------------
92
93/// The top-level error envelope from the Claude API:
94/// `{ "type": "error", "error": { "type": "...", "message": "..." } }`
95#[derive(Debug, Deserialize)]
96pub struct ApiErrorResponse {
97    #[serde(rename = "type")]
98    pub response_type: String,
99    pub error: ApiErrorBody,
100}
101
102/// The inner error object with machine-readable type and human-readable message.
103#[derive(Debug, Clone, Deserialize)]
104pub struct ApiErrorBody {
105    /// Machine-readable error type, e.g. "invalid_request_error"
106    #[serde(rename = "type")]
107    pub error_type: String,
108    /// Human-readable description of what went wrong
109    pub message: String,
110}
111
112impl fmt::Display for ApiErrorBody {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        write!(f, "{}: {}", self.error_type, self.message)
115    }
116}
117
118// ---------------------------------------------------------------------------
119// Parsing: HTTP status + body -> ClaudeApiError
120// ---------------------------------------------------------------------------
121
122impl ClaudeApiError {
123    /// Parse an API error from the HTTP status code and response body text.
124    ///
125    /// Tries to deserialize the structured error JSON first; falls back to
126    /// using the raw body text if parsing fails.
127    pub fn from_response(status: u16, body: &str) -> Self {
128        // Try to extract the structured error message
129        let message = serde_json::from_str::<ApiErrorResponse>(body)
130            .map(|r| r.error.message)
131            .unwrap_or_else(|_| body.to_string());
132
133        match status {
134            400 => ClaudeApiError::InvalidRequest { message },
135            401 => ClaudeApiError::AuthenticationError { message },
136            403 => ClaudeApiError::PermissionDenied { message },
137            404 => ClaudeApiError::NotFound { message },
138            413 => ClaudeApiError::RequestTooLarge { message },
139            429 => ClaudeApiError::RateLimited { message },
140            500 => ClaudeApiError::InternalError { message },
141            529 => ClaudeApiError::Overloaded { message },
142            _ => ClaudeApiError::Unknown { status, message },
143        }
144    }
145}