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 (retry_after={retry_after:?}s)")]
35    RateLimit { retry_after: Option<u64> },
36
37    /// Nonce window violations (nonces must be within time window and unique)
38    #[error("nonce window error: {0}")]
39    NonceWindow(String),
40
41    /// Bad request errors (client-side invalid payload)
42    #[error("bad request: {0}")]
43    BadRequest(String),
44
45    /// Exchange-specific errors from Hyperliquid server
46    #[error("exchange error: {0}")]
47    Exchange(String),
48
49    /// Request timeout
50    #[error("timeout")]
51    Timeout,
52
53    /// Message decoding/parsing errors
54    #[error("decode error: {0}")]
55    Decode(String),
56
57    /// Invariant violation (impossible state)
58    #[error("invariant violated: {0}")]
59    Invariant(&'static str),
60
61    /// HTTP errors with status code
62    #[error("HTTP error {status}: {message}")]
63    Http { status: u16, message: String },
64
65    /// URL parsing errors
66    #[error("URL parse error: {0}")]
67    UrlParse(#[from] url::ParseError),
68
69    /// Standard IO errors
70    #[error("IO error: {0}")]
71    Io(#[from] std::io::Error),
72}
73
74impl Error {
75    /// Create a transport error
76    pub fn transport(msg: impl Into<String>) -> Self {
77        Self::Transport(msg.into())
78    }
79
80    /// Create an auth error
81    pub fn auth(msg: impl Into<String>) -> Self {
82        Self::Auth(msg.into())
83    }
84
85    /// Create a rate limit error
86    pub fn rate_limit(retry_after: Option<u64>) -> Self {
87        Self::RateLimit { retry_after }
88    }
89
90    /// Create a nonce window error
91    pub fn nonce_window(msg: impl Into<String>) -> Self {
92        Self::NonceWindow(msg.into())
93    }
94
95    /// Create a bad request error
96    pub fn bad_request(msg: impl Into<String>) -> Self {
97        Self::BadRequest(msg.into())
98    }
99
100    /// Create an exchange error
101    pub fn exchange(msg: impl Into<String>) -> Self {
102        Self::Exchange(msg.into())
103    }
104
105    /// Create a decode error
106    pub fn decode(msg: impl Into<String>) -> Self {
107        Self::Decode(msg.into())
108    }
109
110    /// Create an HTTP error
111    pub fn http(status: u16, message: impl Into<String>) -> Self {
112        Self::Http {
113            status,
114            message: message.into(),
115        }
116    }
117
118    /// Map reqwest errors to appropriate error types
119    pub fn from_reqwest(error: reqwest::Error) -> Self {
120        if error.is_timeout() {
121            Self::Timeout
122        } else if let Some(status) = error.status() {
123            let status_code = status.as_u16();
124            match status_code {
125                401 | 403 => Self::auth(format!("HTTP {}: authentication failed", status_code)),
126                400 => Self::bad_request(format!("HTTP {}: bad request", status_code)),
127                429 => Self::rate_limit(None), // TODO: Extract retry-after header
128                500..=599 => Self::exchange(format!("HTTP {}: server error", status_code)),
129                _ => Self::http(status_code, format!("HTTP error: {}", error)),
130            }
131        } else if error.is_connect() || error.is_request() {
132            Self::transport(format!("Request error: {}", error))
133        } else {
134            Self::transport(format!("Unknown reqwest error: {}", error))
135        }
136    }
137
138    /// Check if error is retryable
139    pub fn is_retryable(&self) -> bool {
140        match self {
141            Error::Transport(_) | Error::Timeout | Error::RateLimit { .. } => true,
142            Error::Http { status, .. } => *status >= 500,
143            _ => false,
144        }
145    }
146
147    /// Check if error is due to rate limiting
148    pub fn is_rate_limited(&self) -> bool {
149        matches!(self, Error::RateLimit { .. })
150    }
151
152    /// Check if error is due to authentication issues
153    pub fn is_auth_error(&self) -> bool {
154        matches!(self, Error::Auth(_))
155    }
156}
157
158/// Result type alias for Hyperliquid operations
159pub type Result<T> = std::result::Result<T, Error>;
160
161////////////////////////////////////////////////////////////////////////////////
162// Tests
163////////////////////////////////////////////////////////////////////////////////
164
165#[cfg(test)]
166mod tests {
167    use rstest::rstest;
168
169    use super::*;
170
171    #[rstest]
172    fn test_error_constructors() {
173        let transport_err = Error::transport("Connection failed");
174        assert!(matches!(transport_err, Error::Transport(_)));
175        assert_eq!(
176            transport_err.to_string(),
177            "transport error: Connection failed"
178        );
179
180        let auth_err = Error::auth("Invalid signature");
181        assert!(auth_err.is_auth_error());
182
183        let rate_limit_err = Error::rate_limit(Some(30));
184        assert!(rate_limit_err.is_rate_limited());
185        assert!(rate_limit_err.is_retryable());
186
187        let http_err = Error::http(500, "Internal server error");
188        assert!(http_err.is_retryable());
189    }
190
191    #[rstest]
192    fn test_error_display() {
193        let err = Error::RateLimit {
194            retry_after: Some(60),
195        };
196        assert_eq!(err.to_string(), "rate limited (retry_after=Some(60)s)");
197
198        let err = Error::NonceWindow("Nonce too old".to_string());
199        assert_eq!(err.to_string(), "nonce window error: Nonce too old");
200    }
201
202    #[rstest]
203    fn test_retryable_errors() {
204        assert!(Error::transport("test").is_retryable());
205        assert!(Error::Timeout.is_retryable());
206        assert!(Error::rate_limit(None).is_retryable());
207        assert!(Error::http(500, "server error").is_retryable());
208
209        assert!(!Error::auth("test").is_retryable());
210        assert!(!Error::bad_request("test").is_retryable());
211        assert!(!Error::decode("test").is_retryable());
212    }
213}