odos_sdk/
error.rs

1use alloy_primitives::hex;
2use reqwest::StatusCode;
3use thiserror::Error;
4
5use crate::OdosChainError;
6
7/// Result type alias for Odos SDK operations
8pub type Result<T> = std::result::Result<T, OdosError>;
9
10/// Comprehensive error types for the Odos SDK
11///
12/// This enum provides detailed error types for different failure scenarios,
13/// allowing users to handle specific error conditions appropriately.
14///
15/// ## Error Categories
16///
17/// - **Network Errors**: HTTP, timeout, and connectivity issues
18/// - **API Errors**: Responses from the Odos service indicating various failures
19/// - **Input Errors**: Invalid parameters or missing required data
20/// - **System Errors**: Circuit breaker, rate limiting, and internal failures
21///
22/// ## Retryable Errors
23///
24/// Some error types are marked as retryable (see [`OdosError::is_retryable`]):
25/// - Timeout errors
26/// - Certain HTTP errors (5xx status codes, connection issues)
27/// - Rate limiting errors
28/// - Some API errors (server errors, rate limits)
29///
30/// ## Examples
31///
32/// ```rust
33/// use odos_sdk::{OdosError, Result};
34/// use reqwest::StatusCode;
35///
36/// // Create different error types
37/// let api_error = OdosError::api_error(StatusCode::BAD_REQUEST, "Invalid input".to_string());
38/// let timeout_error = OdosError::timeout_error("Request timed out");
39/// let circuit_breaker_error = OdosError::circuit_breaker_error("Circuit breaker is open");
40///
41/// // Check if errors are retryable
42/// assert!(!api_error.is_retryable());  // 4xx errors are not retryable
43/// assert!(timeout_error.is_retryable()); // Timeouts are retryable
44/// assert!(!circuit_breaker_error.is_retryable()); // Circuit breaker prevents retries
45///
46/// // Get error categories for metrics
47/// assert_eq!(api_error.category(), "api");
48/// assert_eq!(timeout_error.category(), "timeout");
49/// assert_eq!(circuit_breaker_error.category(), "circuit_breaker");
50/// ```
51#[derive(Error, Debug)]
52pub enum OdosError {
53    /// HTTP request errors
54    #[error("HTTP request failed: {0}")]
55    Http(#[from] reqwest::Error),
56
57    /// API errors returned by the Odos service
58    #[error("Odos API error (status: {status}): {message}")]
59    Api { status: StatusCode, message: String },
60
61    /// JSON serialization/deserialization errors
62    #[error("JSON processing error: {0}")]
63    Json(#[from] serde_json::Error),
64
65    /// Hex decoding errors
66    #[error("Hex decoding error: {0}")]
67    Hex(#[from] hex::FromHexError),
68
69    /// Invalid input parameters
70    #[error("Invalid input: {0}")]
71    InvalidInput(String),
72
73    /// Missing required data
74    #[error("Missing required data: {0}")]
75    MissingData(String),
76
77    /// Chain not supported
78    #[error("Chain not supported: {chain_id}")]
79    UnsupportedChain { chain_id: u64 },
80
81    /// Contract interaction errors
82    #[error("Contract error: {0}")]
83    Contract(String),
84
85    /// Transaction assembly errors
86    #[error("Transaction assembly failed: {0}")]
87    TransactionAssembly(String),
88
89    /// Quote request errors
90    #[error("Quote request failed: {0}")]
91    QuoteRequest(String),
92
93    /// Configuration errors
94    #[error("Configuration error: {0}")]
95    Configuration(String),
96
97    /// Timeout errors
98    #[error("Operation timed out: {0}")]
99    Timeout(String),
100
101    /// Rate limit exceeded
102    #[error("Rate limit exceeded: {0}")]
103    RateLimit(String),
104
105    /// Circuit breaker is open
106    ///
107    /// This error occurs when the circuit breaker has detected too many failures
108    /// and has opened to prevent further requests. The circuit breaker will
109    /// automatically transition to half-open state after a timeout period,
110    /// allowing a limited number of requests to test if the service has recovered.
111    ///
112    /// ## When this occurs:
113    /// - When the failure count exceeds the configured threshold
114    /// - During the open state of the circuit breaker
115    /// - Before the reset timeout has elapsed
116    ///
117    /// ## How to handle:
118    /// - Wait for the circuit breaker to reset (typically 60 seconds by default)
119    /// - Check the circuit breaker status using [`OdosSorV2::circuit_breaker_status`]
120    /// - Implement exponential backoff in your retry logic
121    /// - Consider using alternative service endpoints if available
122    ///
123    /// This error is **not retryable** as the circuit breaker is specifically
124    /// designed to prevent additional load on a failing service.
125    #[error("Circuit breaker is open: {0}")]
126    CircuitBreakerOpen(String),
127
128    /// Generic internal error
129    #[error("Internal error: {0}")]
130    Internal(String),
131}
132
133impl OdosError {
134    /// Create an API error from response
135    pub fn api_error(status: StatusCode, message: String) -> Self {
136        Self::Api { status, message }
137    }
138
139    /// Create an invalid input error
140    pub fn invalid_input(message: impl Into<String>) -> Self {
141        Self::InvalidInput(message.into())
142    }
143
144    /// Create a missing data error
145    pub fn missing_data(message: impl Into<String>) -> Self {
146        Self::MissingData(message.into())
147    }
148
149    /// Create an unsupported chain error
150    pub fn unsupported_chain(chain_id: u64) -> Self {
151        Self::UnsupportedChain { chain_id }
152    }
153
154    /// Create a contract error
155    pub fn contract_error(message: impl Into<String>) -> Self {
156        Self::Contract(message.into())
157    }
158
159    /// Create a transaction assembly error
160    pub fn transaction_assembly_error(message: impl Into<String>) -> Self {
161        Self::TransactionAssembly(message.into())
162    }
163
164    /// Create a quote request error
165    pub fn quote_request_error(message: impl Into<String>) -> Self {
166        Self::QuoteRequest(message.into())
167    }
168
169    /// Create a configuration error
170    pub fn configuration_error(message: impl Into<String>) -> Self {
171        Self::Configuration(message.into())
172    }
173
174    /// Create a timeout error
175    pub fn timeout_error(message: impl Into<String>) -> Self {
176        Self::Timeout(message.into())
177    }
178
179    /// Create a rate limit error
180    pub fn rate_limit_error(message: impl Into<String>) -> Self {
181        Self::RateLimit(message.into())
182    }
183
184    /// Create a circuit breaker error
185    pub fn circuit_breaker_error(message: impl Into<String>) -> Self {
186        Self::CircuitBreakerOpen(message.into())
187    }
188
189    /// Create an internal error
190    pub fn internal_error(message: impl Into<String>) -> Self {
191        Self::Internal(message.into())
192    }
193
194    /// Check if the error is retryable
195    pub fn is_retryable(&self) -> bool {
196        match self {
197            // HTTP errors that are typically retryable
198            OdosError::Http(err) => {
199                // Timeout, connection errors, etc.
200                err.is_timeout() || err.is_connect() || err.is_request()
201            }
202            // API errors that might be retryable
203            OdosError::Api { status, .. } => {
204                matches!(
205                    *status,
206                    StatusCode::TOO_MANY_REQUESTS
207                        | StatusCode::INTERNAL_SERVER_ERROR
208                        | StatusCode::BAD_GATEWAY
209                        | StatusCode::SERVICE_UNAVAILABLE
210                        | StatusCode::GATEWAY_TIMEOUT
211                )
212            }
213            // Other retryable errors
214            OdosError::Timeout(_) => true,
215            OdosError::RateLimit(_) => true,
216            // Non-retryable errors
217            OdosError::Json(_)
218            | OdosError::Hex(_)
219            | OdosError::InvalidInput(_)
220            | OdosError::MissingData(_)
221            | OdosError::UnsupportedChain { .. }
222            | OdosError::Contract(_)
223            | OdosError::TransactionAssembly(_)
224            | OdosError::QuoteRequest(_)
225            | OdosError::Configuration(_)
226            | OdosError::CircuitBreakerOpen(_)
227            | OdosError::Internal(_) => false,
228        }
229    }
230
231    /// Get the error category for metrics
232    pub fn category(&self) -> &'static str {
233        match self {
234            OdosError::Http(_) => "http",
235            OdosError::Api { .. } => "api",
236            OdosError::Json(_) => "json",
237            OdosError::Hex(_) => "hex",
238            OdosError::InvalidInput(_) => "invalid_input",
239            OdosError::MissingData(_) => "missing_data",
240            OdosError::UnsupportedChain { .. } => "unsupported_chain",
241            OdosError::Contract(_) => "contract",
242            OdosError::TransactionAssembly(_) => "transaction_assembly",
243            OdosError::QuoteRequest(_) => "quote_request",
244            OdosError::Configuration(_) => "configuration",
245            OdosError::Timeout(_) => "timeout",
246            OdosError::RateLimit(_) => "rate_limit",
247            OdosError::CircuitBreakerOpen(_) => "circuit_breaker",
248            OdosError::Internal(_) => "internal",
249        }
250    }
251}
252
253// Compatibility with anyhow for gradual migration
254impl From<anyhow::Error> for OdosError {
255    fn from(err: anyhow::Error) -> Self {
256        Self::Internal(err.to_string())
257    }
258}
259
260// Convert chain errors to appropriate error types
261impl From<OdosChainError> for OdosError {
262    fn from(err: OdosChainError) -> Self {
263        match err {
264            OdosChainError::V2NotAvailable { chain } => {
265                Self::contract_error(format!("V2 router not available on chain: {chain}"))
266            }
267            OdosChainError::V3NotAvailable { chain } => {
268                Self::contract_error(format!("V3 router not available on chain: {chain}"))
269            }
270            OdosChainError::UnsupportedChain { chain } => {
271                Self::contract_error(format!("Unsupported chain: {chain}"))
272            }
273            OdosChainError::InvalidAddress { address } => {
274                Self::invalid_input(format!("Invalid address format: {address}"))
275            }
276        }
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use reqwest::StatusCode;
284
285    #[test]
286    fn test_retryable_errors() {
287        // HTTP timeout should be retryable
288        let timeout_err = OdosError::timeout_error("Request timed out");
289        assert!(timeout_err.is_retryable());
290
291        // API 500 error should be retryable
292        let api_err = OdosError::api_error(
293            StatusCode::INTERNAL_SERVER_ERROR,
294            "Server error".to_string(),
295        );
296        assert!(api_err.is_retryable());
297
298        // Invalid input should not be retryable
299        let invalid_err = OdosError::invalid_input("Bad parameter");
300        assert!(!invalid_err.is_retryable());
301
302        // Rate limit should be retryable
303        let rate_limit_err = OdosError::rate_limit_error("Too many requests");
304        assert!(rate_limit_err.is_retryable());
305    }
306
307    #[test]
308    fn test_error_categories() {
309        let api_err = OdosError::api_error(StatusCode::BAD_REQUEST, "Bad request".to_string());
310        assert_eq!(api_err.category(), "api");
311
312        let timeout_err = OdosError::timeout_error("Timeout");
313        assert_eq!(timeout_err.category(), "timeout");
314
315        let invalid_err = OdosError::invalid_input("Invalid");
316        assert_eq!(invalid_err.category(), "invalid_input");
317    }
318}