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>;