Skip to main content

nautilus_binance/futures/http/
error.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Binance Futures HTTP error types.
17
18use 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/// Binance Futures HTTP client error type.
25#[derive(Debug)]
26pub enum BinanceFuturesHttpError {
27    /// Missing API credentials for authenticated request.
28    MissingCredentials,
29    /// Binance API returned an error response.
30    BinanceError {
31        /// Binance error code.
32        code: i64,
33        /// Error message from Binance.
34        message: String,
35        /// HTTP status of the error response.
36        status: u16,
37        /// Venue-advertised minimum retry delay from the `Retry-After` header.
38        retry_after: Option<Duration>,
39    },
40    /// JSON parsing or serialization error.
41    JsonError(String),
42    /// Request validation error.
43    ValidationError(String),
44    /// Network or connection error.
45    NetworkError(String),
46    /// Request timed out.
47    Timeout(String),
48    /// Request was canceled.
49    Canceled(String),
50    /// The retry elapsed budget was exhausted.
51    RetryBudgetExceeded(String),
52    /// Unexpected HTTP status code.
53    UnexpectedStatus {
54        /// HTTP status code.
55        status: u16,
56        /// Response body.
57        body: String,
58        /// Venue-advertised minimum retry delay from the `Retry-After` header.
59        retry_after: Option<Duration>,
60    },
61}
62
63impl BinanceFuturesHttpError {
64    /// Returns `true` if the error is transient and the operation can be retried.
65    ///
66    /// Retryability is independent of command-outcome classification: a retryable error on a
67    /// state-changing command still leaves an unknown outcome at the execution boundary.
68    #[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    /// Returns the venue-advertised minimum retry delay, when present.
81    #[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
145/// Result type for Binance Futures HTTP operations.
146pub 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}