Skip to main content

opencode_sdk/
error.rs

1//! Error types for opencode_rs.
2
3use thiserror::Error;
4
5/// Result type alias for opencode_rs operations.
6pub type Result<T> = std::result::Result<T, OpencodeError>;
7
8/// Error type for opencode_rs operations.
9#[derive(Debug, Error)]
10pub enum OpencodeError {
11    /// HTTP error with structured response from OpenCode.
12    #[error("HTTP error {status}: {message}")]
13    Http {
14        /// HTTP status code.
15        status: u16,
16        /// Error name from OpenCode's NamedError (e.g., "NotFound", "ValidationError").
17        name: Option<String>,
18        /// Error message.
19        message: String,
20        /// Additional error data.
21        data: Option<serde_json::Value>,
22    },
23
24    /// Network/connection error.
25    #[error("Network error: {0}")]
26    Network(String),
27
28    /// SSE streaming error.
29    #[error("SSE error: {0}")]
30    Sse(String),
31
32    /// JSON serialization/deserialization error.
33    #[error("JSON error: {0}")]
34    Json(#[from] serde_json::Error),
35
36    /// URL parsing error.
37    #[error("URL error: {0}")]
38    Url(#[from] url::ParseError),
39
40    /// Failed to spawn server process.
41    #[error("Failed to spawn server: {message}")]
42    SpawnServer {
43        /// Error message.
44        message: String,
45    },
46
47    /// Server not ready within timeout.
48    #[error("Server not ready within {timeout_ms}ms")]
49    ServerTimeout {
50        /// Timeout in milliseconds.
51        timeout_ms: u64,
52    },
53
54    /// Process execution error.
55    #[error("Process error: {0}")]
56    Process(String),
57
58    /// Invalid configuration.
59    #[error("Invalid configuration: {0}")]
60    InvalidConfig(String),
61
62    /// IO error.
63    #[error("IO error: {0}")]
64    Io(#[from] std::io::Error),
65
66    /// Stream closed unexpectedly.
67    #[error("Stream closed unexpectedly")]
68    StreamClosed,
69
70    /// Session not found.
71    #[error("Session not found: {0}")]
72    SessionNotFound(String),
73
74    /// Internal state error.
75    #[error("Internal state error: {0}")]
76    State(String),
77}
78
79/// Helper to parse OpenCode's NamedError response body.
80#[derive(Debug, Clone)]
81pub struct HttpErrorBody {
82    /// Error name (e.g., "NotFound", "ValidationError").
83    pub name: Option<String>,
84    /// Error message.
85    pub message: Option<String>,
86    /// Additional error data.
87    pub data: Option<serde_json::Value>,
88}
89
90impl HttpErrorBody {
91    /// Parse from a JSON value.
92    pub fn from_json(v: serde_json::Value) -> Self {
93        Self {
94            name: v
95                .get("name")
96                .and_then(|x| x.as_str())
97                .map(|s| s.to_string()),
98            message: v
99                .get("message")
100                .and_then(|x| x.as_str())
101                .map(|s| s.to_string()),
102            data: v.get("data").cloned(),
103        }
104    }
105}
106
107impl OpencodeError {
108    /// Create an HTTP error from status and optional JSON body.
109    pub fn http(status: u16, body_text: &str) -> Self {
110        // Try to parse as JSON to extract NamedError fields
111        let parsed: Option<serde_json::Value> = serde_json::from_str(body_text).ok();
112        let info = parsed.map(HttpErrorBody::from_json);
113
114        Self::Http {
115            status,
116            name: info.as_ref().and_then(|i| i.name.clone()),
117            message: info
118                .as_ref()
119                .and_then(|i| i.message.clone())
120                .unwrap_or_else(|| format!("HTTP {}", status)),
121            data: info.and_then(|i| i.data),
122        }
123    }
124
125    /// Check if this is a "not found" error (404).
126    pub fn is_not_found(&self) -> bool {
127        matches!(self, Self::Http { status: 404, .. })
128    }
129
130    /// Check if this is a validation error (400).
131    pub fn is_validation_error(&self) -> bool {
132        matches!(self, Self::Http { status: 400, .. })
133    }
134
135    /// Check if this is a server error (5xx).
136    pub fn is_server_error(&self) -> bool {
137        matches!(self, Self::Http { status, .. } if *status >= 500)
138    }
139
140    /// Get the error name if this is an HTTP error.
141    pub fn error_name(&self) -> Option<&str> {
142        match self {
143            Self::Http { name, .. } => name.as_deref(),
144            _ => None,
145        }
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_http_error_from_named_error() {
155        let body = r#"{"name":"NotFound","message":"Session not found","data":{"id":"123"}}"#;
156        let err = OpencodeError::http(404, body);
157
158        match err {
159            OpencodeError::Http {
160                status,
161                name,
162                message,
163                data,
164            } => {
165                assert_eq!(status, 404);
166                assert_eq!(name, Some("NotFound".to_string()));
167                assert_eq!(message, "Session not found");
168                assert!(data.is_some());
169            }
170            _ => panic!("Expected Http error"),
171        }
172    }
173
174    #[test]
175    fn test_http_error_from_plain_text() {
176        let err = OpencodeError::http(500, "Internal Server Error");
177
178        match err {
179            OpencodeError::Http {
180                status,
181                name,
182                message,
183                ..
184            } => {
185                assert_eq!(status, 500);
186                assert!(name.is_none());
187                assert_eq!(message, "HTTP 500");
188            }
189            _ => panic!("Expected Http error"),
190        }
191    }
192
193    #[test]
194    fn test_is_not_found() {
195        let err = OpencodeError::http(404, "{}");
196        assert!(err.is_not_found());
197
198        let err = OpencodeError::http(200, "{}");
199        assert!(!err.is_not_found());
200    }
201
202    #[test]
203    fn test_is_validation_error() {
204        let err = OpencodeError::http(400, r#"{"name":"ValidationError"}"#);
205        assert!(err.is_validation_error());
206        assert_eq!(err.error_name(), Some("ValidationError"));
207    }
208
209    #[test]
210    fn test_is_server_error() {
211        let err = OpencodeError::http(500, "{}");
212        assert!(err.is_server_error());
213
214        let err = OpencodeError::http(503, "{}");
215        assert!(err.is_server_error());
216
217        let err = OpencodeError::http(400, "{}");
218        assert!(!err.is_server_error());
219    }
220}