Skip to main content

polymarket_sdk/
error.rs

1//! Error types for the Polymarket SDK.
2
3use std::time::Duration;
4use thiserror::Error;
5
6/// Top-level error type for all SDK operations.
7#[derive(Debug, Error)]
8pub enum PolymarketError {
9    /// HTTP transport error (network, TLS, timeout, etc.).
10    #[error("HTTP error: {0}")]
11    Http(#[from] reqwest::Error),
12
13    /// JSON serialization / deserialization error.
14    #[error("JSON error: {0}")]
15    Json(#[from] serde_json::Error),
16
17    /// API returned a non-success status code.
18    #[error("API error (HTTP {status}): {message}")]
19    Api {
20        /// HTTP status code.
21        status: u16,
22        /// Error message from the API body.
23        message: String,
24    },
25
26    /// Rate limit hit (HTTP 429). Contains the retry-after duration if provided.
27    #[error("Rate limited: retry after {retry_after:?}")]
28    RateLimit {
29        /// Suggested wait before retrying.
30        retry_after: Duration,
31    },
32
33    /// Order was rejected by the exchange.
34    #[error("Order rejected: {reason}")]
35    OrderRejected {
36        /// Human-readable rejection reason.
37        reason: String,
38    },
39
40    /// EIP-712 signing error.
41    #[error("Signing error: {0}")]
42    Signing(String),
43
44    /// Authentication error (missing or invalid credentials).
45    #[error("Auth error: {0}")]
46    Auth(String),
47
48    /// URL parsing error.
49    #[error("URL error: {0}")]
50    Url(#[from] url::ParseError),
51
52    /// Invalid parameter supplied by the caller.
53    #[error("Invalid parameter: {0}")]
54    InvalidParam(String),
55
56    /// Alloy signer error.
57    #[error("Signer error: {0}")]
58    Signer(#[from] alloy::signers::Error),
59
60    /// WebSocket error.
61    #[cfg(feature = "ws")]
62    #[error("WebSocket error: {0}")]
63    WebSocket(String),
64}
65
66impl PolymarketError {
67    /// Returns `true` if this error is likely transient and the request can be retried.
68    ///
69    /// Retryable errors include network timeouts, connection resets, 429 rate limits,
70    /// and 5xx server errors.
71    pub fn is_retryable(&self) -> bool {
72        match self {
73            Self::RateLimit { .. } => true,
74            Self::Http(e) => e.is_timeout() || e.is_connect(),
75            Self::Api { status, .. } => *status == 429 || *status >= 500,
76            _ => false,
77        }
78    }
79
80    /// If this is a rate-limit error, returns the recommended retry-after duration.
81    pub fn retry_after(&self) -> Option<Duration> {
82        match self {
83            Self::RateLimit { retry_after } => Some(*retry_after),
84            _ => None,
85        }
86    }
87}
88
89/// Convenience alias.
90pub type Result<T> = std::result::Result<T, PolymarketError>;
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_rate_limit_is_retryable() {
98        let err = PolymarketError::RateLimit {
99            retry_after: Duration::from_secs(5),
100        };
101        assert!(err.is_retryable());
102        assert_eq!(err.retry_after(), Some(Duration::from_secs(5)));
103    }
104
105    #[test]
106    fn test_api_5xx_is_retryable() {
107        let err = PolymarketError::Api {
108            status: 503,
109            message: "Service Unavailable".into(),
110        };
111        assert!(err.is_retryable());
112    }
113
114    #[test]
115    fn test_auth_error_not_retryable() {
116        let err = PolymarketError::Auth("bad key".into());
117        assert!(!err.is_retryable());
118    }
119
120    #[test]
121    fn test_order_rejected_not_retryable() {
122        let err = PolymarketError::OrderRejected {
123            reason: "insufficient balance".into(),
124        };
125        assert!(!err.is_retryable());
126        assert!(err.retry_after().is_none());
127    }
128}