Skip to main content

rstructor/error/
mod.rs

1use std::time::Duration;
2use thiserror::Error;
3
4/// Classification of API errors for better handling and retry logic.
5///
6/// This enum categorizes HTTP errors from LLM providers into actionable types,
7/// making it easier to determine appropriate responses (retry, fix config, wait, etc.).
8///
9/// # Example
10///
11/// ```
12/// use rstructor::{RStructorError, ApiErrorKind};
13///
14/// fn handle_api_error(err: &RStructorError) {
15///     if let Some(kind) = err.api_error_kind() {
16///         match kind {
17///             ApiErrorKind::RateLimited { retry_after } => {
18///                 println!("Rate limited! Wait {:?} and retry", retry_after);
19///             }
20///             ApiErrorKind::AuthenticationFailed => {
21///                 println!("Check your API key");
22///             }
23///             ApiErrorKind::InvalidModel { model, suggestion } => {
24///                 println!("Model '{}' not found", model);
25///                 if let Some(s) = suggestion {
26///                     println!("Try: {}", s);
27///                 }
28///             }
29///             _ if err.is_retryable() => {
30///                 println!("Transient error, will retry");
31///             }
32///             _ => {
33///                 println!("Unrecoverable error");
34///             }
35///         }
36///     }
37/// }
38/// ```
39#[derive(Debug, Clone, PartialEq)]
40pub enum ApiErrorKind {
41    /// Rate limit exceeded (HTTP 429)
42    ///
43    /// The API is rate limiting requests. Wait for the specified duration before retrying.
44    RateLimited {
45        /// How long to wait before retrying (if provided by the API)
46        retry_after: Option<Duration>,
47    },
48
49    /// Invalid or unknown model (HTTP 404 with model context)
50    ///
51    /// The requested model does not exist or is not accessible.
52    InvalidModel {
53        /// The model name that was not found
54        model: String,
55        /// A suggested alternative model, if available
56        suggestion: Option<String>,
57    },
58
59    /// Service temporarily unavailable (HTTP 503)
60    ///
61    /// The API service is temporarily down. This is usually transient.
62    ServiceUnavailable,
63
64    /// Gateway/proxy error (HTTP 520-524, Cloudflare errors)
65    ///
66    /// An error occurred at the gateway level. Usually transient.
67    GatewayError {
68        /// The specific HTTP status code
69        code: u16,
70    },
71
72    /// Authentication failed (HTTP 401)
73    ///
74    /// The API key is invalid, expired, or missing.
75    AuthenticationFailed,
76
77    /// Permission denied (HTTP 403)
78    ///
79    /// The API key doesn't have permission for this operation or model.
80    PermissionDenied,
81
82    /// Request too large (HTTP 413)
83    ///
84    /// The request payload (usually the prompt) is too large.
85    RequestTooLarge,
86
87    /// Invalid request (HTTP 400)
88    ///
89    /// The request was malformed or contained invalid parameters.
90    BadRequest {
91        /// Details about what was invalid
92        details: String,
93    },
94
95    /// Server error (HTTP 500, 502)
96    ///
97    /// An internal server error occurred. May be transient.
98    ServerError {
99        /// The specific HTTP status code
100        code: u16,
101    },
102
103    /// Generic/unclassified API error
104    Other {
105        /// The HTTP status code
106        code: u16,
107        /// The error message from the API
108        message: String,
109    },
110
111    /// Unexpected response format from API
112    ///
113    /// The API returned a successful HTTP status but the response content
114    /// was missing expected fields (e.g., empty choices array, no content).
115    UnexpectedResponse {
116        /// Description of what was expected vs received
117        details: String,
118    },
119}
120
121impl ApiErrorKind {
122    /// Returns whether this error is potentially retryable.
123    ///
124    /// Retryable errors are transient issues that may succeed on a subsequent attempt.
125    ///
126    /// # Example
127    ///
128    /// ```
129    /// use rstructor::ApiErrorKind;
130    /// use std::time::Duration;
131    ///
132    /// let rate_limited = ApiErrorKind::RateLimited { retry_after: Some(Duration::from_secs(5)) };
133    /// assert!(rate_limited.is_retryable());
134    ///
135    /// let auth_failed = ApiErrorKind::AuthenticationFailed;
136    /// assert!(!auth_failed.is_retryable());
137    /// ```
138    pub fn is_retryable(&self) -> bool {
139        matches!(
140            self,
141            ApiErrorKind::RateLimited { .. }
142                | ApiErrorKind::ServiceUnavailable
143                | ApiErrorKind::GatewayError { .. }
144                | ApiErrorKind::ServerError { .. }
145        )
146    }
147
148    /// Returns the suggested wait duration for retryable errors.
149    ///
150    /// For rate-limited errors, returns the `retry_after` duration if available.
151    /// For other retryable errors, returns a sensible default.
152    pub fn retry_delay(&self) -> Option<Duration> {
153        match self {
154            ApiErrorKind::RateLimited { retry_after } => {
155                Some(retry_after.unwrap_or(Duration::from_secs(5)))
156            }
157            ApiErrorKind::ServiceUnavailable => Some(Duration::from_secs(2)),
158            ApiErrorKind::GatewayError { .. } => Some(Duration::from_secs(1)),
159            ApiErrorKind::ServerError { .. } => Some(Duration::from_secs(2)),
160            _ => None,
161        }
162    }
163
164    /// Returns a user-friendly message describing the error and suggested action.
165    pub fn user_message(&self, provider_name: &str) -> String {
166        match self {
167            ApiErrorKind::RateLimited { retry_after } => {
168                if let Some(duration) = retry_after {
169                    format!(
170                        "Rate limit exceeded. Please wait {} seconds and try again.",
171                        duration.as_secs()
172                    )
173                } else {
174                    "Rate limit exceeded. Please wait a moment and try again.".to_string()
175                }
176            }
177            ApiErrorKind::InvalidModel { model, suggestion } => {
178                let mut msg = format!("Model '{}' not found.", model);
179                if let Some(s) = suggestion {
180                    msg.push_str(&format!(" Try using '{}'.", s));
181                }
182                msg
183            }
184            ApiErrorKind::ServiceUnavailable => {
185                format!(
186                    "{} service is temporarily unavailable. Please try again.",
187                    provider_name
188                )
189            }
190            ApiErrorKind::GatewayError { code } => {
191                format!(
192                    "Gateway error ({}). This is usually transient - please retry.",
193                    code
194                )
195            }
196            ApiErrorKind::AuthenticationFailed => {
197                format!(
198                    "Authentication failed. Check your {}_API_KEY environment variable.",
199                    provider_name.to_uppercase()
200                )
201            }
202            ApiErrorKind::PermissionDenied => {
203                "Permission denied. Your API key may not have access to this model or feature."
204                    .to_string()
205            }
206            ApiErrorKind::RequestTooLarge => {
207                "Request too large. Try reducing the prompt length or max_tokens.".to_string()
208            }
209            ApiErrorKind::BadRequest { details } => {
210                format!("Invalid request: {}", details)
211            }
212            ApiErrorKind::ServerError { code } => {
213                format!(
214                    "{} server error ({}). This may be transient - please retry.",
215                    provider_name, code
216                )
217            }
218            ApiErrorKind::Other { code, message } => {
219                format!("{} API error ({}): {}", provider_name, code, message)
220            }
221            ApiErrorKind::UnexpectedResponse { details } => {
222                format!(
223                    "{} returned an unexpected response: {}",
224                    provider_name, details
225                )
226            }
227        }
228    }
229}
230
231impl std::fmt::Display for ApiErrorKind {
232    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233        match self {
234            ApiErrorKind::RateLimited { retry_after } => {
235                write!(f, "Rate limited")?;
236                if let Some(d) = retry_after {
237                    write!(f, " (retry after {}s)", d.as_secs())?;
238                }
239                Ok(())
240            }
241            ApiErrorKind::InvalidModel { model, .. } => write!(f, "Invalid model: {}", model),
242            ApiErrorKind::ServiceUnavailable => write!(f, "Service unavailable"),
243            ApiErrorKind::GatewayError { code } => write!(f, "Gateway error ({})", code),
244            ApiErrorKind::AuthenticationFailed => write!(f, "Authentication failed"),
245            ApiErrorKind::PermissionDenied => write!(f, "Permission denied"),
246            ApiErrorKind::RequestTooLarge => write!(f, "Request too large"),
247            ApiErrorKind::BadRequest { details } => write!(f, "Bad request: {}", details),
248            ApiErrorKind::ServerError { code } => write!(f, "Server error ({})", code),
249            ApiErrorKind::Other { code, message } => write!(f, "API error ({}): {}", code, message),
250            ApiErrorKind::UnexpectedResponse { details } => {
251                write!(f, "Unexpected response: {}", details)
252            }
253        }
254    }
255}
256
257/// Error types for the rstructor library.
258///
259/// This enum defines the various error types that can occur within the rstructor library.
260/// Each variant represents a different category of error and includes context about what went wrong.
261///
262/// # Examples
263///
264/// Creating and handling errors:
265///
266/// ```
267/// use rstructor::{RStructorError, Result};
268///
269/// // Function that might return an error
270/// fn validate_age(age: i32) -> Result<()> {
271///     if age < 0 {
272///         return Err(RStructorError::ValidationError("Age cannot be negative".into()));
273///     }
274///     if age > 150 {
275///         return Err(RStructorError::ValidationError("Age is unrealistically high".into()));
276///     }
277///     Ok(())
278/// }
279///
280/// // Using the function and handling errors
281/// let result = validate_age(200);
282/// match result {
283///     Ok(()) => println!("Age is valid"),
284///     Err(RStructorError::ValidationError(msg)) => println!("Invalid age: {}", msg),
285///     Err(e) => println!("Unexpected error: {}", e),
286/// }
287/// ```
288#[derive(Error, Debug)]
289pub enum RStructorError {
290    /// Error interacting with the LLM API (with rich error classification)
291    #[error("{}", .kind.user_message(.provider))]
292    ApiError {
293        /// The provider that returned the error (e.g., "OpenAI", "Anthropic")
294        provider: String,
295        /// The classified error kind
296        kind: ApiErrorKind,
297    },
298
299    /// Error validating data against schema or business rules
300    #[error("Validation error: {0}")]
301    ValidationError(String),
302
303    /// Error related to JSON Schema generation or processing
304    #[error("Schema error: {0}")]
305    SchemaError(String),
306
307    /// Error serializing or deserializing data
308    #[error("Serialization error: {0}")]
309    SerializationError(String),
310
311    /// Operation timed out
312    #[error("Timeout error")]
313    Timeout,
314
315    /// HTTP client error (from reqwest)
316    #[error("HTTP client error: {0}")]
317    HttpError(#[from] reqwest::Error),
318
319    /// JSON parsing error (from serde_json)
320    #[error("JSON error: {0}")]
321    JsonError(#[from] serde_json::Error),
322}
323
324impl RStructorError {
325    /// Create a new API error with rich classification.
326    ///
327    /// # Arguments
328    ///
329    /// * `provider` - The LLM provider name (e.g., "OpenAI", "Anthropic")
330    /// * `kind` - The classified error kind
331    pub fn api_error(provider: impl Into<String>, kind: ApiErrorKind) -> Self {
332        RStructorError::ApiError {
333            provider: provider.into(),
334            kind,
335        }
336    }
337
338    /// Returns the API error kind if this is an API error.
339    ///
340    /// # Example
341    ///
342    /// ```
343    /// use rstructor::{RStructorError, ApiErrorKind};
344    ///
345    /// let err = RStructorError::api_error("OpenAI", ApiErrorKind::AuthenticationFailed);
346    /// assert!(matches!(err.api_error_kind(), Some(ApiErrorKind::AuthenticationFailed)));
347    /// ```
348    pub fn api_error_kind(&self) -> Option<&ApiErrorKind> {
349        match self {
350            RStructorError::ApiError { kind, .. } => Some(kind),
351            _ => None,
352        }
353    }
354
355    /// Returns whether this error is potentially retryable.
356    ///
357    /// Retryable errors include:
358    /// - Rate limiting (429)
359    /// - Service unavailable (503)
360    /// - Gateway errors (520-524)
361    /// - Server errors (500, 502)
362    /// - Timeout errors
363    ///
364    /// # Example
365    ///
366    /// ```
367    /// use rstructor::{RStructorError, ApiErrorKind};
368    /// use std::time::Duration;
369    ///
370    /// let rate_limited = RStructorError::api_error(
371    ///     "OpenAI",
372    ///     ApiErrorKind::RateLimited { retry_after: Some(Duration::from_secs(5)) }
373    /// );
374    /// assert!(rate_limited.is_retryable());
375    ///
376    /// let auth_error = RStructorError::api_error("OpenAI", ApiErrorKind::AuthenticationFailed);
377    /// assert!(!auth_error.is_retryable());
378    /// ```
379    pub fn is_retryable(&self) -> bool {
380        match self {
381            RStructorError::ApiError { kind, .. } => kind.is_retryable(),
382            RStructorError::Timeout => true,
383            _ => false,
384        }
385    }
386
387    /// Returns the suggested retry delay for retryable errors.
388    ///
389    /// Returns `None` for non-retryable errors.
390    pub fn retry_delay(&self) -> Option<Duration> {
391        match self {
392            RStructorError::ApiError { kind, .. } => kind.retry_delay(),
393            RStructorError::Timeout => Some(Duration::from_secs(1)),
394            _ => None,
395        }
396    }
397}
398
399// Manual implementation of PartialEq for RStructorError
400// Note: HttpError and JsonError variants are considered unequal
401// because reqwest::Error and serde_json::Error don't implement PartialEq
402impl PartialEq for RStructorError {
403    fn eq(&self, other: &Self) -> bool {
404        match (self, other) {
405            (
406                Self::ApiError {
407                    provider: p1,
408                    kind: k1,
409                },
410                Self::ApiError {
411                    provider: p2,
412                    kind: k2,
413                },
414            ) => p1 == p2 && k1 == k2,
415            (Self::ValidationError(a), Self::ValidationError(b)) => a == b,
416            (Self::SchemaError(a), Self::SchemaError(b)) => a == b,
417            (Self::SerializationError(a), Self::SerializationError(b)) => a == b,
418            (Self::Timeout, Self::Timeout) => true,
419            // HttpError and JsonError don't implement PartialEq, so we always return false
420            (Self::HttpError(_), Self::HttpError(_)) => false,
421            (Self::JsonError(_), Self::JsonError(_)) => false,
422            _ => false,
423        }
424    }
425}
426
427/// A specialized Result type for rstructor operations.
428///
429/// This type is used throughout the rstructor library to return either
430/// a success value of type T or an RStructorError.
431///
432/// # Examples
433///
434/// Using Result type in functions:
435///
436/// ```
437/// use rstructor::{RStructorError, Result};
438///
439/// fn parse_json_data(data: &str) -> Result<serde_json::Value> {
440///     match serde_json::from_str(data) {
441///         Ok(value) => Ok(value),
442///         Err(e) => Err(RStructorError::JsonError(e)),
443///     }
444/// }
445///
446/// // Using the ? operator with Result
447/// fn process_data(input: &str) -> Result<String> {
448///     let json = parse_json_data(input)?;
449///     // Process the JSON...
450///     Ok("Processed successfully".to_string())
451/// }
452/// ```
453pub type Result<T> = std::result::Result<T, RStructorError>;