1#[derive(Debug, thiserror::Error)]
3#[non_exhaustive]
4pub enum HlError {
5 #[error("Signing error: {message}")]
7 Signing {
8 message: String,
10 #[source]
12 source: Option<Box<dyn std::error::Error + Send + Sync>>,
13 },
14 #[error("Serialization error: {message}")]
16 Serialization {
17 message: String,
19 #[source]
21 source: Option<Box<dyn std::error::Error + Send + Sync>>,
22 },
23 #[error("HTTP error: {message}")]
25 Http {
26 message: String,
28 #[source]
30 source: Option<Box<dyn std::error::Error + Send + Sync>>,
31 },
32 #[error("Timeout: {message}")]
34 Timeout {
35 message: String,
37 #[source]
39 source: Option<Box<dyn std::error::Error + Send + Sync>>,
40 },
41 #[error("WebSocket error: {message}")]
43 WebSocket {
44 message: String,
46 #[source]
48 source: Option<Box<dyn std::error::Error + Send + Sync>>,
49 },
50 #[error("API error (HTTP {status}): {body}")]
52 Api {
53 status: u16,
55 body: String,
57 },
58 #[error("Order rejected: {reason}")]
60 Rejected {
61 reason: String,
63 },
64 #[error("Invalid address: {0}")]
66 InvalidAddress(String),
67 #[error("Rate limited (429): retry after {retry_after_ms}ms")]
69 RateLimited {
70 retry_after_ms: u64,
72 message: String,
74 },
75 #[error("Parse error: {0}")]
77 Parse(String),
78 #[error("Validation error: {0}")]
80 Validation(String),
81 #[error("Config error: {0}")]
83 Config(String),
84 #[error("WebSocket reconnect cancelled")]
86 WsCancelled,
87 #[error("WebSocket reconnect failed after {attempts} attempts")]
89 WsReconnectExhausted {
90 attempts: u32,
92 },
93}
94
95impl HlError {
96 pub fn http(message: impl Into<String>) -> Self {
98 HlError::Http {
99 message: message.into(),
100 source: None,
101 }
102 }
103
104 pub fn timeout(message: impl Into<String>) -> Self {
106 HlError::Timeout {
107 message: message.into(),
108 source: None,
109 }
110 }
111
112 pub fn signing(message: impl Into<String>) -> Self {
114 HlError::Signing {
115 message: message.into(),
116 source: None,
117 }
118 }
119
120 pub fn serialization(message: impl Into<String>) -> Self {
122 HlError::Serialization {
123 message: message.into(),
124 source: None,
125 }
126 }
127
128 pub fn websocket(message: impl Into<String>) -> Self {
130 HlError::WebSocket {
131 message: message.into(),
132 source: None,
133 }
134 }
135
136 pub fn is_retryable(&self) -> bool {
138 match self {
139 HlError::Http { .. } => true,
140 HlError::Timeout { .. } => true,
141 HlError::WebSocket { .. } => true,
142 HlError::RateLimited { .. } => true,
143 HlError::Api { status, .. } => {
144 *status >= 500
146 }
147 _ => false,
148 }
149 }
150
151 pub fn retry_after_ms(&self) -> Option<u64> {
153 match self {
154 HlError::RateLimited { retry_after_ms, .. } => Some(*retry_after_ms),
155 _ => None,
156 }
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn is_retryable_http_error() {
166 assert!(HlError::http("timeout").is_retryable());
167 }
168
169 #[test]
170 fn is_retryable_rate_limited() {
171 assert!(HlError::RateLimited {
172 retry_after_ms: 1000,
173 message: "slow down".into()
174 }
175 .is_retryable());
176 }
177
178 #[test]
179 fn is_retryable_api_5xx() {
180 assert!(HlError::Api {
181 status: 500,
182 body: "internal error".into()
183 }
184 .is_retryable());
185 assert!(HlError::Api {
186 status: 502,
187 body: "bad gateway".into()
188 }
189 .is_retryable());
190 assert!(HlError::Api {
191 status: 503,
192 body: "unavailable".into()
193 }
194 .is_retryable());
195 }
196
197 #[test]
198 fn not_retryable_api_4xx() {
199 assert!(!HlError::Api {
200 status: 400,
201 body: "bad request".into()
202 }
203 .is_retryable());
204 assert!(!HlError::Api {
205 status: 404,
206 body: "not found".into()
207 }
208 .is_retryable());
209 assert!(!HlError::Api {
210 status: 422,
211 body: "unprocessable".into()
212 }
213 .is_retryable());
214 }
215
216 #[test]
217 fn is_retryable_timeout() {
218 assert!(HlError::timeout("request timed out").is_retryable());
219 }
220
221 #[test]
222 fn is_retryable_websocket() {
223 assert!(HlError::websocket("connection failed").is_retryable());
224 }
225
226 #[test]
227 fn not_retryable_rejected() {
228 assert!(!HlError::Rejected {
229 reason: "order rejected".into()
230 }
231 .is_retryable());
232 }
233
234 #[test]
235 fn not_retryable_signing() {
236 assert!(!HlError::signing("key error").is_retryable());
237 }
238
239 #[test]
240 fn not_retryable_parse() {
241 assert!(!HlError::Parse("bad json".into()).is_retryable());
242 }
243
244 #[test]
245 fn not_retryable_serialization() {
246 assert!(!HlError::serialization("serde fail").is_retryable());
247 }
248
249 #[test]
250 fn not_retryable_invalid_address() {
251 assert!(!HlError::InvalidAddress("bad addr".into()).is_retryable());
252 }
253
254 #[test]
255 fn retry_after_ms_rate_limited() {
256 let err = HlError::RateLimited {
257 retry_after_ms: 5000,
258 message: "".into(),
259 };
260 assert_eq!(err.retry_after_ms(), Some(5000));
261 }
262
263 #[test]
264 fn retry_after_ms_none_for_other_errors() {
265 assert_eq!(HlError::http("x").retry_after_ms(), None);
266 assert_eq!(HlError::timeout("x").retry_after_ms(), None);
267 assert_eq!(HlError::websocket("x").retry_after_ms(), None);
268 assert_eq!(HlError::signing("x").retry_after_ms(), None);
269 assert_eq!(HlError::Parse("x".into()).retry_after_ms(), None);
270 assert_eq!(
271 HlError::Api {
272 status: 500,
273 body: "x".into()
274 }
275 .retry_after_ms(),
276 None
277 );
278 assert_eq!(
279 HlError::Rejected { reason: "x".into() }.retry_after_ms(),
280 None
281 );
282 }
283
284 #[test]
285 fn error_display_formatting() {
286 let err = HlError::http("connection refused");
287 assert_eq!(format!("{err}"), "HTTP error: connection refused");
288
289 let err = HlError::Api {
290 status: 404,
291 body: "not found".into(),
292 };
293 assert_eq!(format!("{err}"), "API error (HTTP 404): not found");
294
295 let err = HlError::RateLimited {
296 retry_after_ms: 2000,
297 message: "slow".into(),
298 };
299 assert!(format!("{err}").contains("2000ms"));
300
301 let err = HlError::timeout("request timed out");
302 assert_eq!(format!("{err}"), "Timeout: request timed out");
303
304 let err = HlError::websocket("connection failed");
305 assert_eq!(format!("{err}"), "WebSocket error: connection failed");
306
307 let err = HlError::Rejected {
308 reason: "insufficient margin".into(),
309 };
310 assert_eq!(format!("{err}"), "Order rejected: insufficient margin");
311 }
312
313 #[test]
314 fn http_error_with_source_preserves_chain() {
315 let io_err =
316 std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "connection refused");
317 let err = HlError::Http {
318 message: "request failed".into(),
319 source: Some(Box::new(io_err)),
320 };
321 assert!(
322 std::error::Error::source(&err).is_some(),
323 "source should be present when provided"
324 );
325 }
326
327 #[test]
328 fn http_error_without_source() {
329 let err = HlError::http("no underlying cause");
330 assert!(
331 std::error::Error::source(&err).is_none(),
332 "source should be None for convenience constructor"
333 );
334 }
335
336 #[test]
337 fn serialization_not_retryable() {
338 let err = HlError::serialization("bad json");
339 assert!(
340 !err.is_retryable(),
341 "Serialization errors should not be retryable"
342 );
343 }
344
345 #[test]
346 fn config_error_not_retryable() {
347 let err = HlError::Config("invalid timeout".into());
348 assert!(!err.is_retryable(), "Config errors should not be retryable");
349 }
350
351 #[test]
352 fn config_error_display() {
353 let err = HlError::Config("missing API key".into());
354 assert_eq!(format!("{err}"), "Config error: missing API key");
355 }
356}