openrouter_rs/error.rs
1//! # Error Handling
2//!
3//! This module provides comprehensive error types for the OpenRouter SDK.
4//! All errors implement the standard library's `Error` trait and can be
5//! used with any error handling framework.
6//!
7//! ## 🎯 Error Categories
8//!
9//! ### HTTP Request Errors
10//! - **`HttpRequest`**: Network-level failures (connectivity, timeouts)
11//! - **`Api`**: Normalized API failures from the OpenRouter API
12//!
13//! ### OpenRouter API Errors
14//! - **`ApiErrorContext`**: status/code/message/request-id/metadata envelope
15//! - **`ApiErrorKind::Moderation`**: Content moderation violations
16//! - **`ApiErrorKind::Provider`**: Provider-specific upstream failures
17//!
18//! ### Validation Errors
19//! - **`ConfigError`**: Invalid SDK configuration or request/tool validation issues
20//! - **`KeyNotConfigured`**: Missing or invalid API keys
21//!
22//! ### Data Processing Errors
23//! - **`UninitializedFieldError`**: Builder pattern validation failures
24//! - **`Serialization`**: JSON serialization/deserialization errors
25//!
26//! ### System Errors
27//! - **`Io`**: File system and I/O operations
28//! - **`Unknown`**: Unexpected errors
29//!
30//! ## 🚀 Usage Examples
31//!
32//! ### Basic Error Handling
33//!
34//! ```no_run
35//! use openrouter_rs::{OpenRouterClient, error::OpenRouterError};
36//!
37//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
38//! let client = OpenRouterClient::builder()
39//! .api_key("invalid_key")
40//! .build()?;
41//!
42//! match client.models().list().await {
43//! Ok(models) => println!("Found {} models", models.len()),
44//! Err(OpenRouterError::Api(api_error)) => {
45//! eprintln!("API error {}: {}", api_error.status, api_error.message);
46//! }
47//! Err(OpenRouterError::HttpRequest(e)) => {
48//! eprintln!("Network error: {}", e);
49//! }
50//! Err(e) => eprintln!("Other error: {}", e),
51//! }
52//! # Ok(())
53//! # }
54//! ```
55//!
56//! ### Rate Limiting Handling
57//!
58//! ```no_run
59//! use openrouter_rs::error::OpenRouterError;
60//! use openrouter_rs::{OpenRouterClient, api::chat::{ChatCompletionRequest, Message}, types::Role};
61//!
62//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
63//! let client = OpenRouterClient::builder()
64//! .api_key("your_api_key")
65//! .build()?;
66//! let request = ChatCompletionRequest::builder()
67//! .model("google/gemini-2.5-flash")
68//! .messages(vec![Message::new(Role::User, "Hello!")])
69//! .build()?;
70//!
71//! match client.chat().create(&request).await {
72//! Err(OpenRouterError::Api(api_error)) if api_error.is_retryable() => {
73//! println!("Rate limited, retrying after delay...");
74//! tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
75//! // Retry logic here
76//! }
77//! Ok(response) => println!("Success!"),
78//! Err(e) => return Err(e.into()),
79//! }
80//! # Ok(())
81//! # }
82//! ```
83//!
84//! ### Validation Error Handling
85//!
86//! ```rust
87//! use openrouter_rs::error::OpenRouterError;
88//!
89//! fn describe(error: OpenRouterError) {
90//! match error {
91//! OpenRouterError::ConfigError(msg) => {
92//! eprintln!("Invalid SDK configuration: {}", msg);
93//! }
94//! other => eprintln!("Unexpected error: {}", other),
95//! }
96//! }
97//! ```
98//!
99//! ## 🔄 Error Conversion
100//!
101//! The SDK automatically converts common error types:
102//!
103//! - transport client errors → `OpenRouterError::HttpRequest`
104//! - `serde_json::Error` → `OpenRouterError::Serialization`
105//! - `std::io::Error` → `OpenRouterError::Io`
106//! - `derive_builder::UninitializedFieldError` → `OpenRouterError::UninitializedFieldError`
107
108use http::StatusCode;
109use serde_json::Value;
110use thiserror::Error;
111
112/// Stable, backend-neutral request error wrapper used by [`OpenRouterError::HttpRequest`].
113#[derive(Debug, Clone, PartialEq, Eq, Error)]
114#[error("{message}")]
115pub struct HttpRequestError {
116 message: String,
117}
118
119impl HttpRequestError {
120 pub fn new(message: impl Into<String>) -> Self {
121 Self {
122 message: message.into(),
123 }
124 }
125
126 pub fn message(&self) -> &str {
127 &self.message
128 }
129}
130
131/// Normalized API error category.
132#[derive(Debug, Clone)]
133pub enum ApiErrorKind {
134 /// Generic API error payload without specialized metadata.
135 Generic,
136 /// Moderation rejection with structured moderation metadata.
137 Moderation {
138 reasons: Vec<String>,
139 flagged_input: String,
140 provider_name: String,
141 model_slug: String,
142 },
143 /// Provider-side failure metadata.
144 Provider { provider_name: String, raw: Value },
145}
146
147/// Normalized API error payload used across all endpoint modules.
148#[derive(Debug, Clone)]
149pub struct ApiErrorContext {
150 pub status: StatusCode,
151 pub api_code: Option<i64>,
152 pub message: String,
153 pub request_id: Option<String>,
154 pub metadata: Option<Value>,
155 pub kind: ApiErrorKind,
156}
157
158impl ApiErrorContext {
159 /// Returns true if the request is typically retryable.
160 pub fn is_retryable(&self) -> bool {
161 matches!(
162 self.status,
163 StatusCode::REQUEST_TIMEOUT
164 | StatusCode::TOO_MANY_REQUESTS
165 | StatusCode::INTERNAL_SERVER_ERROR
166 | StatusCode::BAD_GATEWAY
167 | StatusCode::SERVICE_UNAVAILABLE
168 | StatusCode::GATEWAY_TIMEOUT
169 )
170 }
171
172 pub fn is_client_error(&self) -> bool {
173 self.status.is_client_error()
174 }
175
176 pub fn is_server_error(&self) -> bool {
177 self.status.is_server_error()
178 }
179}
180
181impl std::fmt::Display for ApiErrorContext {
182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183 if let Some(api_code) = self.api_code {
184 write!(
185 f,
186 "API error {} (api_code={}): {}",
187 self.status, api_code, self.message
188 )?;
189 } else {
190 write!(f, "API error {}: {}", self.status, self.message)?;
191 }
192
193 if let Some(request_id) = &self.request_id {
194 write!(f, " [request_id={}]", request_id)?;
195 }
196
197 Ok(())
198 }
199}
200
201/// Comprehensive error type for OpenRouter SDK operations
202///
203/// This enum covers all possible error conditions that can occur when
204/// using the OpenRouter SDK, from network issues to API-specific errors.
205/// All variants implement `std::error::Error` and provide detailed
206/// context information.
207///
208/// # Examples
209///
210/// ```rust
211/// use openrouter_rs::error::{ApiErrorKind, OpenRouterError};
212///
213/// // Pattern matching on specific error types
214/// fn handle_error(error: OpenRouterError) {
215/// match error {
216/// OpenRouterError::Api(api_error) if api_error.status == http::StatusCode::UNAUTHORIZED => {
217/// println!("Check your API key");
218/// }
219/// OpenRouterError::Api(api_error) => match &api_error.kind {
220/// ApiErrorKind::Moderation { reasons, .. } => {
221/// println!("Content flagged for: {:?}", reasons);
222/// }
223/// _ => println!("Other API error: {}", api_error),
224/// },
225/// OpenRouterError::ConfigError(msg) => {
226/// println!("Configuration issue: {}", msg);
227/// }
228/// _ => println!("Other error: {}", error),
229/// }
230/// }
231/// ```
232#[derive(Error, Debug)]
233pub enum OpenRouterError {
234 // HTTP request errors
235 #[error("HTTP request failed: {0}")]
236 HttpRequest(HttpRequestError),
237
238 // API response errors
239 #[error("{0}")]
240 Api(Box<ApiErrorContext>),
241
242 // Configuration errors
243 #[error("Config error: {0}")]
244 ConfigError(String),
245
246 #[error("API key not configured")]
247 KeyNotConfigured,
248
249 // Data processing errors
250 #[error("Uninitialized field error: {0}")]
251 UninitializedFieldError(#[from] derive_builder::UninitializedFieldError),
252
253 #[error("Serialization error: {0}")]
254 Serialization(#[from] serde_json::Error),
255
256 // System IO errors
257 #[error("IO error: {0}")]
258 Io(#[from] std::io::Error),
259
260 // Uncategorized errors
261 #[error("Unknown error: {0}")]
262 Unknown(String),
263}
264
265impl From<reqwest::Error> for OpenRouterError {
266 fn from(err: reqwest::Error) -> Self {
267 OpenRouterError::HttpRequest(HttpRequestError::new(err.to_string()))
268 }
269}