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