Skip to main content

aix_core/
error.rs

1//! Comprehensive error handling for the AIX library.
2//!
3//! This module provides a unified error type that can represent all possible
4//! failure modes when interacting with AI providers.
5
6use std::error::Error as StdError;
7use std::fmt;
8use std::time::Duration;
9
10/// Unified error type for all AIX operations.
11#[derive(Debug, Clone)]
12pub enum AixError {
13    /// Transport layer errors (network, DNS, etc.)
14    Transport {
15        /// The underlying error
16        source: String,
17        /// Additional context about the error
18        context: String,
19    },
20    /// Provider-specific errors (API errors, validation, etc.)
21    Provider {
22        /// Name of the provider that returned the error
23        provider: String,
24        /// Error code from the provider (if available)
25        code: Option<String>,
26        /// Human-readable error message
27        message: String,
28        /// HTTP status code (if applicable)
29        status: Option<u16>,
30    },
31    /// Rate limiting errors
32    RateLimit {
33        /// Name of the provider that rate limited the request
34        provider: String,
35        /// Suggested retry delay (if provided by the provider)
36        retry_after: Option<Duration>,
37        /// Human-readable error message
38        message: String,
39    },
40    /// Serialization/deserialization errors
41    Serialization {
42        /// The underlying error
43        source: String,
44        /// Additional context about what was being serialized/deserialized
45        context: String,
46    },
47    /// Configuration errors
48    Config {
49        /// Human-readable error message
50        message: String,
51    },
52    /// Streaming-related errors
53    Stream {
54        /// Human-readable error message
55        message: String,
56        /// The underlying error (if available)
57        source: Option<String>,
58    },
59    /// Safety/content policy violations
60    Safety {
61        /// Name of the provider that flagged the content
62        provider: String,
63        /// Category of safety violation
64        category: String,
65        /// Human-readable error message
66        message: String,
67    },
68    /// Authentication/authorization errors
69    Auth {
70        /// Name of the provider that rejected the authentication
71        provider: String,
72        /// Human-readable error message
73        message: String,
74    },
75    /// Timeout errors
76    Timeout {
77        /// The operation that timed out
78        operation: String,
79        /// How long we waited before timing out
80        duration: Duration,
81    },
82    /// Catch-all for other errors
83    Other {
84        /// Human-readable error message
85        message: String,
86        /// The underlying error (if available)
87        source: Option<String>,
88    },
89}
90
91impl AixError {
92    /// Check if this error is retryable.
93    ///
94    /// Returns true if the error warrants a retry attempt.
95    pub fn is_retryable(&self) -> bool {
96        match self {
97            AixError::Transport { .. } => true,
98            AixError::Provider { status, .. } => {
99                status.map_or(false, |s| s >= 500 || s == 429)
100            }
101            AixError::RateLimit { .. } => true,
102            AixError::Timeout { .. } => true,
103            AixError::Serialization { .. } => false,
104            AixError::Config { .. } => false,
105            AixError::Stream { .. } => false,
106            AixError::Safety { .. } => false,
107            AixError::Auth { .. } => false,
108            AixError::Other { .. } => false,
109        }
110    }
111
112    /// Create a new transport error.
113    pub fn transport<S: Into<String>, C: Into<String>>(source: S, context: C) -> Self {
114        AixError::Transport {
115            source: source.into(),
116            context: context.into(),
117        }
118    }
119
120    /// Create a new provider error.
121    pub fn provider<P: Into<String>, M: Into<String>>(
122        provider: P,
123        message: M,
124    ) -> Self {
125        AixError::Provider {
126            provider: provider.into(),
127            code: None,
128            message: message.into(),
129            status: None,
130        }
131    }
132
133    /// Create a new provider error with status and code.
134    pub fn provider_with_details<P: Into<String>, M: Into<String>, C: Into<String>>(
135        provider: P,
136        message: M,
137        status: u16,
138        code: C,
139    ) -> Self {
140        AixError::Provider {
141            provider: provider.into(),
142            code: Some(code.into()),
143            message: message.into(),
144            status: Some(status),
145        }
146    }
147
148    /// Create a new rate limit error.
149    pub fn rate_limit<P: Into<String>, M: Into<String>>(
150        provider: P,
151        message: M,
152    ) -> Self {
153        AixError::RateLimit {
154            provider: provider.into(),
155            retry_after: None,
156            message: message.into(),
157        }
158    }
159
160    /// Create a new rate limit error with retry after.
161    pub fn rate_limit_with_retry<P: Into<String>, M: Into<String>>(
162        provider: P,
163        message: M,
164        retry_after: Duration,
165    ) -> Self {
166        AixError::RateLimit {
167            provider: provider.into(),
168            retry_after: Some(retry_after),
169            message: message.into(),
170        }
171    }
172
173    /// Create a new serialization error.
174    pub fn serialization<S: Into<String>, C: Into<String>>(source: S, context: C) -> Self {
175        AixError::Serialization {
176            source: source.into(),
177            context: context.into(),
178        }
179    }
180
181    /// Create a new config error.
182    pub fn config<M: Into<String>>(message: M) -> Self {
183        AixError::Config {
184            message: message.into(),
185        }
186    }
187
188    /// Create a new stream error.
189    pub fn stream<M: Into<String>>(message: M) -> Self {
190        AixError::Stream {
191            message: message.into(),
192            source: None,
193        }
194    }
195
196    /// Create a new stream error with source.
197    pub fn stream_with_source<M: Into<String>, S: Into<String>>(
198        message: M,
199        source: S,
200    ) -> Self {
201        AixError::Stream {
202            message: message.into(),
203            source: Some(source.into()),
204        }
205    }
206
207    /// Create a new safety error.
208    pub fn safety<P: Into<String>, C: Into<String>, M: Into<String>>(
209        provider: P,
210        category: C,
211        message: M,
212    ) -> Self {
213        AixError::Safety {
214            provider: provider.into(),
215            category: category.into(),
216            message: message.into(),
217        }
218    }
219
220    /// Create a new auth error.
221    pub fn auth<P: Into<String>, M: Into<String>>(provider: P, message: M) -> Self {
222        AixError::Auth {
223            provider: provider.into(),
224            message: message.into(),
225        }
226    }
227
228    /// Create a new timeout error.
229    pub fn timeout<O: Into<String>>(operation: O, duration: Duration) -> Self {
230        AixError::Timeout {
231            operation: operation.into(),
232            duration,
233        }
234    }
235
236    /// Create a new other error.
237    pub fn other<M: Into<String>>(message: M) -> Self {
238        AixError::Other {
239            message: message.into(),
240            source: None,
241        }
242    }
243
244    /// Create a new other error with source.
245    pub fn other_with_source<M: Into<String>, S: Into<String>>(
246        message: M,
247        source: S,
248    ) -> Self {
249        AixError::Other {
250            message: message.into(),
251            source: Some(source.into()),
252        }
253    }
254}
255
256impl fmt::Display for AixError {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        match self {
259            AixError::Transport { source, context } => {
260                write!(f, "Transport error in {}: {}", context, source)
261            }
262            AixError::Provider {
263                provider,
264                code,
265                message,
266                status,
267            } => {
268                write!(f, "Provider error from {}", provider)?;
269                if let Some(status) = status {
270                    write!(f, " (status {})", status)?;
271                }
272                if let Some(code) = code {
273                    write!(f, " (code: {})", code)?;
274                }
275                write!(f, ": {}", message)
276            }
277            AixError::RateLimit {
278                provider,
279                retry_after,
280                message,
281            } => {
282                write!(f, "Rate limit error from {}: {}", provider, message)?;
283                if let Some(retry_after) = retry_after {
284                    write!(f, " (retry after: {:?})", retry_after)?;
285                }
286                Ok(())
287            }
288            AixError::Serialization { source, context } => {
289                write!(f, "Serialization error in {}: {}", context, source)
290            }
291            AixError::Config { message } => {
292                write!(f, "Configuration error: {}", message)
293            }
294            AixError::Stream { message, source } => {
295                write!(f, "Stream error: {}", message)?;
296                if let Some(source) = source {
297                    write!(f, " (source: {})", source)?;
298                }
299                Ok(())
300            }
301            AixError::Safety {
302                provider,
303                category,
304                message,
305            } => {
306                write!(
307                    f,
308                    "Safety violation from {} (category: {}): {}",
309                    provider, category, message
310                )
311            }
312            AixError::Auth { provider, message } => {
313                write!(f, "Authentication error from {}: {}", provider, message)
314            }
315            AixError::Timeout { operation, duration } => {
316                write!(
317                    f,
318                    "Operation '{}' timed out after {:?}",
319                    operation, duration
320                )
321            }
322            AixError::Other { message, source } => {
323                write!(f, "Error: {}", message)?;
324                if let Some(source) = source {
325                    write!(f, " (source: {})", source)?;
326                }
327                Ok(())
328            }
329        }
330    }
331}
332
333impl StdError for AixError {}
334
335/// Result type alias for AIX operations.
336pub type AixResult<T> = Result<T, AixError>;
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    #[test]
343    fn test_error_retryability() {
344        assert!(AixError::transport("network error", "request").is_retryable());
345        assert!(AixError::provider_with_details("openai", "error", 500, "internal_error").is_retryable());
346        assert!(AixError::provider_with_details("openai", "error", 429, "rate_limit").is_retryable());
347        assert!(AixError::rate_limit("openai", "too many requests").is_retryable());
348        assert!(AixError::timeout("chat", Duration::from_secs(30)).is_retryable());
349        
350        assert!(!AixError::provider_with_details("openai", "error", 400, "bad_request").is_retryable());
351        assert!(!AixError::config("invalid api key").is_retryable());
352        assert!(!AixError::auth("openai", "unauthorized").is_retryable());
353        assert!(!AixError::safety("openai", "hate", "content flagged").is_retryable());
354    }
355
356    #[test]
357    fn test_error_display() {
358        let err = AixError::provider_with_details("openai", "invalid request", 400, "invalid_request");
359        assert_eq!(err.to_string(), "Provider error from openai (status 400) (code: invalid_request): invalid request");
360    }
361}