Skip to main content

rustrade_data/exchange/binance/
error.rs

1//! Error type for the Binance historical klines REST client.
2//!
3//! Modelled on `MassiveError` (the `massive` feature's REST error): the
4//! library surfaces these errors **without** automatic retry, reconnection, or
5//! adaptive backoff. The consumer decides how to handle rate limits and API
6//! failures (see [`BinanceDataError::RateLimited`] for the resume contract).
7//!
8//! It is deliberately a **dedicated** type rather than a new variant on the
9//! shared [`DataError`](crate::error::DataError) — that shared enum derives
10//! `Eq`/`Ord`/`Hash`/`Serialize`,
11//! and a `retry_after: Duration` payload would not fit cleanly. Keeping the
12//! Binance REST error local mirrors the Massive client exactly.
13
14use std::time::Duration;
15
16/// Errors returned by the Binance historical klines REST client.
17///
18/// The kline endpoints are public and unauthenticated, so there is no auth or
19/// environment-variable variant (contrast the `massive` feature's `MassiveError`,
20/// which carries both). WebSocket disconnects are handled by the live path's
21/// `Connector`, not here.
22#[derive(Debug, Clone, PartialEq)]
23pub enum BinanceDataError {
24    /// Rate limited by the API (HTTP `429`) or IP-banned for repeat violations
25    /// (HTTP `418`). Carries the optional `Retry-After` duration parsed from the
26    /// response header.
27    ///
28    /// # Contract
29    ///
30    /// The historical `Stream` **yields this error and ends** — it does **not**
31    /// wait, retry, or run a process-global limiter. The consumer owns
32    /// retry/backoff and **resumes** losslessly by re-invoking `fetch_candles`
33    /// with `start` advanced to `last_close_time + 1ms` (the next candle's open).
34    /// The `[start, end]` range is `close_time`-inclusive, so resuming exactly at
35    /// the last `close_time` would re-yield that final candle; the `+1ms` step
36    /// skips it without leaving a gap (pagination keys off `open_time`, and
37    /// `open ≡ close − interval`).
38    RateLimited { retry_after: Option<Duration> },
39
40    /// API returned a non-success, non-rate-limit response (e.g. `400 Invalid
41    /// symbol`, `400 Invalid interval`).
42    Api { status: u16, message: String },
43
44    /// Network or HTTP client error (DNS, TLS, timeout, connection reset).
45    Http { message: String },
46
47    /// Response body could not be deserialised into the expected kline shape.
48    Deserialize { message: String, payload: String },
49
50    /// Client-side input validation failed before any request was made
51    /// (e.g. empty symbol, or a candle whose `close_time` overflows the
52    /// representable `DateTime<Utc>` range).
53    InvalidInput { message: String },
54}
55
56impl std::fmt::Display for BinanceDataError {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        match self {
59            BinanceDataError::RateLimited { retry_after } => {
60                write!(f, "Binance rate limited")?;
61                if let Some(duration) = retry_after {
62                    write!(f, " (retry after {:?})", duration)?;
63                }
64                Ok(())
65            }
66            BinanceDataError::Api { status, message } => {
67                write!(f, "Binance API error ({}): {}", status, message)
68            }
69            BinanceDataError::Http { message } => {
70                write!(f, "Binance HTTP error: {}", message)
71            }
72            BinanceDataError::Deserialize { message, payload } => {
73                let boundary = payload.floor_char_boundary(100);
74                let truncated = &payload[..boundary];
75                let ellipsis = if boundary < payload.len() { "..." } else { "" };
76                write!(
77                    f,
78                    "Binance deserialize error: {} (payload: {truncated}{ellipsis})",
79                    message
80                )
81            }
82            BinanceDataError::InvalidInput { message } => {
83                write!(f, "Binance invalid input: {}", message)
84            }
85        }
86    }
87}
88
89impl std::error::Error for BinanceDataError {}
90
91impl From<reqwest::Error> for BinanceDataError {
92    fn from(err: reqwest::Error) -> Self {
93        BinanceDataError::Http {
94            message: err.to_string(),
95        }
96    }
97}