Skip to main content

opencode_sdk/types/
error.rs

1//! API error types for opencode_rs.
2//!
3//! Contains typed error structures matching TypeScript MessageV2.APIError.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// API error from the OpenCode server.
9///
10/// Matches the TypeScript `MessageV2.APIError` schema.
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12#[serde(rename_all = "camelCase")]
13pub struct APIError {
14    /// Error message.
15    pub message: String,
16    /// HTTP status code if applicable.
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub status_code: Option<u16>,
19    /// Whether this error is retryable.
20    pub is_retryable: bool,
21    /// Response headers from the failed request.
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub response_headers: Option<HashMap<String, String>>,
24    /// Response body from the failed request.
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub response_body: Option<String>,
27    /// Additional error metadata.
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub metadata: Option<HashMap<String, String>>,
30}
31
32impl std::fmt::Display for APIError {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        write!(f, "{}", self.message)?;
35        if let Some(code) = self.status_code {
36            write!(f, " (status: {})", code)?;
37        }
38        Ok(())
39    }
40}
41
42impl std::error::Error for APIError {}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    #[test]
49    fn test_api_error_minimal() {
50        let json = r#"{"message":"Something went wrong","isRetryable":false}"#;
51        let error: APIError = serde_json::from_str(json).unwrap();
52        assert_eq!(error.message, "Something went wrong");
53        assert!(!error.is_retryable);
54        assert!(error.status_code.is_none());
55    }
56
57    #[test]
58    fn test_api_error_full() {
59        let json = r#"{
60            "message": "Rate limited",
61            "statusCode": 429,
62            "isRetryable": true,
63            "responseHeaders": {"retry-after": "60"},
64            "responseBody": "Too many requests",
65            "metadata": {"region": "us-east-1"}
66        }"#;
67        let error: APIError = serde_json::from_str(json).unwrap();
68        assert_eq!(error.message, "Rate limited");
69        assert_eq!(error.status_code, Some(429));
70        assert!(error.is_retryable);
71        assert_eq!(
72            error.response_headers.as_ref().unwrap().get("retry-after"),
73            Some(&"60".to_string())
74        );
75        assert_eq!(error.response_body, Some("Too many requests".to_string()));
76        assert_eq!(
77            error.metadata.as_ref().unwrap().get("region"),
78            Some(&"us-east-1".to_string())
79        );
80    }
81
82    #[test]
83    fn test_api_error_display() {
84        let error = APIError {
85            message: "Not found".to_string(),
86            status_code: Some(404),
87            is_retryable: false,
88            response_headers: None,
89            response_body: None,
90            metadata: None,
91        };
92        assert_eq!(error.to_string(), "Not found (status: 404)");
93    }
94
95    #[test]
96    fn test_api_error_roundtrip() {
97        let error = APIError {
98            message: "Test error".to_string(),
99            status_code: Some(500),
100            is_retryable: true,
101            response_headers: Some(HashMap::from([(
102                "x-request-id".to_string(),
103                "123".to_string(),
104            )])),
105            response_body: Some("Internal error".to_string()),
106            metadata: None,
107        };
108        let json = serde_json::to_string(&error).unwrap();
109        let parsed: APIError = serde_json::from_str(&json).unwrap();
110        assert_eq!(error, parsed);
111    }
112}