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}