Skip to main content

claude_codes/io/
errors.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use std::fmt;
4
5/// Error type for parsing failures that preserves the raw input.
6///
7/// When deserialization fails, this error exposes:
8/// - `raw_line`: the exact string from Claude's stdout (even if it's not valid JSON)
9/// - `raw_json`: the parsed `serde_json::Value` when JSON was valid but didn't match our types, `None` when the input wasn't valid JSON
10/// - `error_message`: the underlying serde error description
11#[derive(Debug, Clone)]
12pub struct ParseError {
13    /// The exact line from Claude's stdout that failed to parse
14    pub raw_line: String,
15    /// The parsed JSON value if the line was valid JSON, `None` if not
16    pub raw_json: Option<Value>,
17    /// The underlying serde error message
18    pub error_message: String,
19}
20
21impl fmt::Display for ParseError {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        write!(
24            f,
25            "Failed to parse ClaudeOutput: {} (raw: {})",
26            self.error_message, self.raw_line
27        )
28    }
29}
30
31impl std::error::Error for ParseError {}
32
33/// Known Anthropic API error types.
34///
35/// Maps to the `type` field inside an error response from the Anthropic API.
36#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37pub enum ApiErrorType {
38    /// Internal server error (HTTP 500)
39    ApiError,
40    /// Service overloaded (HTTP 529)
41    OverloadedError,
42    /// Bad request (HTTP 400)
43    InvalidRequestError,
44    /// Invalid API key (HTTP 401)
45    AuthenticationError,
46    /// Too many requests (HTTP 429)
47    RateLimitError,
48    /// An error type not yet known to this version of the crate.
49    Unknown(String),
50}
51
52impl ApiErrorType {
53    pub fn as_str(&self) -> &str {
54        match self {
55            Self::ApiError => "api_error",
56            Self::OverloadedError => "overloaded_error",
57            Self::InvalidRequestError => "invalid_request_error",
58            Self::AuthenticationError => "authentication_error",
59            Self::RateLimitError => "rate_limit_error",
60            Self::Unknown(s) => s.as_str(),
61        }
62    }
63}
64
65impl fmt::Display for ApiErrorType {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        f.write_str(self.as_str())
68    }
69}
70
71impl From<&str> for ApiErrorType {
72    fn from(s: &str) -> Self {
73        match s {
74            "api_error" => Self::ApiError,
75            "overloaded_error" => Self::OverloadedError,
76            "invalid_request_error" => Self::InvalidRequestError,
77            "authentication_error" => Self::AuthenticationError,
78            "rate_limit_error" => Self::RateLimitError,
79            other => Self::Unknown(other.to_string()),
80        }
81    }
82}
83
84impl Serialize for ApiErrorType {
85    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
86        serializer.serialize_str(self.as_str())
87    }
88}
89
90impl<'de> Deserialize<'de> for ApiErrorType {
91    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
92        let s = String::deserialize(deserializer)?;
93        Ok(Self::from(s.as_str()))
94    }
95}
96
97/// API error message from Anthropic.
98///
99/// When Claude Code encounters an API error (e.g., 500, 529 overloaded), it outputs
100/// a JSON message with `type: "error"`. This struct captures that error information.
101///
102/// # Example JSON
103///
104/// ```json
105/// {
106///   "type": "error",
107///   "error": {
108///     "type": "api_error",
109///     "message": "Internal server error"
110///   },
111///   "request_id": "req_011CXPC6BqUogB959LWEf52X"
112/// }
113/// ```
114///
115/// # Example
116///
117/// ```
118/// use claude_codes::ClaudeOutput;
119///
120/// let json = r#"{"type":"error","error":{"type":"api_error","message":"Internal server error"},"request_id":"req_123"}"#;
121/// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
122///
123/// if let ClaudeOutput::Error(err) = output {
124///     println!("Error type: {}", err.error.error_type);
125///     println!("Message: {}", err.error.message);
126/// }
127/// ```
128#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
129pub struct AnthropicError {
130    /// The nested error details
131    pub error: AnthropicErrorDetails,
132    /// The request ID for debugging/support
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub request_id: Option<String>,
135}
136
137impl AnthropicError {
138    /// Check if this is an overloaded error (HTTP 529)
139    pub fn is_overloaded(&self) -> bool {
140        self.error.error_type == ApiErrorType::OverloadedError
141    }
142
143    /// Check if this is a server error (HTTP 500)
144    pub fn is_server_error(&self) -> bool {
145        self.error.error_type == ApiErrorType::ApiError
146    }
147
148    /// Check if this is an invalid request error (HTTP 400)
149    pub fn is_invalid_request(&self) -> bool {
150        self.error.error_type == ApiErrorType::InvalidRequestError
151    }
152
153    /// Check if this is an authentication error (HTTP 401)
154    pub fn is_authentication_error(&self) -> bool {
155        self.error.error_type == ApiErrorType::AuthenticationError
156    }
157
158    /// Check if this is a rate limit error (HTTP 429)
159    pub fn is_rate_limited(&self) -> bool {
160        self.error.error_type == ApiErrorType::RateLimitError
161    }
162}
163
164/// Details of an Anthropic API error.
165#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
166pub struct AnthropicErrorDetails {
167    /// The type of error
168    #[serde(rename = "type")]
169    pub error_type: ApiErrorType,
170    /// Human-readable error message
171    pub message: String,
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use crate::io::ClaudeOutput;
178
179    #[test]
180    fn test_deserialize_anthropic_error() {
181        let json = r#"{
182            "type": "error",
183            "error": {
184                "type": "api_error",
185                "message": "Internal server error"
186            },
187            "request_id": "req_011CXPC6BqUogB959LWEf52X"
188        }"#;
189
190        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
191        assert!(output.is_api_error());
192        assert_eq!(output.message_type(), "error");
193
194        if let ClaudeOutput::Error(err) = output {
195            assert_eq!(err.error.error_type, ApiErrorType::ApiError);
196            assert_eq!(err.error.message, "Internal server error");
197            assert_eq!(
198                err.request_id,
199                Some("req_011CXPC6BqUogB959LWEf52X".to_string())
200            );
201            assert!(err.is_server_error());
202            assert!(!err.is_overloaded());
203        } else {
204            panic!("Expected Error variant");
205        }
206    }
207
208    #[test]
209    fn test_deserialize_anthropic_overloaded_error() {
210        let json = r#"{
211            "type": "error",
212            "error": {
213                "type": "overloaded_error",
214                "message": "Overloaded"
215            }
216        }"#;
217
218        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
219
220        if let ClaudeOutput::Error(err) = output {
221            assert!(err.is_overloaded());
222            assert!(!err.is_server_error());
223            assert!(err.request_id.is_none());
224        } else {
225            panic!("Expected Error variant");
226        }
227    }
228
229    #[test]
230    fn test_deserialize_anthropic_rate_limit_error() {
231        let json = r#"{
232            "type": "error",
233            "error": {
234                "type": "rate_limit_error",
235                "message": "Rate limit exceeded"
236            },
237            "request_id": "req_456"
238        }"#;
239
240        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
241
242        if let ClaudeOutput::Error(err) = output {
243            assert!(err.is_rate_limited());
244            assert!(!err.is_overloaded());
245            assert!(!err.is_server_error());
246        } else {
247            panic!("Expected Error variant");
248        }
249    }
250
251    #[test]
252    fn test_deserialize_anthropic_authentication_error() {
253        let json = r#"{
254            "type": "error",
255            "error": {
256                "type": "authentication_error",
257                "message": "Invalid API key"
258            }
259        }"#;
260
261        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
262
263        if let ClaudeOutput::Error(err) = output {
264            assert!(err.is_authentication_error());
265        } else {
266            panic!("Expected Error variant");
267        }
268    }
269
270    #[test]
271    fn test_deserialize_anthropic_invalid_request_error() {
272        let json = r#"{
273            "type": "error",
274            "error": {
275                "type": "invalid_request_error",
276                "message": "Invalid request body"
277            }
278        }"#;
279
280        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
281
282        if let ClaudeOutput::Error(err) = output {
283            assert!(err.is_invalid_request());
284        } else {
285            panic!("Expected Error variant");
286        }
287    }
288
289    #[test]
290    fn test_anthropic_error_as_helper() {
291        let json = r#"{"type":"error","error":{"type":"api_error","message":"Error"}}"#;
292        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
293
294        let err = output.as_anthropic_error();
295        assert!(err.is_some());
296        assert_eq!(err.unwrap().error.error_type, ApiErrorType::ApiError);
297
298        // Non-error should return None
299        let result_json = r#"{
300            "type": "result",
301            "subtype": "success",
302            "is_error": false,
303            "duration_ms": 100,
304            "duration_api_ms": 200,
305            "num_turns": 1,
306            "session_id": "abc",
307            "total_cost_usd": 0.01
308        }"#;
309        let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
310        assert!(result.as_anthropic_error().is_none());
311    }
312
313    #[test]
314    fn test_anthropic_error_roundtrip() {
315        let error = AnthropicError {
316            error: AnthropicErrorDetails {
317                error_type: ApiErrorType::ApiError,
318                message: "Test error".to_string(),
319            },
320            request_id: Some("req_123".to_string()),
321        };
322
323        let json = serde_json::to_string(&error).unwrap();
324        assert!(json.contains("\"type\":\"api_error\""));
325        assert!(json.contains("\"message\":\"Test error\""));
326        assert!(json.contains("\"request_id\":\"req_123\""));
327
328        let parsed: AnthropicError = serde_json::from_str(&json).unwrap();
329        assert_eq!(parsed, error);
330    }
331
332    #[test]
333    fn test_anthropic_error_session_id_is_none() {
334        let json = r#"{"type":"error","error":{"type":"api_error","message":"Error"}}"#;
335        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
336        assert!(output.session_id().is_none());
337    }
338}