nautilus_binance/spot/http/
error.rs1use std::{fmt::Display, time::Duration};
19
20use nautilus_network::http::error::HttpClientError;
21
22use crate::common::error::{is_retryable_http_status, is_retryable_venue_code};
23pub use crate::spot::sbe::SbeDecodeError;
25
26#[derive(Debug)]
28pub enum BinanceSpotHttpError {
29 MissingCredentials,
31 BinanceError {
33 code: i64,
35 message: String,
37 status: u16,
39 retry_after: Option<Duration>,
41 },
42 SbeDecodeError(SbeDecodeError),
44 JsonError(String),
46 ResponseParseError(String),
48 ValidationError(String),
50 NetworkError(String),
52 Timeout(String),
54 Canceled(String),
56 RetryBudgetExceeded(String),
58 UnexpectedStatus {
60 status: u16,
62 body: String,
64 retry_after: Option<Duration>,
66 },
67}
68
69impl BinanceSpotHttpError {
70 #[must_use]
75 pub fn is_retryable(&self) -> bool {
76 match self {
77 Self::NetworkError(_) | Self::Timeout(_) => true,
78 Self::BinanceError { code, status, .. } => {
79 is_retryable_venue_code(*code) || is_retryable_http_status(*status)
80 }
81 Self::UnexpectedStatus { status, .. } => is_retryable_http_status(*status),
82 _ => false,
83 }
84 }
85
86 #[must_use]
88 pub fn retry_after(&self) -> Option<Duration> {
89 match self {
90 Self::BinanceError { retry_after, .. } | Self::UnexpectedStatus { retry_after, .. } => {
91 *retry_after
92 }
93 _ => None,
94 }
95 }
96}
97
98impl Display for BinanceSpotHttpError {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 match self {
101 Self::MissingCredentials => write!(f, "Missing API credentials"),
102 Self::BinanceError {
103 code,
104 message,
105 status,
106 ..
107 } => {
108 write!(f, "Binance error {code} (HTTP {status}): {message}")
109 }
110 Self::SbeDecodeError(err) => write!(f, "SBE decode error: {err}"),
111 Self::JsonError(msg) => write!(f, "JSON decode error: {msg}"),
112 Self::ResponseParseError(msg) => write!(f, "Response parse error: {msg}"),
113 Self::ValidationError(msg) => write!(f, "Validation error: {msg}"),
114 Self::NetworkError(msg) => write!(f, "Network error: {msg}"),
115 Self::Timeout(msg) => write!(f, "Timeout: {msg}"),
116 Self::Canceled(msg) => write!(f, "Canceled: {msg}"),
117 Self::RetryBudgetExceeded(msg) => write!(f, "Retry budget exceeded: {msg}"),
118 Self::UnexpectedStatus { status, body, .. } => {
119 write!(f, "Unexpected status {status}: {body}")
120 }
121 }
122 }
123}
124
125impl std::error::Error for BinanceSpotHttpError {}
126
127impl From<SbeDecodeError> for BinanceSpotHttpError {
128 fn from(err: SbeDecodeError) -> Self {
129 Self::SbeDecodeError(err)
130 }
131}
132
133impl From<anyhow::Error> for BinanceSpotHttpError {
134 fn from(err: anyhow::Error) -> Self {
135 Self::NetworkError(err.to_string())
136 }
137}
138
139impl From<HttpClientError> for BinanceSpotHttpError {
140 fn from(err: HttpClientError) -> Self {
141 match err {
142 HttpClientError::TimeoutError(msg) => Self::Timeout(msg),
143 HttpClientError::InvalidProxy(msg) | HttpClientError::ClientBuildError(msg) => {
144 Self::NetworkError(msg)
145 }
146 HttpClientError::Error(msg) | HttpClientError::TransportError(msg) => {
147 Self::NetworkError(msg)
148 }
149 }
150 }
151}
152
153pub type BinanceSpotHttpResult<T> = Result<T, BinanceSpotHttpError>;
155
156#[cfg(test)]
157mod tests {
158 use rstest::rstest;
159
160 use super::*;
161
162 fn venue_error(code: i64, status: u16) -> BinanceSpotHttpError {
163 BinanceSpotHttpError::BinanceError {
164 code,
165 message: "venue message".to_string(),
166 status,
167 retry_after: None,
168 }
169 }
170
171 #[rstest]
172 #[case::network(BinanceSpotHttpError::NetworkError("connection reset".to_string()), true)]
173 #[case::timeout(BinanceSpotHttpError::Timeout("timed out".to_string()), true)]
174 #[case::missing_credentials(BinanceSpotHttpError::MissingCredentials, false)]
175 #[case::validation(BinanceSpotHttpError::ValidationError("bad param".to_string()), false)]
176 #[case::canceled(BinanceSpotHttpError::Canceled("shutdown".to_string()), false)]
177 #[case::budget(
178 BinanceSpotHttpError::RetryBudgetExceeded("exceeded".to_string()),
179 false
180 )]
181 #[case::parse(BinanceSpotHttpError::ResponseParseError("bad body".to_string()), false)]
182 fn test_is_retryable_transport_and_local(
183 #[case] error: BinanceSpotHttpError,
184 #[case] expected: bool,
185 ) {
186 assert_eq!(error.is_retryable(), expected);
187 }
188
189 #[rstest]
190 #[case::rate_limit_code(venue_error(-1003, 429), true)]
191 #[case::order_rate_limit_code(venue_error(-1015, 400), true)]
192 #[case::status_500(venue_error(-1000, 500), true)]
193 #[case::status_418(venue_error(-1003, 418), true)]
194 #[case::permanent_venue_code(venue_error(-1100, 400), false)]
195 #[case::auth_code(venue_error(-2015, 401), false)]
196 #[case::timestamp_drift(venue_error(-1021, 400), false)]
197 fn test_is_retryable_venue_errors(#[case] error: BinanceSpotHttpError, #[case] expected: bool) {
198 assert_eq!(error.is_retryable(), expected);
199 }
200
201 #[rstest]
202 #[case::status_429(BinanceSpotHttpError::UnexpectedStatus {
203 status: 429,
204 body: "rate limited".to_string(),
205 retry_after: None,
206 }, true)]
207 #[case::status_500(BinanceSpotHttpError::UnexpectedStatus {
208 status: 500,
209 body: "internal server error".to_string(),
210 retry_after: None,
211 }, true)]
212 #[case::status_401(BinanceSpotHttpError::UnexpectedStatus {
213 status: 401,
214 body: "unauthorized".to_string(),
215 retry_after: None,
216 }, false)]
217 #[case::status_400(BinanceSpotHttpError::UnexpectedStatus {
218 status: 400,
219 body: "bad request".to_string(),
220 retry_after: None,
221 }, false)]
222 fn test_is_retryable_unexpected_status(
223 #[case] error: BinanceSpotHttpError,
224 #[case] expected: bool,
225 ) {
226 assert_eq!(error.is_retryable(), expected);
227 }
228
229 #[rstest]
230 fn test_retry_after_accessor() {
231 let delay = Duration::from_secs(2);
232 let error = BinanceSpotHttpError::BinanceError {
233 code: -1003,
234 message: "Too many requests".to_string(),
235 status: 429,
236 retry_after: Some(delay),
237 };
238 assert_eq!(error.retry_after(), Some(delay));
239 assert_eq!(
240 BinanceSpotHttpError::NetworkError("x".to_string()).retry_after(),
241 None
242 );
243 }
244
245 #[rstest]
246 fn test_display_includes_status() {
247 let err = venue_error(-1100, 400);
248 let msg = err.to_string();
249 assert!(msg.contains("-1100"));
250 assert!(msg.contains("400"));
251 }
252}