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_submit_outcome_unknown(&self) -> bool {
156 match self {
157 Self::Transport(_)
158 | Self::Timeout
159 | Self::RateLimit { .. }
160 | Self::Serde(_)
161 | Self::Decode(_)
162 | Self::Io(_) => true,
163 Self::Http { status, .. } => *status >= 500,
164 Self::Auth(_) | Self::BadRequest(_) | Self::Exchange(_) | Self::UrlParse(_) => false,
165 }
166 }
167
168 pub fn is_rate_limited(&self) -> bool {
169 matches!(self, Self::RateLimit { .. })
170 }
171
172 pub fn is_auth_error(&self) -> bool {
173 matches!(self, Self::Auth(_))
174 }
175
176 pub fn is_http_status_error(&self) -> bool {
179 matches!(
180 self,
181 Self::Auth(_) | Self::BadRequest(_) | Self::RateLimit { .. } | Self::Http { .. }
182 )
183 }
184}
185
186pub type Result<T> = std::result::Result<T, Error>;
187
188#[cfg(test)]
189mod tests {
190 use rstest::rstest;
191
192 use super::*;
193
194 #[rstest]
195 fn test_error_constructors() {
196 let transport_err = Error::transport("Connection failed");
197 assert!(matches!(transport_err, Error::Transport(_)));
198 assert_eq!(
199 transport_err.to_string(),
200 "transport error: Connection failed"
201 );
202
203 let auth_err = Error::auth("Invalid signature");
204 assert!(auth_err.is_auth_error());
205
206 let rate_limit_err = Error::rate_limit("test", 30, Some(30000));
207 assert!(rate_limit_err.is_rate_limited());
208 assert!(rate_limit_err.is_retryable());
209
210 let http_err = Error::http(500, "Internal server error");
211 assert!(http_err.is_retryable());
212 }
213
214 #[rstest]
215 fn test_error_display() {
216 let err = Error::RateLimit {
217 scope: "order",
218 weight: 10,
219 retry_after_ms: Some(60000),
220 };
221 assert_eq!(
222 err.to_string(),
223 "Rate limited on order (weight=10) retry_after_ms=Some(60000)"
224 );
225 }
226
227 #[rstest]
228 fn test_retryable_errors() {
229 assert!(Error::transport("test").is_retryable());
230 assert!(Error::Timeout.is_retryable());
231 assert!(Error::rate_limit("test", 10, None).is_retryable());
232 assert!(Error::http(500, "server error").is_retryable());
233
234 assert!(!Error::auth("test").is_retryable());
235 assert!(!Error::bad_request("test").is_retryable());
236 assert!(!Error::decode("test").is_retryable());
237 }
238
239 #[rstest]
240 fn test_submit_outcome_unknown_errors() {
241 assert!(Error::transport("test").is_submit_outcome_unknown());
242 assert!(Error::Timeout.is_submit_outcome_unknown());
243 assert!(Error::rate_limit("test", 10, None).is_submit_outcome_unknown());
244 assert!(Error::http(500, "server error").is_submit_outcome_unknown());
245 assert!(Error::decode("bad json").is_submit_outcome_unknown());
246
247 assert!(!Error::auth("test").is_submit_outcome_unknown());
248 assert!(!Error::bad_request("test").is_submit_outcome_unknown());
249 assert!(!Error::http(404, "not found").is_submit_outcome_unknown());
250 }
251}