openai_ergonomic/
errors.rs

1//! Error types for the `OpenAI` ergonomic wrapper.
2//!
3//! This module provides comprehensive error handling with detailed error
4//! information and proper error chaining.
5
6use thiserror::Error;
7
8/// Result type used throughout the crate.
9pub type Result<T> = std::result::Result<T, Error>;
10
11/// Main error type for the `OpenAI` ergonomic wrapper.
12#[derive(Error, Debug)]
13pub enum Error {
14    /// Invalid request parameters or configuration.
15    #[error("Invalid request: {0}")]
16    InvalidRequest(String),
17
18    /// Authentication errors.
19    #[error("Authentication failed: {0}")]
20    Authentication(String),
21
22    /// Rate limiting errors.
23    #[error("Rate limit exceeded: {0}")]
24    RateLimit(String),
25
26    /// HTTP client errors.
27    #[error("HTTP error: {0}")]
28    Http(#[from] reqwest::Error),
29
30    /// HTTP middleware errors.
31    #[error("HTTP middleware error: {0}")]
32    HttpMiddleware(#[from] reqwest_middleware::Error),
33
34    /// JSON serialization/deserialization errors.
35    #[error("JSON error: {0}")]
36    Json(#[from] serde_json::Error),
37
38    /// `OpenAI` API errors with status code and message.
39    #[error("`OpenAI` API error (status {status}): {message}")]
40    Api {
41        /// HTTP status code returned by the API
42        status: u16,
43        /// Error message from the API
44        message: String,
45        /// Type of error (if provided by API)
46        error_type: Option<String>,
47        /// Error code (if provided by API)
48        error_code: Option<String>,
49    },
50
51    /// Streaming connection errors.
52    #[error("Stream connection error: {message}")]
53    StreamConnection {
54        /// Error message describing the connection issue
55        message: String,
56    },
57
58    /// Streaming data parsing errors.
59    #[error("Stream parsing error: {message}, chunk: {chunk}")]
60    StreamParsing {
61        /// Error message describing the parsing issue
62        message: String,
63        /// The problematic chunk data
64        chunk: String,
65    },
66
67    /// Streaming buffer management errors.
68    #[error("Stream buffer error: {message}")]
69    StreamBuffer {
70        /// Error message describing the buffer issue
71        message: String,
72    },
73
74    /// Generic streaming errors.
75    #[error("Stream error: {0}")]
76    Stream(String),
77
78    /// File operation errors.
79    #[error("File error: {0}")]
80    File(#[from] std::io::Error),
81
82    /// Configuration errors.
83    #[error("Configuration error: {0}")]
84    Config(String),
85
86    /// Builder validation errors.
87    #[error("Builder validation error: {0}")]
88    Builder(String),
89
90    /// Generic internal errors.
91    #[error("Internal error: {0}")]
92    Internal(String),
93}
94
95impl Error {
96    /// Create a new API error with status and message.
97    pub fn api(status: u16, message: impl Into<String>) -> Self {
98        Self::Api {
99            status,
100            message: message.into(),
101            error_type: None,
102            error_code: None,
103        }
104    }
105
106    /// Create a new API error with full details.
107    pub fn api_detailed(
108        status: u16,
109        message: impl Into<String>,
110        error_type: Option<String>,
111        error_code: Option<String>,
112    ) -> Self {
113        Self::Api {
114            status,
115            message: message.into(),
116            error_type,
117            error_code,
118        }
119    }
120
121    /// Check if this is a rate limit error.
122    pub fn is_rate_limit(&self) -> bool {
123        matches!(self, Error::RateLimit(_)) || matches!(self, Error::Api { status: 429, .. })
124    }
125
126    /// Check if this is an authentication error.
127    pub fn is_auth_error(&self) -> bool {
128        matches!(self, Error::Authentication(_)) || matches!(self, Error::Api { status: 401, .. })
129    }
130
131    /// Check if this is a client error (4xx status codes).
132    pub fn is_client_error(&self) -> bool {
133        match self {
134            Error::Api { status, .. } => (400..500).contains(status),
135            Error::Authentication(_) | Error::RateLimit(_) | Error::InvalidRequest(_) => true,
136            _ => false,
137        }
138    }
139
140    /// Check if this is a server error (5xx status codes).
141    pub fn is_server_error(&self) -> bool {
142        match self {
143            Error::Api { status, .. } => (500..600).contains(status),
144            _ => false,
145        }
146    }
147
148    /// Check if this error might be retryable.
149    pub fn is_retryable(&self) -> bool {
150        self.is_rate_limit() || self.is_server_error()
151    }
152}
153
154/// Specialized error types for different API endpoints.
155pub mod chat {
156    use super::Error;
157
158    /// Create an error for invalid chat messages.
159    pub fn invalid_messages(msg: impl Into<String>) -> Error {
160        Error::InvalidRequest(format!("Invalid chat messages: {}", msg.into()))
161    }
162
163    /// Create an error for unsupported model.
164    pub fn unsupported_model(model: impl Into<String>) -> Error {
165        Error::InvalidRequest(format!("Unsupported model: {}", model.into()))
166    }
167}
168
169/// Specialized error types for responses API.
170pub mod responses {
171    use super::Error;
172
173    /// Create an error for invalid tool definition.
174    pub fn invalid_tool(msg: impl Into<String>) -> Error {
175        Error::InvalidRequest(format!("Invalid tool definition: {}", msg.into()))
176    }
177
178    /// Create an error for missing required response format.
179    pub fn missing_response_format() -> Error {
180        Error::InvalidRequest("Response format is required for structured outputs".to_string())
181    }
182}
183
184/// Specialized error types for file operations.
185pub mod files {
186    use super::Error;
187
188    /// Create an error for file upload failures.
189    pub fn upload_failed(msg: impl Into<String>) -> Error {
190        Error::File(std::io::Error::new(
191            std::io::ErrorKind::Other,
192            format!("File upload failed: {}", msg.into()),
193        ))
194    }
195
196    /// Create an error for unsupported file type.
197    pub fn unsupported_type(file_type: impl Into<String>) -> Error {
198        Error::InvalidRequest(format!("Unsupported file type: {}", file_type.into()))
199    }
200}
201
202/// Specialized error types for streaming operations.
203pub mod streaming {
204    use super::Error;
205
206    /// Create an error for stream connection failures.
207    pub fn connection_failed(msg: impl Into<String>) -> Error {
208        Error::Stream(format!("Stream connection failed: {}", msg.into()))
209    }
210
211    /// Create an error for stream parsing failures.
212    pub fn parse_failed(msg: impl Into<String>) -> Error {
213        Error::Stream(format!("Stream parsing failed: {}", msg.into()))
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    use std::io;
221
222    #[test]
223    fn test_error_display() {
224        let error = Error::InvalidRequest("test message".to_string());
225        assert_eq!(error.to_string(), "Invalid request: test message");
226
227        let error = Error::Authentication("invalid API key".to_string());
228        assert_eq!(error.to_string(), "Authentication failed: invalid API key");
229
230        let error = Error::RateLimit("rate limit exceeded".to_string());
231        assert_eq!(
232            error.to_string(),
233            "Rate limit exceeded: rate limit exceeded"
234        );
235    }
236
237    #[test]
238    fn test_api_error_constructors() {
239        let error = Error::api(400, "Bad request");
240        match error {
241            Error::Api {
242                status,
243                message,
244                error_type,
245                error_code,
246            } => {
247                assert_eq!(status, 400);
248                assert_eq!(message, "Bad request");
249                assert!(error_type.is_none());
250                assert!(error_code.is_none());
251            }
252            _ => panic!("Expected API error"),
253        }
254
255        let error = Error::api_detailed(
256            429,
257            "Rate limit exceeded",
258            Some("rate_limit_exceeded".to_string()),
259            Some("RL001".to_string()),
260        );
261        match error {
262            Error::Api {
263                status,
264                message,
265                error_type,
266                error_code,
267            } => {
268                assert_eq!(status, 429);
269                assert_eq!(message, "Rate limit exceeded");
270                assert_eq!(error_type, Some("rate_limit_exceeded".to_string()));
271                assert_eq!(error_code, Some("RL001".to_string()));
272            }
273            _ => panic!("Expected API error"),
274        }
275    }
276
277    #[test]
278    fn test_is_rate_limit() {
279        let error = Error::RateLimit("exceeded".to_string());
280        assert!(error.is_rate_limit());
281
282        let error = Error::api(429, "Too Many Requests");
283        assert!(error.is_rate_limit());
284
285        let error = Error::api(400, "Bad Request");
286        assert!(!error.is_rate_limit());
287
288        let error = Error::InvalidRequest("invalid".to_string());
289        assert!(!error.is_rate_limit());
290    }
291
292    #[test]
293    fn test_is_auth_error() {
294        let error = Error::Authentication("invalid key".to_string());
295        assert!(error.is_auth_error());
296
297        let error = Error::api(401, "Unauthorized");
298        assert!(error.is_auth_error());
299
300        let error = Error::api(403, "Forbidden");
301        assert!(!error.is_auth_error());
302
303        let error = Error::InvalidRequest("invalid".to_string());
304        assert!(!error.is_auth_error());
305    }
306
307    #[test]
308    fn test_is_client_error() {
309        let error = Error::api(400, "Bad Request");
310        assert!(error.is_client_error());
311
312        let error = Error::api(404, "Not Found");
313        assert!(error.is_client_error());
314
315        let error = Error::api(499, "Client Error");
316        assert!(error.is_client_error());
317
318        let error = Error::Authentication("invalid".to_string());
319        assert!(error.is_client_error());
320
321        let error = Error::RateLimit("exceeded".to_string());
322        assert!(error.is_client_error());
323
324        let error = Error::InvalidRequest("invalid".to_string());
325        assert!(error.is_client_error());
326
327        let error = Error::api(500, "Server Error");
328        assert!(!error.is_client_error());
329
330        let error = Error::Internal("internal".to_string());
331        assert!(!error.is_client_error());
332    }
333
334    #[test]
335    fn test_is_server_error() {
336        let error = Error::api(500, "Internal Server Error");
337        assert!(error.is_server_error());
338
339        let error = Error::api(502, "Bad Gateway");
340        assert!(error.is_server_error());
341
342        let error = Error::api(599, "Server Error");
343        assert!(error.is_server_error());
344
345        let error = Error::api(400, "Client Error");
346        assert!(!error.is_server_error());
347
348        let error = Error::Internal("internal".to_string());
349        assert!(!error.is_server_error());
350    }
351
352    #[test]
353    fn test_is_retryable() {
354        // Rate limit errors are retryable
355        let error = Error::RateLimit("exceeded".to_string());
356        assert!(error.is_retryable());
357
358        let error = Error::api(429, "Too Many Requests");
359        assert!(error.is_retryable());
360
361        // Server errors are retryable
362        let error = Error::api(500, "Internal Server Error");
363        assert!(error.is_retryable());
364
365        let error = Error::api(502, "Bad Gateway");
366        assert!(error.is_retryable());
367
368        // Client errors are not retryable
369        let error = Error::api(400, "Bad Request");
370        assert!(!error.is_retryable());
371
372        let error = Error::Authentication("invalid".to_string());
373        assert!(!error.is_retryable());
374
375        let error = Error::InvalidRequest("invalid".to_string());
376        assert!(!error.is_retryable());
377    }
378
379    #[test]
380    fn test_from_reqwest_error() {
381        // Test that the conversion trait exists by creating a function that uses it
382        #[allow(clippy::items_after_statements)]
383        fn _test_reqwest_error_conversion(reqwest_error: reqwest::Error) -> Error {
384            reqwest_error.into()
385        }
386
387        // The test passes if this compiles - we verify the trait implementation exists
388    }
389
390    #[test]
391    fn test_from_serde_json_error() {
392        let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
393        let error: Error = json_error.into();
394        assert!(matches!(error, Error::Json(_)));
395    }
396
397    #[test]
398    fn test_from_io_error() {
399        let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
400        let error: Error = io_error.into();
401        assert!(matches!(error, Error::File(_)));
402    }
403
404    #[test]
405    fn test_stream_errors() {
406        let error = Error::StreamConnection {
407            message: "connection lost".to_string(),
408        };
409        assert_eq!(
410            error.to_string(),
411            "Stream connection error: connection lost"
412        );
413
414        let error = Error::StreamParsing {
415            message: "invalid data".to_string(),
416            chunk: "bad chunk".to_string(),
417        };
418        assert_eq!(
419            error.to_string(),
420            "Stream parsing error: invalid data, chunk: bad chunk"
421        );
422
423        let error = Error::StreamBuffer {
424            message: "buffer overflow".to_string(),
425        };
426        assert_eq!(error.to_string(), "Stream buffer error: buffer overflow");
427
428        let error = Error::Stream("generic stream error".to_string());
429        assert_eq!(error.to_string(), "Stream error: generic stream error");
430    }
431
432    #[test]
433    fn test_specialized_error_modules() {
434        // Test chat module errors
435        let error = chat::invalid_messages("empty messages");
436        assert!(matches!(error, Error::InvalidRequest(_)));
437        assert!(error.to_string().contains("Invalid chat messages"));
438
439        let error = chat::unsupported_model("gpt-5");
440        assert!(matches!(error, Error::InvalidRequest(_)));
441        assert!(error.to_string().contains("Unsupported model"));
442
443        // Test responses module errors
444        let error = responses::invalid_tool("missing name");
445        assert!(matches!(error, Error::InvalidRequest(_)));
446        assert!(error.to_string().contains("Invalid tool definition"));
447
448        let error = responses::missing_response_format();
449        assert!(matches!(error, Error::InvalidRequest(_)));
450        assert!(error.to_string().contains("Response format is required"));
451
452        // Test files module errors
453        let error = files::upload_failed("network error");
454        assert!(matches!(error, Error::File(_)));
455
456        let error = files::unsupported_type("txt");
457        assert!(matches!(error, Error::InvalidRequest(_)));
458        assert!(error.to_string().contains("Unsupported file type"));
459
460        // Test streaming module errors
461        let error = streaming::connection_failed("timeout");
462        assert!(matches!(error, Error::Stream(_)));
463        assert!(error.to_string().contains("Stream connection failed"));
464
465        let error = streaming::parse_failed("invalid JSON");
466        assert!(matches!(error, Error::Stream(_)));
467        assert!(error.to_string().contains("Stream parsing failed"));
468    }
469
470    #[test]
471    fn test_error_debug_format() {
472        let error = Error::InvalidRequest("test".to_string());
473        let debug_str = format!("{error:?}");
474        assert!(debug_str.contains("InvalidRequest"));
475        assert!(debug_str.contains("test"));
476    }
477
478    #[test]
479    fn test_error_chains() {
480        // Test that errors properly chain when using From traits
481        let io_error = io::Error::new(io::ErrorKind::PermissionDenied, "access denied");
482        let wrapped_error: Error = io_error.into();
483
484        match wrapped_error {
485            Error::File(ref err) => {
486                assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
487            }
488            _ => panic!("Expected File error"),
489        }
490    }
491
492    #[test]
493    fn test_config_error() {
494        let error = Error::Config("missing API key".to_string());
495        assert_eq!(error.to_string(), "Configuration error: missing API key");
496    }
497
498    #[test]
499    fn test_builder_error() {
500        let error = Error::Builder("validation failed".to_string());
501        assert_eq!(
502            error.to_string(),
503            "Builder validation error: validation failed"
504        );
505    }
506
507    #[test]
508    fn test_internal_error() {
509        let error = Error::Internal("unexpected state".to_string());
510        assert_eq!(error.to_string(), "Internal error: unexpected state");
511    }
512
513    #[test]
514    fn test_error_status_boundaries() {
515        // Test edge cases for status code ranges
516        let error = Error::api(399, "Client Error");
517        assert!(!error.is_client_error());
518
519        let error = Error::api(400, "Client Error");
520        assert!(error.is_client_error());
521
522        let error = Error::api(499, "Client Error");
523        assert!(error.is_client_error());
524
525        let error = Error::api(500, "Server Error");
526        assert!(!error.is_client_error());
527        assert!(error.is_server_error());
528
529        let error = Error::api(599, "Server Error");
530        assert!(error.is_server_error());
531
532        let error = Error::api(600, "Unknown");
533        assert!(!error.is_server_error());
534    }
535}