chat_gpt_lib_rs/
error.rs

1//! The `error` module defines all error types that may arise when interacting with the OpenAI API.
2//!
3//! The main error type exported here is [`OpenAIError`], which enumerates various errors such as
4//! configuration issues, HTTP/network problems, or API-level responses indicating invalid requests,
5//! rate limits, and so on.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use chat_gpt_lib_rs::OpenAIError;
11//!
12//! fn example() -> Result<(), OpenAIError> {
13//!     // Simulate an error scenario:
14//!     Err(OpenAIError::ConfigError("No API key set".into()))
15//! }
16//! ```
17
18use thiserror::Error;
19
20/// Represents any error that can occur while using the OpenAI Rust client library.
21///
22/// This enum covers:
23/// 1. Configuration errors, such as missing API keys or invalid builder settings.
24/// 2. Network/HTTP errors encountered while making requests.
25/// 3. Errors returned by the OpenAI API itself (e.g., rate limits, invalid parameters).
26/// 4. JSON parsing errors (e.g., unexpected response formats).
27/// 5. IO errors that might occur during operations like file handling.
28#[derive(Debug, Error)]
29pub enum OpenAIError {
30    /// Errors related to invalid configuration or missing environment variables.
31    #[error("Configuration Error: {0}")]
32    ConfigError(String),
33
34    /// Errors returned by the underlying HTTP client (`reqwest`).
35    ///
36    /// This typically indicates network-level issues, timeouts, TLS errors, etc.
37    #[error("HTTP Error: {0}")]
38    HTTPError(#[from] reqwest::Error),
39
40    /// Errors that happen due to invalid or unexpected responses from the OpenAI API.
41    ///
42    /// For instance, if the response body is not valid JSON or doesn't match the expected schema.
43    #[error("Deserialization/Parsing Error: {0}")]
44    DeserializeError(#[from] serde_json::Error),
45
46    /// Errors reported by the OpenAI API in its response body.
47    ///
48    /// This might include invalid request parameters, rate-limit violations, or internal
49    /// server errors. The attached string typically contains a more descriptive message
50    /// returned by the API.
51    #[error("OpenAI API Error: {message}")]
52    APIError {
53        /// A short summary of what went wrong (as provided by the OpenAI API).
54        message: String,
55        /// The type/category of error (e.g. 'invalid_request_error', 'rate_limit_error', etc.).
56        #[allow(dead_code)]
57        err_type: Option<String>,
58        /// An optional error code that might be returned by the OpenAI API.
59        #[allow(dead_code)]
60        code: Option<String>,
61    },
62
63    /// Errors that occur during I/O operations.
64    #[error("IO Error: {0}")]
65    IOError(#[from] std::io::Error),
66}
67
68impl OpenAIError {
69    /// Creates an [`OpenAIError::APIError`] from detailed information about the error.
70    ///
71    /// # Parameters
72    ///
73    /// * `message` - A short description of the error.
74    /// * `err_type` - The error type from OpenAI (e.g., "invalid_request_error").
75    /// * `code` - An optional error code from OpenAI.
76    ///
77    /// # Example
78    ///
79    /// ```rust
80    /// use chat_gpt_lib_rs::OpenAIError;
81    ///
82    /// let api_err = OpenAIError::api_error("Invalid request", Some("invalid_request_error"), None);
83    /// ```
84    pub fn api_error(
85        message: impl Into<String>,
86        err_type: Option<&str>,
87        code: Option<&str>,
88    ) -> Self {
89        OpenAIError::APIError {
90            message: message.into(),
91            err_type: err_type.map(|s| s.to_string()),
92            code: code.map(|s| s.to_string()),
93        }
94    }
95}
96
97/// An internal struct that represents the standard error response from the OpenAI API.
98///
99/// When the OpenAI API returns an error (e.g., 4xx or 5xx status code), it often includes
100/// a JSON body describing the error. This struct captures those fields. Your code can
101/// deserialize this into an [`OpenAIAPIErrorBody`] and then map it to an [`OpenAIError::APIError`].
102#[derive(Debug, serde::Deserialize)]
103pub(crate) struct OpenAIAPIErrorBody {
104    /// The actual error details in a nested structure.
105    pub error: OpenAIAPIErrorDetails,
106}
107
108/// The nested structure holding the error details returned by OpenAI.
109#[derive(Debug, serde::Deserialize)]
110pub(crate) struct OpenAIAPIErrorDetails {
111    /// A human-readable error message.
112    pub message: String,
113    /// The type/category of the error (e.g., "invalid_request_error", "rate_limit_error").
114    #[serde(rename = "type")]
115    pub err_type: String,
116    /// An optional error code (e.g., "invalid_api_key").
117    pub code: Option<String>,
118}
119
120impl From<OpenAIAPIErrorBody> for OpenAIError {
121    fn from(body: OpenAIAPIErrorBody) -> Self {
122        OpenAIError::APIError {
123            message: body.error.message,
124            err_type: Some(body.error.err_type),
125            code: body.error.code,
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    //! # Tests for the `error` module
133    //!
134    //! We verify each variant of [`OpenAIError`] along with its convenience methods
135    //! (e.g. `api_error`) and `From<OpenAIAPIErrorBody>` implementation. This ensures
136    //! that errors are created, displayed, and converted as expected under various conditions.
137
138    use super::*;
139    use std::fmt::Write as _;
140
141    /// Produces a `reqwest::Error` by making a **blocking** request to an invalid URL.
142    /// This requires `reqwest` with the `"blocking"` feature enabled.
143    fn produce_reqwest_error() -> reqwest::Error {
144        // Attempting to make a request to a non-routable domain or an invalid protocol
145        reqwest::blocking::Client::new()
146            .get("http://this-domain-should-not-exist9999.test")
147            .send()
148            .unwrap_err()
149    }
150
151    /// Produces a `serde_json::Error` by parsing invalid JSON.
152    fn produce_serde_json_error() -> serde_json::Error {
153        serde_json::from_str::<serde_json::Value>("\"unterminated string").unwrap_err()
154    }
155
156    /// Produces a `std::io::Error` by trying to open a non-existent file.
157    fn produce_io_error() -> std::io::Error {
158        std::fs::File::open("non_existent_file.txt").unwrap_err()
159    }
160
161    #[test]
162    fn test_config_error() {
163        let err = OpenAIError::ConfigError("No API key found".to_string());
164        let display_str = format!("{}", err);
165
166        // Verify it's the correct variant
167        match &err {
168            OpenAIError::ConfigError(msg) => {
169                assert_eq!(msg, "No API key found");
170            }
171            other => panic!("Expected ConfigError, got: {:?}", other),
172        }
173
174        // Check Display output
175        assert!(
176            display_str.contains("No API key found"),
177            "Display should contain the config error message, got: {}",
178            display_str
179        );
180    }
181
182    #[test]
183    fn test_http_error() {
184        let reqwest_err = produce_reqwest_error();
185        let err = OpenAIError::HTTPError(reqwest_err);
186
187        let display_str = format!("{}", err);
188        assert!(
189            display_str.contains("HTTP Error:"),
190            "Should contain 'HTTP Error:' prefix, got: {}",
191            display_str
192        );
193
194        // Pattern-match on &err
195        match &err {
196            OpenAIError::HTTPError(e) => {
197                let e_str = format!("{}", e);
198                // Accept multiple possible error messages
199                assert!(
200                    e_str.contains("error sending request")
201                        || e_str.contains("dns error")
202                        || e_str.contains("Could not resolve host")
203                        || e_str.contains("Name or service not known"),
204                    "Expected mention of DNS/resolve error or sending request, got: {}",
205                    e_str
206                );
207            }
208            other => panic!("Expected HTTPError, got: {:?}", other),
209        }
210    }
211
212    #[test]
213    fn test_deserialize_error() {
214        // Produce a serde_json::Error, then convert to OpenAIError
215        let serde_err = produce_serde_json_error();
216        let err = OpenAIError::DeserializeError(serde_err);
217
218        // Display
219        let display_str = format!("{}", err);
220        assert!(
221            display_str.contains("Deserialization/Parsing Error:"),
222            "Should contain 'Deserialization/Parsing Error:', got: {}",
223            display_str
224        );
225
226        // Pattern-match on &err to avoid partial moves
227        match &err {
228            OpenAIError::DeserializeError(e) => {
229                let e_str = format!("{}", e);
230                assert!(
231                    e_str.contains("EOF while parsing a string")
232                        || e_str.contains("unterminated string"),
233                    "Expected mention of parse error about unterminated, got: {}",
234                    e_str
235                );
236            }
237            other => panic!("Expected DeserializeError, got: {:?}", other),
238        }
239    }
240
241    #[test]
242    fn test_api_error() {
243        // Create an APIError variant via the convenience method
244        let err = OpenAIError::api_error(
245            "Something went wrong",
246            Some("invalid_request_error"),
247            Some("ERR123"),
248        );
249        let display_str = format!("{}", err);
250
251        match &err {
252            OpenAIError::APIError {
253                message,
254                err_type,
255                code,
256            } => {
257                assert_eq!(message, "Something went wrong");
258                assert_eq!(err_type.as_deref(), Some("invalid_request_error"));
259                assert_eq!(code.as_deref(), Some("ERR123"));
260            }
261            other => panic!("Expected APIError, got: {:?}", other),
262        }
263
264        // Check Display output
265        assert!(
266            display_str.contains("OpenAI API Error: Something went wrong"),
267            "Expected 'OpenAI API Error:' prefix, got: {}",
268            display_str
269        );
270    }
271
272    #[test]
273    fn test_from_openaiapierrorbody() {
274        let body = OpenAIAPIErrorBody {
275            error: OpenAIAPIErrorDetails {
276                message: "Rate limit exceeded".to_string(),
277                err_type: "rate_limit_error".to_string(),
278                code: Some("rate_limit_code".to_string()),
279            },
280        };
281        let err = OpenAIError::from(body);
282
283        match &err {
284            OpenAIError::APIError {
285                message,
286                err_type,
287                code,
288            } => {
289                assert_eq!(message, "Rate limit exceeded");
290                assert_eq!(err_type.as_deref(), Some("rate_limit_error"));
291                assert_eq!(code.as_deref(), Some("rate_limit_code"));
292            }
293            other => panic!("Expected APIError from error body, got: {:?}", other),
294        }
295    }
296
297    #[test]
298    fn test_io_error() {
299        let io_err = produce_io_error();
300        let err: OpenAIError = io_err.into();
301
302        let display_str = format!("{}", err);
303        assert!(
304            display_str.contains("IO Error:"),
305            "Display should contain 'IO Error:' prefix, got: {}",
306            display_str
307        );
308
309        // Pattern-match on &err
310        match &err {
311            OpenAIError::IOError(e) => {
312                let e_str = format!("{}", e);
313                let lower = e_str.to_lowercase();
314                assert!(
315                    lower.contains("no such file")
316                        || lower.contains("not found")
317                        || lower.contains("os error 2"),
318                    "Expected mention of file not found error, got: {}",
319                    e_str
320                );
321            }
322            other => panic!("Expected IOError, got: {:?}", other),
323        }
324    }
325
326    #[test]
327    fn test_display_trait_all_variants() {
328        let config_err = OpenAIError::ConfigError("missing key".to_string());
329        let http_err = OpenAIError::HTTPError(produce_reqwest_error());
330        let deser_err = OpenAIError::DeserializeError(produce_serde_json_error());
331        let api_err = OpenAIError::api_error("Remote server said no", Some("some_api_error"), None);
332        let io_err = OpenAIError::IOError(produce_io_error());
333
334        let mut combined = String::new();
335        writeln!(&mut combined, "{}", config_err).unwrap();
336        writeln!(&mut combined, "{}", http_err).unwrap();
337        writeln!(&mut combined, "{}", deser_err).unwrap();
338        writeln!(&mut combined, "{}", api_err).unwrap();
339        writeln!(&mut combined, "{}", io_err).unwrap();
340
341        // Just a quick check of the combined output
342        assert!(combined.contains("Configuration Error: missing key"));
343        assert!(combined.contains("HTTP Error:"));
344        assert!(combined.contains("Deserialization/Parsing Error:"));
345        assert!(combined.contains("OpenAI API Error: Remote server said no"));
346        assert!(combined.contains("IO Error:"));
347    }
348}