asterdex_sdk/rest/error.rs
1// US-001: Error type — fully implemented in this wave (US-003 extends as needed)
2use std::time::Duration;
3use thiserror::Error;
4
5/// All SDK error variants — see each variant for handling instructions.
6#[derive(Error, Debug)]
7pub enum AsterDexError {
8 /// API returned a structured error `{ "code": int, "msg": string }`
9 #[error("API error {code}: {msg}")]
10 ApiError {
11 /// Numeric error code from the exchange
12 code: i64,
13 /// Human-readable error message from the exchange
14 msg: String,
15 },
16
17 /// HTTP 429 — Too Many Requests
18 #[error("Rate limited, retry after: {retry_after:?}")]
19 RateLimited {
20 /// Duration to wait before retrying, parsed from `Retry-After` header
21 retry_after: Option<Duration>,
22 },
23
24 /// HTTP 418 — IP banned
25 #[error("IP banned, retry after: {retry_after:?}")]
26 IpBanned {
27 /// Duration to wait before retrying, parsed from `Retry-After` header
28 retry_after: Option<Duration>,
29 },
30
31 /// HTTP 5xx or other non-success status that is not a structured API error
32 #[error("HTTP error {status}: {body}")]
33 HttpError {
34 /// HTTP status code
35 status: u16,
36 /// Response body or error description (truncated to prevent log spam)
37 body: String,
38 },
39
40 /// Network-level error (DNS, connect, TLS, read, body decode — no HTTP status)
41 #[error("Network error ({kind}): {message}")]
42 NetworkError {
43 /// Short classifier: "timeout", "connect", "tls", "body", "decode", "other"
44 kind: &'static str,
45 /// Short description (no body, safe to log)
46 message: String,
47 },
48
49 /// Request timed out (client-side) — distinguished so callers can retry aggressively
50 #[error("Request timeout: {message}")]
51 Timeout {
52 /// Short description of what timed out
53 message: String,
54 },
55
56 /// Client-side validation failure (caller-supplied parameters are invalid)
57 #[error("Invalid parameters: {message}")]
58 InvalidParams {
59 /// Description of what is invalid
60 message: String,
61 },
62
63 /// WebSocket connection or protocol error
64 #[error("WebSocket error: {message}")]
65 WebSocketError {
66 /// Description of the WebSocket error
67 message: String,
68 },
69
70 /// Configuration error (missing env var, invalid key, etc.)
71 #[error("Config error: {message}")]
72 ConfigError {
73 /// Description of the configuration issue
74 message: String,
75 },
76
77 /// JSON serialization/deserialization error
78 #[error("Serde error: {message}")]
79 SerdeError {
80 /// Description of the serialization/deserialization failure.
81 /// Body preview (first 512 bytes) is included when helpful, never the full body.
82 message: String,
83 },
84}
85
86impl AsterDexError {
87 /// Classify a `reqwest::Error` into the most specific variant.
88 ///
89 /// Produces `Timeout` for client-side timeouts, otherwise `NetworkError`
90 /// with a stable `kind` tag for retry logic. Never returns `HttpError`
91 /// (that is reserved for non-success HTTP responses).
92 pub(crate) fn from_reqwest(e: reqwest::Error) -> Self {
93 if e.is_timeout() {
94 return AsterDexError::Timeout {
95 message: e.to_string(),
96 };
97 }
98 let kind = if e.is_connect() {
99 "connect"
100 } else if e.is_request() {
101 "request"
102 } else if e.is_body() {
103 "body"
104 } else if e.is_decode() {
105 "decode"
106 } else {
107 "other"
108 };
109 AsterDexError::NetworkError {
110 kind,
111 message: e.to_string(),
112 }
113 }
114}