nautilus_polymarket/http/
error.rs1use nautilus_network::http::{HttpClientError, ReqwestError, StatusCode};
19use thiserror::Error;
20
21#[derive(Debug, Error)]
23pub enum Error {
24 #[error("transport error: {0}")]
25 Transport(String),
26
27 #[error("serde error: {0}")]
28 Serde(#[from] serde_json::Error),
29
30 #[error("auth error: {0}")]
31 Auth(String),
32
33 #[error("Rate limited on {scope} (weight={weight}) retry_after_ms={retry_after_ms:?}")]
34 RateLimit {
35 scope: &'static str,
36 weight: u32,
37 retry_after_ms: Option<u64>,
38 },
39
40 #[error("bad request: {0}")]
41 BadRequest(String),
42
43 #[error("exchange error: {0}")]
44 Exchange(String),
45
46 #[error("timeout")]
47 Timeout,
48
49 #[error("decode error: {0}")]
50 Decode(String),
51
52 #[error("HTTP error {status}: {message}")]
53 Http { status: u16, message: String },
54
55 #[error("URL parse error: {0}")]
56 UrlParse(#[from] url::ParseError),
57
58 #[error("IO error: {0}")]
59 Io(#[from] std::io::Error),
60}
61
62impl Error {
63 pub fn transport(msg: impl Into<String>) -> Self {
64 Self::Transport(msg.into())
65 }
66
67 pub fn auth(msg: impl Into<String>) -> Self {
68 Self::Auth(msg.into())
69 }
70
71 pub fn rate_limit(scope: &'static str, weight: u32, retry_after_ms: Option<u64>) -> Self {
72 Self::RateLimit {
73 scope,
74 weight,
75 retry_after_ms,
76 }
77 }
78
79 pub fn bad_request(msg: impl Into<String>) -> Self {
80 Self::BadRequest(msg.into())
81 }
82
83 pub fn exchange(msg: impl Into<String>) -> Self {
84 Self::Exchange(msg.into())
85 }
86
87 pub fn decode(msg: impl Into<String>) -> Self {
88 Self::Decode(msg.into())
89 }
90
91 pub fn http(status: u16, message: impl Into<String>) -> Self {
92 Self::Http {
93 status,
94 message: message.into(),
95 }
96 }
97
98 pub fn from_http_status(status: StatusCode, body: &[u8]) -> Self {
100 let message = String::from_utf8_lossy(body).to_string();
101 match status.as_u16() {
102 401 | 403 => Self::auth(format!("HTTP {}: {message}", status.as_u16())),
103 400 => Self::bad_request(format!("HTTP {}: {message}", status.as_u16())),
104 429 => Self::rate_limit("unknown", 0, None),
105 _ => Self::http(status.as_u16(), message),
106 }
107 }
108
109 pub fn from_status_code(status: u16, body: &[u8]) -> Self {
111 let message = String::from_utf8_lossy(body).to_string();
112 match status {
113 401 | 403 => Self::auth(format!("HTTP {status}: {message}")),
114 400 => Self::bad_request(format!("HTTP {status}: {message}")),
115 429 => Self::rate_limit("unknown", 0, None),
116 _ => Self::http(status, message),
117 }
118 }
119
120 #[expect(clippy::needless_pass_by_value)]
122 pub fn from_reqwest(error: ReqwestError) -> Self {
123 if error.is_timeout() {
124 Self::Timeout
125 } else if let Some(status) = error.status() {
126 let status_code = status.as_u16();
127 match status_code {
128 401 | 403 => Self::auth(format!("HTTP {status_code}: authentication failed")),
129 400 => Self::bad_request(format!("HTTP {status_code}: bad request")),
130 429 => Self::rate_limit("unknown", 0, None),
131 _ => Self::http(status_code, format!("HTTP error: {error}")),
132 }
133 } else if error.is_connect() || error.is_request() {
134 Self::transport(format!("Request error: {error}"))
135 } else {
136 Self::transport(format!("Unknown reqwest error: {error}"))
137 }
138 }
139
140 #[expect(clippy::needless_pass_by_value)]
141 pub fn from_http_client(error: HttpClientError) -> Self {
142 Self::transport(format!("HTTP client error: {error}"))
143 }
144
145 pub fn is_retryable(&self) -> bool {
146 match self {
147 Self::Transport(_) | Self::Timeout | Self::RateLimit { .. } => true,
148 Self::Http { status, .. } => *status >= 500,
149 _ => false,
150 }
151 }
152
153 pub fn is_rate_limited(&self) -> bool {
154 matches!(self, Self::RateLimit { .. })
155 }
156
157 pub fn is_auth_error(&self) -> bool {
158 matches!(self, Self::Auth(_))
159 }
160
161 pub fn is_http_status_error(&self) -> bool {
164 matches!(
165 self,
166 Self::Auth(_) | Self::BadRequest(_) | Self::RateLimit { .. } | Self::Http { .. }
167 )
168 }
169}
170
171pub type Result<T> = std::result::Result<T, Error>;
172
173#[cfg(test)]
174mod tests {
175 use rstest::rstest;
176
177 use super::*;
178
179 #[rstest]
180 fn test_error_constructors() {
181 let transport_err = Error::transport("Connection failed");
182 assert!(matches!(transport_err, Error::Transport(_)));
183 assert_eq!(
184 transport_err.to_string(),
185 "transport error: Connection failed"
186 );
187
188 let auth_err = Error::auth("Invalid signature");
189 assert!(auth_err.is_auth_error());
190
191 let rate_limit_err = Error::rate_limit("test", 30, Some(30000));
192 assert!(rate_limit_err.is_rate_limited());
193 assert!(rate_limit_err.is_retryable());
194
195 let http_err = Error::http(500, "Internal server error");
196 assert!(http_err.is_retryable());
197 }
198
199 #[rstest]
200 fn test_error_display() {
201 let err = Error::RateLimit {
202 scope: "order",
203 weight: 10,
204 retry_after_ms: Some(60000),
205 };
206 assert_eq!(
207 err.to_string(),
208 "Rate limited on order (weight=10) retry_after_ms=Some(60000)"
209 );
210 }
211
212 #[rstest]
213 fn test_retryable_errors() {
214 assert!(Error::transport("test").is_retryable());
215 assert!(Error::Timeout.is_retryable());
216 assert!(Error::rate_limit("test", 10, None).is_retryable());
217 assert!(Error::http(500, "server error").is_retryable());
218
219 assert!(!Error::auth("test").is_retryable());
220 assert!(!Error::bad_request("test").is_retryable());
221 assert!(!Error::decode("test").is_retryable());
222 }
223}