odos_sdk/
error.rs

1use std::time::Duration;
2
3use alloy_primitives::hex;
4use reqwest::StatusCode;
5use thiserror::Error;
6
7use crate::{
8    error_code::{OdosErrorCode, TraceId},
9    OdosChainError,
10};
11
12/// Result type alias for Odos SDK operations
13pub type Result<T> = std::result::Result<T, OdosError>;
14
15/// Comprehensive error types for the Odos SDK
16///
17/// This enum provides detailed error types for different failure scenarios,
18/// allowing users to handle specific error conditions appropriately.
19///
20/// ## Error Categories
21///
22/// - **Network Errors**: HTTP, timeout, and connectivity issues
23/// - **API Errors**: Responses from the Odos service indicating various failures
24/// - **Input Errors**: Invalid parameters or missing required data
25/// - **System Errors**: Rate limiting and internal failures
26///
27/// ## Retryable Errors
28///
29/// Some error types are marked as retryable (see [`OdosError::is_retryable`]):
30/// - Timeout errors
31/// - Certain HTTP errors (5xx status codes, connection issues)
32/// - Some API errors (server errors)
33///
34/// **Note**: Rate limiting errors (429) are NOT retryable. Applications must handle
35/// rate limits globally with proper coordination rather than retrying individual requests.
36///
37/// ## Examples
38///
39/// ```rust
40/// use odos_sdk::{OdosError, Result};
41/// use reqwest::StatusCode;
42///
43/// // Create different error types
44/// let api_error = OdosError::api_error(StatusCode::BAD_REQUEST, "Invalid input".to_string());
45/// let timeout_error = OdosError::timeout_error("Request timed out");
46/// let rate_limit_error = OdosError::rate_limit_error("Too many requests");
47///
48/// // Check if errors are retryable
49/// assert!(!api_error.is_retryable());  // 4xx errors are not retryable
50/// assert!(timeout_error.is_retryable()); // Timeouts are retryable
51/// assert!(!rate_limit_error.is_retryable()); // Rate limits are NOT retryable
52///
53/// // Get error categories for metrics
54/// assert_eq!(api_error.category(), "api");
55/// assert_eq!(timeout_error.category(), "timeout");
56/// assert_eq!(rate_limit_error.category(), "rate_limit");
57/// ```
58#[derive(Error, Debug)]
59pub enum OdosError {
60    /// HTTP request errors
61    #[error("HTTP request failed: {0}")]
62    Http(#[from] reqwest::Error),
63
64    /// API errors returned by the Odos service
65    #[error("Odos API error (status: {status}): {message}{}", trace_id.map(|tid| format!(" [trace: {}]", tid)).unwrap_or_default())]
66    Api {
67        status: StatusCode,
68        message: String,
69        code: OdosErrorCode,
70        trace_id: Option<TraceId>,
71    },
72
73    /// JSON serialization/deserialization errors
74    #[error("JSON processing error: {0}")]
75    Json(#[from] serde_json::Error),
76
77    /// Hex decoding errors
78    #[error("Hex decoding error: {0}")]
79    Hex(#[from] hex::FromHexError),
80
81    /// Invalid input parameters
82    #[error("Invalid input: {0}")]
83    InvalidInput(String),
84
85    /// Missing required data
86    #[error("Missing required data: {0}")]
87    MissingData(String),
88
89    /// Chain not supported
90    #[error("Chain not supported: {chain_id}")]
91    UnsupportedChain { chain_id: u64 },
92
93    /// Contract interaction errors
94    #[error("Contract error: {0}")]
95    Contract(String),
96
97    /// Transaction assembly errors
98    #[error("Transaction assembly failed: {0}")]
99    TransactionAssembly(String),
100
101    /// Quote request errors
102    #[error("Quote request failed: {0}")]
103    QuoteRequest(String),
104
105    /// Configuration errors
106    #[error("Configuration error: {0}")]
107    Configuration(String),
108
109    /// Timeout errors
110    #[error("Operation timed out: {0}")]
111    Timeout(String),
112
113    /// Rate limit exceeded
114    ///
115    /// Contains an optional `retry_after` duration from the Retry-After HTTP header,
116    /// the error code from the Odos API, and an optional `trace_id` for debugging.
117    #[error("Rate limit exceeded: {message}{}", trace_id.map(|tid| format!(" [trace: {}]", tid)).unwrap_or_default())]
118    RateLimit {
119        message: String,
120        retry_after: Option<Duration>,
121        code: OdosErrorCode,
122        trace_id: Option<TraceId>,
123    },
124
125    /// Generic internal error
126    #[error("Internal error: {0}")]
127    Internal(String),
128}
129
130impl OdosError {
131    /// Create an API error from response (without error code or trace ID)
132    pub fn api_error(status: StatusCode, message: String) -> Self {
133        Self::Api {
134            status,
135            message,
136            code: OdosErrorCode::Unknown(0),
137            trace_id: None,
138        }
139    }
140
141    /// Create an API error with error code and trace ID
142    pub fn api_error_with_code(
143        status: StatusCode,
144        message: String,
145        code: OdosErrorCode,
146        trace_id: Option<TraceId>,
147    ) -> Self {
148        Self::Api {
149            status,
150            message,
151            code,
152            trace_id,
153        }
154    }
155
156    /// Create an invalid input error
157    pub fn invalid_input(message: impl Into<String>) -> Self {
158        Self::InvalidInput(message.into())
159    }
160
161    /// Create a missing data error
162    pub fn missing_data(message: impl Into<String>) -> Self {
163        Self::MissingData(message.into())
164    }
165
166    /// Create an unsupported chain error
167    pub fn unsupported_chain(chain_id: u64) -> Self {
168        Self::UnsupportedChain { chain_id }
169    }
170
171    /// Create a contract error
172    pub fn contract_error(message: impl Into<String>) -> Self {
173        Self::Contract(message.into())
174    }
175
176    /// Create a transaction assembly error
177    pub fn transaction_assembly_error(message: impl Into<String>) -> Self {
178        Self::TransactionAssembly(message.into())
179    }
180
181    /// Create a quote request error
182    pub fn quote_request_error(message: impl Into<String>) -> Self {
183        Self::QuoteRequest(message.into())
184    }
185
186    /// Create a configuration error
187    pub fn configuration_error(message: impl Into<String>) -> Self {
188        Self::Configuration(message.into())
189    }
190
191    /// Create a timeout error
192    pub fn timeout_error(message: impl Into<String>) -> Self {
193        Self::Timeout(message.into())
194    }
195
196    /// Create a rate limit error with optional retry-after duration
197    pub fn rate_limit_error(message: impl Into<String>) -> Self {
198        Self::RateLimit {
199            message: message.into(),
200            retry_after: None,
201            code: OdosErrorCode::Unknown(429),
202            trace_id: None,
203        }
204    }
205
206    /// Create a rate limit error with retry-after duration
207    pub fn rate_limit_error_with_retry_after(
208        message: impl Into<String>,
209        retry_after: Option<Duration>,
210    ) -> Self {
211        Self::RateLimit {
212            message: message.into(),
213            retry_after,
214            code: OdosErrorCode::Unknown(429),
215            trace_id: None,
216        }
217    }
218
219    /// Create a rate limit error with all fields
220    pub fn rate_limit_error_with_retry_after_and_trace(
221        message: impl Into<String>,
222        retry_after: Option<Duration>,
223        code: OdosErrorCode,
224        trace_id: Option<TraceId>,
225    ) -> Self {
226        Self::RateLimit {
227            message: message.into(),
228            retry_after,
229            code,
230            trace_id,
231        }
232    }
233
234    /// Create an internal error
235    pub fn internal_error(message: impl Into<String>) -> Self {
236        Self::Internal(message.into())
237    }
238
239    /// Check if the error is retryable
240    ///
241    /// For API errors, the retryability is determined by the error code.
242    /// For Unknown error codes, falls back to HTTP status code checking.
243    pub fn is_retryable(&self) -> bool {
244        match self {
245            // HTTP errors that are typically retryable
246            OdosError::Http(err) => {
247                // Timeout, connection errors, etc.
248                err.is_timeout() || err.is_connect() || err.is_request()
249            }
250            // API errors - use error code retryability logic
251            OdosError::Api { status, code, .. } => {
252                // If we have a known error code, use its retryability logic
253                if matches!(code, OdosErrorCode::Unknown(_)) {
254                    // Fall back to status code checking for unknown error codes
255                    matches!(
256                        *status,
257                        StatusCode::INTERNAL_SERVER_ERROR
258                            | StatusCode::BAD_GATEWAY
259                            | StatusCode::SERVICE_UNAVAILABLE
260                            | StatusCode::GATEWAY_TIMEOUT
261                    )
262                } else {
263                    code.is_retryable()
264                }
265            }
266            // Other retryable errors
267            OdosError::Timeout(_) => true,
268            // NEVER retry rate limits - application must handle globally
269            OdosError::RateLimit { .. } => false,
270            // Non-retryable errors
271            OdosError::Json(_)
272            | OdosError::Hex(_)
273            | OdosError::InvalidInput(_)
274            | OdosError::MissingData(_)
275            | OdosError::UnsupportedChain { .. }
276            | OdosError::Contract(_)
277            | OdosError::TransactionAssembly(_)
278            | OdosError::QuoteRequest(_)
279            | OdosError::Configuration(_)
280            | OdosError::Internal(_) => false,
281        }
282    }
283
284    /// Check if this error is specifically a rate limit error
285    ///
286    /// This is a convenience method to help with error handling patterns.
287    /// Rate limit errors indicate that the Odos API has rejected the request
288    /// due to too many requests being made in a given time period.
289    ///
290    /// # Examples
291    ///
292    /// ```rust
293    /// use odos_sdk::{OdosError, OdosSor, QuoteRequest};
294    ///
295    /// # async fn example(client: &OdosSor, request: &QuoteRequest) {
296    /// match client.get_swap_quote(request).await {
297    ///     Ok(quote) => { /* handle quote */ }
298    ///     Err(e) if e.is_rate_limit() => {
299    ///         // Specific handling for rate limits
300    ///         eprintln!("Rate limited - consider backing off");
301    ///     }
302    ///     Err(e) => { /* handle other errors */ }
303    /// }
304    /// # }
305    /// ```
306    pub fn is_rate_limit(&self) -> bool {
307        matches!(self, OdosError::RateLimit { .. })
308    }
309
310    /// Get the retry-after duration for rate limit errors
311    ///
312    /// Returns `Some(duration)` if this is a rate limit error with a retry-after value,
313    /// `None` otherwise.
314    ///
315    /// # Examples
316    ///
317    /// ```rust
318    /// use odos_sdk::OdosError;
319    /// use std::time::Duration;
320    ///
321    /// let error = OdosError::rate_limit_error_with_retry_after(
322    ///     "Rate limited",
323    ///     Some(Duration::from_secs(30))
324    /// );
325    ///
326    /// if let Some(duration) = error.retry_after() {
327    ///     println!("Retry after {} seconds", duration.as_secs());
328    /// }
329    /// ```
330    pub fn retry_after(&self) -> Option<Duration> {
331        match self {
332            OdosError::RateLimit { retry_after, .. } => *retry_after,
333            _ => None,
334        }
335    }
336
337    /// Get the Odos API error code if available
338    ///
339    /// Returns the strongly-typed error code for API and rate limit errors,
340    /// or `None` for other error types.
341    ///
342    /// # Examples
343    ///
344    /// ```rust
345    /// use odos_sdk::{OdosError, error_code::OdosErrorCode};
346    /// use reqwest::StatusCode;
347    ///
348    /// let error = OdosError::api_error_with_code(
349    ///     StatusCode::BAD_REQUEST,
350    ///     "Invalid chain ID".to_string(),
351    ///     OdosErrorCode::from(4001),
352    ///     None
353    /// );
354    ///
355    /// if let Some(code) = error.error_code() {
356    ///     if code.is_invalid_chain_id() {
357    ///         println!("Chain ID validation failed");
358    ///     }
359    /// }
360    /// ```
361    pub fn error_code(&self) -> Option<&OdosErrorCode> {
362        match self {
363            OdosError::Api { code, .. } => Some(code),
364            OdosError::RateLimit { code, .. } => Some(code),
365            _ => None,
366        }
367    }
368
369    /// Get the Odos API trace ID if available
370    ///
371    /// Returns the trace ID for debugging API errors, or `None` for other error types
372    /// or if the trace ID was not included in the API response.
373    ///
374    /// # Examples
375    ///
376    /// ```rust
377    /// use odos_sdk::OdosError;
378    ///
379    /// # fn handle_error(error: &OdosError) {
380    /// if let Some(trace_id) = error.trace_id() {
381    ///     eprintln!("Error trace ID for support: {}", trace_id);
382    /// }
383    /// # }
384    /// ```
385    pub fn trace_id(&self) -> Option<TraceId> {
386        match self {
387            OdosError::Api { trace_id, .. } => *trace_id,
388            OdosError::RateLimit { trace_id, .. } => *trace_id,
389            _ => None,
390        }
391    }
392
393    /// Get the error category for metrics
394    pub fn category(&self) -> &'static str {
395        match self {
396            OdosError::Http(_) => "http",
397            OdosError::Api { .. } => "api",
398            OdosError::Json(_) => "json",
399            OdosError::Hex(_) => "hex",
400            OdosError::InvalidInput(_) => "invalid_input",
401            OdosError::MissingData(_) => "missing_data",
402            OdosError::UnsupportedChain { .. } => "unsupported_chain",
403            OdosError::Contract(_) => "contract",
404            OdosError::TransactionAssembly(_) => "transaction_assembly",
405            OdosError::QuoteRequest(_) => "quote_request",
406            OdosError::Configuration(_) => "configuration",
407            OdosError::Timeout(_) => "timeout",
408            OdosError::RateLimit { .. } => "rate_limit",
409            OdosError::Internal(_) => "internal",
410        }
411    }
412}
413
414// Convert chain errors to appropriate error types
415impl From<OdosChainError> for OdosError {
416    fn from(err: OdosChainError) -> Self {
417        match err {
418            OdosChainError::LimitOrderNotAvailable { chain } => Self::contract_error(format!(
419                "Limit Order router not available on chain: {chain}"
420            )),
421            OdosChainError::V2NotAvailable { chain } => {
422                Self::contract_error(format!("V2 router not available on chain: {chain}"))
423            }
424            OdosChainError::V3NotAvailable { chain } => {
425                Self::contract_error(format!("V3 router not available on chain: {chain}"))
426            }
427            OdosChainError::UnsupportedChain { chain } => {
428                Self::contract_error(format!("Unsupported chain: {chain}"))
429            }
430            OdosChainError::InvalidAddress { address } => {
431                Self::invalid_input(format!("Invalid address format: {address}"))
432            }
433        }
434    }
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440    use reqwest::StatusCode;
441
442    #[test]
443    fn test_retryable_errors() {
444        // HTTP timeout should be retryable
445        let timeout_err = OdosError::timeout_error("Request timed out");
446        assert!(timeout_err.is_retryable());
447
448        // API 500 error should be retryable
449        let api_err = OdosError::api_error(
450            StatusCode::INTERNAL_SERVER_ERROR,
451            "Server error".to_string(),
452        );
453        assert!(api_err.is_retryable());
454
455        // Invalid input should not be retryable
456        let invalid_err = OdosError::invalid_input("Bad parameter");
457        assert!(!invalid_err.is_retryable());
458
459        // Rate limit should NOT be retryable (application must handle globally)
460        let rate_limit_err = OdosError::rate_limit_error("Too many requests");
461        assert!(!rate_limit_err.is_retryable());
462    }
463
464    #[test]
465    fn test_error_categories() {
466        let api_err = OdosError::api_error(StatusCode::BAD_REQUEST, "Bad request".to_string());
467        assert_eq!(api_err.category(), "api");
468
469        let timeout_err = OdosError::timeout_error("Timeout");
470        assert_eq!(timeout_err.category(), "timeout");
471
472        let invalid_err = OdosError::invalid_input("Invalid");
473        assert_eq!(invalid_err.category(), "invalid_input");
474    }
475}