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