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}