Skip to main content

openapi_to_rust/
http_error.rs

1//! HTTP client error types with comprehensive retry detection.
2//!
3//! This module provides error types for HTTP operations with built-in support for
4//! retry detection, error categorization, and detailed error information.
5//!
6//! # Overview
7//!
8//! The [`HttpError`] enum covers all possible failure modes for HTTP requests:
9//! - Network errors (connection failures, DNS issues)
10//! - HTTP errors (4xx client errors, 5xx server errors)
11//! - Serialization/deserialization errors
12//! - Authentication errors
13//! - Timeouts
14//! - Configuration errors
15//!
16//! # Retry Detection
17//!
18//! The error type includes built-in retry detection via [`HttpError::is_retryable()`]:
19//! - Network errors → retryable
20//! - Timeouts → retryable
21//! - 429 (rate limit) → retryable
22//! - 500, 502, 503, 504 (server errors) → retryable
23//! - All other errors → not retryable
24//!
25//! When using the generated HTTP client with retry middleware (reqwest-retry),
26//! retryable errors are automatically retried with exponential backoff.
27//!
28//! # Examples
29//!
30//! ## Basic Error Handling
31//!
32//! ```
33//! # use openapi_to_rust::http_error::HttpError;
34//! fn handle_api_error(error: HttpError) {
35//!     match error {
36//!         HttpError::Network(e) => {
37//!             eprintln!("Network error: {}", e);
38//!             // Will be retried automatically if retry is configured
39//!         }
40//!         HttpError::Http { status, message, .. } => {
41//!             match status {
42//!                 400 => eprintln!("Bad request: {}", message),
43//!                 401 => eprintln!("Unauthorized - check API key"),
44//!                 404 => eprintln!("Not found"),
45//!                 429 => eprintln!("Rate limited - will retry"),
46//!                 500..=599 => eprintln!("Server error: {}", message),
47//!                 _ => eprintln!("HTTP error {}: {}", status, message),
48//!             }
49//!         }
50//!         HttpError::Timeout => {
51//!             eprintln!("Request timeout - will retry");
52//!         }
53//!         e => {
54//!             eprintln!("Other error: {}", e);
55//!         }
56//!     }
57//! }
58//! ```
59//!
60//! ## Retry Detection
61//!
62//! ```
63//! # use openapi_to_rust::http_error::HttpError;
64//! fn classify_error(error: &HttpError) {
65//!     if error.is_retryable() {
66//!         println!("Retryable error: {}", error);
67//!         // If retry middleware is configured, this will be retried automatically
68//!     } else if error.is_client_error() {
69//!         println!("Client error (4xx): fix the request");
70//!     } else if error.is_server_error() {
71//!         println!("Server error (5xx): may be transient");
72//!     } else {
73//!         println!("Non-retryable error: {}", error);
74//!     }
75//! }
76//! ```
77//!
78//! ## Creating Errors
79//!
80//! ```
81//! use openapi_to_rust::http_error::HttpError;
82//!
83//! // Create HTTP error from status code
84//! let error = HttpError::from_status(404, "Resource not found", None);
85//!
86//! // Create serialization error
87//! let error = HttpError::serialization_error("invalid JSON");
88//!
89//! // Create deserialization error
90//! let error = HttpError::deserialization_error("unexpected field");
91//! ```
92//!
93//! # Integration with reqwest-retry
94//!
95//! When the generated HTTP client is configured with retry middleware,
96//! the retry logic automatically handles retryable errors:
97//!
98//! ```toml
99//! [http_client.retry]
100//! max_retries = 3
101//! initial_delay_ms = 500
102//! max_delay_ms = 16000
103//! ```
104//!
105//! The retry middleware uses exponential backoff and will retry:
106//! - Network errors (connection failures)
107//! - Timeouts
108//! - HTTP 429 (rate limit)
109//! - HTTP 500, 502, 503, 504 (server errors)
110//!
111//! # Error Categories
112//!
113//! Errors can be categorized using helper methods:
114//! - [`HttpError::is_retryable()`] - Should this error be retried?
115//! - [`HttpError::is_client_error()`] - Is this a 4xx error?
116//! - [`HttpError::is_server_error()`] - Is this a 5xx error?
117
118use thiserror::Error;
119
120/// HTTP client errors that can occur during API requests
121#[derive(Error, Debug)]
122pub enum HttpError {
123    /// Network or connection error
124    #[error("Network error: {0}")]
125    Network(#[from] reqwest::Error),
126
127    /// Request serialization error
128    #[error("Failed to serialize request: {0}")]
129    Serialization(String),
130
131    /// Response deserialization error
132    #[error("Failed to deserialize response: {0}")]
133    Deserialization(String),
134
135    /// HTTP error response (4xx, 5xx)
136    #[error("HTTP error {status}: {message}")]
137    Http {
138        status: u16,
139        message: String,
140        body: Option<String>,
141    },
142
143    /// Authentication error
144    #[error("Authentication error: {0}")]
145    Auth(String),
146
147    /// Request timeout
148    #[error("Request timeout")]
149    Timeout,
150
151    /// Invalid configuration
152    #[error("Configuration error: {0}")]
153    Config(String),
154
155    /// Generic error
156    #[error("{0}")]
157    Other(String),
158}
159
160impl HttpError {
161    /// Create an HTTP error from a status code and message
162    pub fn from_status(status: u16, message: impl Into<String>, body: Option<String>) -> Self {
163        Self::Http {
164            status,
165            message: message.into(),
166            body,
167        }
168    }
169
170    /// Create a serialization error
171    pub fn serialization_error(error: impl std::fmt::Display) -> Self {
172        Self::Serialization(error.to_string())
173    }
174
175    /// Create a deserialization error
176    pub fn deserialization_error(error: impl std::fmt::Display) -> Self {
177        Self::Deserialization(error.to_string())
178    }
179
180    /// Check if this is a client error (4xx)
181    pub fn is_client_error(&self) -> bool {
182        matches!(self, Self::Http { status, .. } if *status >= 400 && *status < 500)
183    }
184
185    /// Check if this is a server error (5xx)
186    pub fn is_server_error(&self) -> bool {
187        matches!(self, Self::Http { status, .. } if *status >= 500 && *status < 600)
188    }
189
190    /// Check if this error is retryable
191    pub fn is_retryable(&self) -> bool {
192        match self {
193            Self::Network(_) => true,
194            Self::Timeout => true,
195            Self::Http { status, .. } => {
196                // Retry on 429 (rate limit), 500, 502, 503, 504
197                matches!(status, 429 | 500 | 502 | 503 | 504)
198            }
199            _ => false,
200        }
201    }
202}
203
204/// Result type for HTTP operations
205pub type HttpResult<T> = Result<T, HttpError>;