nautilus_binance/futures/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};
23
24#[derive(Debug)]
26pub enum BinanceFuturesHttpError {
27 MissingCredentials,
29 BinanceError {
31 code: i64,
33 message: String,
35 status: u16,
37 retry_after: Option<Duration>,
39 },
40 JsonError(String),
42 ValidationError(String),
44 NetworkError(String),
46 Timeout(String),
48 Canceled(String),
50 RetryBudgetExceeded(String),
52 UnexpectedStatus {
54 status: u16,
56 body: String,
58 retry_after: Option<Duration>,
60 },
61}
62
63impl BinanceFuturesHttpError {
64 #[must_use]
69 pub fn is_retryable(&self) -> bool {
70 match self {
71 Self::NetworkError(_) | Self::Timeout(_) => true,
72 Self::BinanceError { code, status, .. } => {
73 is_retryable_venue_code(*code) || is_retryable_http_status(*status)
74 }
75 Self::UnexpectedStatus { status, .. } => is_retryable_http_status(*status),
76 _ => false,
77 }
78 }
79
80 #[must_use]
82 pub fn retry_after(&self) -> Option<Duration> {
83 match self {
84 Self::BinanceError { retry_after, .. } | Self::UnexpectedStatus { retry_after, .. } => {
85 *retry_after
86 }
87 _ => None,
88 }
89 }
90}
91
92impl Display for BinanceFuturesHttpError {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 match self {
95 Self::MissingCredentials => write!(f, "Missing API credentials"),
96 Self::BinanceError {
97 code,
98 message,
99 status,
100 ..
101 } => {
102 write!(f, "Binance error {code} (HTTP {status}): {message}")
103 }
104 Self::JsonError(msg) => write!(f, "JSON error: {msg}"),
105 Self::ValidationError(msg) => write!(f, "Validation error: {msg}"),
106 Self::NetworkError(msg) => write!(f, "Network error: {msg}"),
107 Self::Timeout(msg) => write!(f, "Timeout: {msg}"),
108 Self::Canceled(msg) => write!(f, "Canceled: {msg}"),
109 Self::RetryBudgetExceeded(msg) => write!(f, "Retry budget exceeded: {msg}"),
110 Self::UnexpectedStatus { status, body, .. } => {
111 write!(f, "Unexpected status {status}: {body}")
112 }
113 }
114 }
115}
116
117impl std::error::Error for BinanceFuturesHttpError {}
118
119impl From<serde_json::Error> for BinanceFuturesHttpError {
120 fn from(err: serde_json::Error) -> Self {
121 Self::JsonError(err.to_string())
122 }
123}
124
125impl From<anyhow::Error> for BinanceFuturesHttpError {
126 fn from(err: anyhow::Error) -> Self {
127 Self::NetworkError(err.to_string())
128 }
129}
130
131impl From<HttpClientError> for BinanceFuturesHttpError {
132 fn from(err: HttpClientError) -> Self {
133 match err {
134 HttpClientError::TimeoutError(msg) => Self::Timeout(msg),
135 HttpClientError::InvalidProxy(msg) | HttpClientError::ClientBuildError(msg) => {
136 Self::NetworkError(msg)
137 }
138 HttpClientError::Error(msg) | HttpClientError::TransportError(msg) => {
139 Self::NetworkError(msg)
140 }
141 }
142 }
143}
144
145pub type BinanceFuturesHttpResult<T> = Result<T, BinanceFuturesHttpError>;
147
148#[cfg(test)]
149mod tests {
150 use rstest::rstest;
151
152 use super::*;
153
154 fn venue_error(code: i64, status: u16) -> BinanceFuturesHttpError {
155 BinanceFuturesHttpError::BinanceError {
156 code,
157 message: "venue message".to_string(),
158 status,
159 retry_after: None,
160 }
161 }
162
163 #[rstest]
164 #[case::network(BinanceFuturesHttpError::NetworkError("connection reset".to_string()), true)]
165 #[case::timeout(BinanceFuturesHttpError::Timeout("timed out".to_string()), true)]
166 #[case::missing_credentials(BinanceFuturesHttpError::MissingCredentials, false)]
167 #[case::validation(BinanceFuturesHttpError::ValidationError("bad param".to_string()), false)]
168 #[case::canceled(BinanceFuturesHttpError::Canceled("shutdown".to_string()), false)]
169 #[case::budget(
170 BinanceFuturesHttpError::RetryBudgetExceeded("exceeded".to_string()),
171 false
172 )]
173 fn test_is_retryable_transport_and_local(
174 #[case] error: BinanceFuturesHttpError,
175 #[case] expected: bool,
176 ) {
177 assert_eq!(error.is_retryable(), expected);
178 }
179
180 #[rstest]
181 #[case::rate_limit_code(venue_error(-1003, 429), true)]
182 #[case::order_rate_limit_code(venue_error(-1015, 400), true)]
183 #[case::status_500(venue_error(-1000, 500), true)]
184 #[case::status_418(venue_error(-1003, 418), true)]
185 #[case::permanent_venue_code(venue_error(-1100, 400), false)]
186 #[case::auth_code(venue_error(-2015, 401), false)]
187 fn test_is_retryable_venue_errors(
188 #[case] error: BinanceFuturesHttpError,
189 #[case] expected: bool,
190 ) {
191 assert_eq!(error.is_retryable(), expected);
192 }
193
194 #[rstest]
195 fn test_retry_after_accessor() {
196 let delay = Duration::from_secs(5);
197 let error = BinanceFuturesHttpError::UnexpectedStatus {
198 status: 429,
199 body: "rate limited".to_string(),
200 retry_after: Some(delay),
201 };
202 assert_eq!(error.retry_after(), Some(delay));
203 assert_eq!(
204 BinanceFuturesHttpError::JsonError("x".to_string()).retry_after(),
205 None
206 );
207 }
208
209 #[rstest]
210 fn test_display_includes_status() {
211 let err = venue_error(-1100, 400);
212 let msg = err.to_string();
213 assert!(msg.contains("-1100"));
214 assert!(msg.contains("400"));
215 }
216}