Skip to main content

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}