nautilus_hyperliquid/http/
error.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use thiserror::Error;
17
18/// Comprehensive error type for Hyperliquid operations
19#[derive(Debug, Error)]
20pub enum Error {
21    /// Transport layer errors (network, connection issues)
22    #[error("transport error: {0}")]
23    Transport(String),
24
25    /// JSON serialization/deserialization errors
26    #[error("serde error: {0}")]
27    Serde(#[from] serde_json::Error),
28
29    /// Authentication errors (signature mismatch, wrong wallet)
30    #[error("auth error: {0}")]
31    Auth(String),
32
33    /// Rate limiting errors with optional retry information
34    #[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    /// Nonce window violations (nonces must be within time window and unique)
42    #[error("nonce window error: {0}")]
43    NonceWindow(String),
44
45    /// Bad request errors (client-side invalid payload)
46    #[error("bad request: {0}")]
47    BadRequest(String),
48
49    /// Exchange-specific errors from Hyperliquid server
50    #[error("exchange error: {0}")]
51    Exchange(String),
52
53    /// Request timeout
54    #[error("timeout")]
55    Timeout,
56
57    /// Message decoding/parsing errors
58    #[error("decode error: {0}")]
59    Decode(String),
60
61    /// Invariant violation (impossible state)
62    #[error("invariant violated: {0}")]
63    Invariant(&'static str),
64
65    /// HTTP errors with status code
66    #[error("HTTP error {status}: {message}")]
67    Http { status: u16, message: String },
68
69    /// URL parsing errors
70    #[error("URL parse error: {0}")]
71    UrlParse(#[from] url::ParseError),
72
73    /// Standard IO errors
74    #[error("IO error: {0}")]
75    Io(#[from] std::io::Error),
76}
77
78impl Error {
79    /// Create a transport error
80    pub fn transport(msg: impl Into<String>) -> Self {
81        Self::Transport(msg.into())
82    }
83
84    /// Create an auth error
85    pub fn auth(msg: impl Into<String>) -> Self {
86        Self::Auth(msg.into())
87    }
88
89    /// Create a rate limit error
90    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    /// Create a nonce window error
99    pub fn nonce_window(msg: impl Into<String>) -> Self {
100        Self::NonceWindow(msg.into())
101    }
102
103    /// Create a bad request error
104    pub fn bad_request(msg: impl Into<String>) -> Self {
105        Self::BadRequest(msg.into())
106    }
107
108    /// Create an exchange error
109    pub fn exchange(msg: impl Into<String>) -> Self {
110        Self::Exchange(msg.into())
111    }
112
113    /// Create a decode error
114    pub fn decode(msg: impl Into<String>) -> Self {
115        Self::Decode(msg.into())
116    }
117
118    /// Create an HTTP error
119    pub fn http(status: u16, message: impl Into<String>) -> Self {
120        Self::Http {
121            status,
122            message: message.into(),
123        }
124    }
125
126    /// Create an error from HTTP status code and body
127    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    /// Map reqwest errors to appropriate error types
139    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    /// Map HTTP client errors to appropriate error types
159    pub fn from_http_client(error: nautilus_network::http::HttpClientError) -> Self {
160        Self::transport(format!("HTTP client error: {}", error))
161    }
162
163    /// Check if error is retryable
164    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    /// Check if error is due to rate limiting
173    pub fn is_rate_limited(&self) -> bool {
174        matches!(self, Self::RateLimit { .. })
175    }
176
177    /// Check if error is due to authentication issues
178    pub fn is_auth_error(&self) -> bool {
179        matches!(self, Self::Auth(_))
180    }
181}
182
183/// Result type alias for Hyperliquid operations
184pub type Result<T> = std::result::Result<T, Error>;
185
186////////////////////////////////////////////////////////////////////////////////
187// Tests
188////////////////////////////////////////////////////////////////////////////////
189
190#[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}