Skip to main content

nautilus_binance/common/
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//! Adapter-level error types aggregating HTTP and WebSocket errors.
17
18use std::fmt::Display;
19
20/// Binance WebSocket streams error type shared by spot and futures clients.
21#[derive(Debug)]
22pub enum BinanceWsError {
23    /// General client error.
24    ClientError(String),
25    /// Authentication failed.
26    AuthenticationError(String),
27    /// Message parsing error.
28    ParseError(String),
29    /// Network or connection error.
30    NetworkError(String),
31    /// Operation timed out.
32    Timeout(String),
33}
34
35impl Display for BinanceWsError {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        match self {
38            Self::ClientError(msg) => write!(f, "Client error: {msg}"),
39            Self::AuthenticationError(msg) => write!(f, "Authentication error: {msg}"),
40            Self::ParseError(msg) => write!(f, "Parse error: {msg}"),
41            Self::NetworkError(msg) => write!(f, "Network error: {msg}"),
42            Self::Timeout(msg) => write!(f, "Timeout: {msg}"),
43        }
44    }
45}
46
47impl std::error::Error for BinanceWsError {}
48
49/// Result type for Binance WebSocket stream operations.
50pub type BinanceWsResult<T> = Result<T, BinanceWsError>;
51
52/// Adapter-level error aggregating HTTP, WebSocket, and SBE errors.
53#[derive(Debug, thiserror::Error)]
54pub enum BinanceError {
55    /// A Spot HTTP API error.
56    #[error("Spot HTTP error: {0}")]
57    SpotHttp(#[from] crate::spot::http::error::BinanceSpotHttpError),
58
59    /// A Futures HTTP API error.
60    #[error("Futures HTTP error: {0}")]
61    FuturesHttp(#[from] crate::futures::http::error::BinanceFuturesHttpError),
62
63    /// A WebSocket streams error (spot or futures).
64    #[error("WebSocket error: {0}")]
65    WebSocket(#[from] BinanceWsError),
66
67    /// A Spot WebSocket Trading API error.
68    #[error("Spot WS API error: {0}")]
69    SpotWsApi(#[from] crate::spot::websocket::trading::error::BinanceWsApiError),
70
71    /// A Futures WebSocket Trading API error.
72    #[error("Futures WS API error: {0}")]
73    FuturesWsApi(#[from] crate::futures::websocket::trading::error::BinanceFuturesWsApiError),
74
75    /// A configuration or build error.
76    #[error("Config error: {0}")]
77    Config(String),
78}
79
80/// Binance error codes indicating authentication or permission failures.
81const BINANCE_AUTH_ERROR_CODES: [i64; 3] = [
82    -2015, // Invalid API-key, IP, or permissions for action
83    -2014, // API-key format invalid
84    -1022, // Signature for this request is not valid
85];
86
87/// Binance error codes indicating rate limiting or throttling.
88const BINANCE_RATE_LIMIT_ERROR_CODES: [i64; 2] = [
89    -1003, // Too many requests; WAF limit violated
90    -1015, // Too many new orders; rate limit violated
91];
92
93impl BinanceError {
94    /// Returns `true` if the error is likely transient and the operation can be retried.
95    #[must_use]
96    pub fn is_retryable(&self) -> bool {
97        match self {
98            Self::SpotHttp(e) => match e {
99                crate::spot::http::error::BinanceSpotHttpError::NetworkError(_)
100                | crate::spot::http::error::BinanceSpotHttpError::Timeout(_) => true,
101                crate::spot::http::error::BinanceSpotHttpError::BinanceError { code, .. } => {
102                    BINANCE_RATE_LIMIT_ERROR_CODES.contains(code)
103                }
104                crate::spot::http::error::BinanceSpotHttpError::UnexpectedStatus {
105                    status, ..
106                } => *status == 429 || *status >= 500,
107                _ => false,
108            },
109            Self::FuturesHttp(e) => match e {
110                crate::futures::http::error::BinanceFuturesHttpError::NetworkError(_)
111                | crate::futures::http::error::BinanceFuturesHttpError::Timeout(_) => true,
112                crate::futures::http::error::BinanceFuturesHttpError::BinanceError {
113                    code, ..
114                } => BINANCE_RATE_LIMIT_ERROR_CODES.contains(code),
115                crate::futures::http::error::BinanceFuturesHttpError::UnexpectedStatus {
116                    status,
117                    ..
118                } => *status == 429 || *status >= 500,
119                _ => false,
120            },
121            Self::WebSocket(e) => matches!(
122                e,
123                BinanceWsError::NetworkError(_) | BinanceWsError::Timeout(_)
124            ),
125            Self::SpotWsApi(e) => matches!(
126                e,
127                crate::spot::websocket::trading::error::BinanceWsApiError::ConnectionError(_)
128                    | crate::spot::websocket::trading::error::BinanceWsApiError::Timeout(_)
129            ),
130            Self::FuturesWsApi(e) => matches!(
131                e,
132                crate::futures::websocket::trading::error::BinanceFuturesWsApiError::ConnectionError(_)
133            ),
134            Self::Config(_) => false,
135        }
136    }
137
138    /// Returns `true` if the error is fatal and requires intervention.
139    #[must_use]
140    pub fn is_fatal(&self) -> bool {
141        match self {
142            Self::SpotHttp(e) => match e {
143                crate::spot::http::error::BinanceSpotHttpError::MissingCredentials => true,
144                crate::spot::http::error::BinanceSpotHttpError::BinanceError { code, .. } => {
145                    BINANCE_AUTH_ERROR_CODES.contains(code)
146                }
147                crate::spot::http::error::BinanceSpotHttpError::UnexpectedStatus {
148                    status, ..
149                } => *status == 401 || *status == 403,
150                _ => false,
151            },
152            Self::FuturesHttp(e) => match e {
153                crate::futures::http::error::BinanceFuturesHttpError::MissingCredentials => true,
154                crate::futures::http::error::BinanceFuturesHttpError::BinanceError {
155                    code, ..
156                } => BINANCE_AUTH_ERROR_CODES.contains(code),
157                crate::futures::http::error::BinanceFuturesHttpError::UnexpectedStatus {
158                    status,
159                    ..
160                } => *status == 401 || *status == 403,
161                _ => false,
162            },
163            Self::WebSocket(e) => {
164                matches!(e, BinanceWsError::AuthenticationError(_))
165            }
166            Self::SpotWsApi(e) => matches!(
167                e,
168                crate::spot::websocket::trading::error::BinanceWsApiError::AuthenticationError(_)
169            ),
170            Self::FuturesWsApi(_) => false,
171            Self::Config(_) => true,
172        }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use rstest::rstest;
179
180    use super::*;
181    use crate::{
182        futures::http::error::BinanceFuturesHttpError, spot::http::error::BinanceSpotHttpError,
183    };
184
185    #[rstest]
186    fn test_spot_http_network_error_is_retryable() {
187        let err = BinanceError::SpotHttp(BinanceSpotHttpError::NetworkError(
188            "connection reset".to_string(),
189        ));
190        assert!(err.is_retryable());
191        assert!(!err.is_fatal());
192    }
193
194    #[rstest]
195    fn test_spot_http_timeout_is_retryable() {
196        let err = BinanceError::SpotHttp(BinanceSpotHttpError::Timeout("timed out".to_string()));
197        assert!(err.is_retryable());
198    }
199
200    #[rstest]
201    fn test_spot_http_missing_credentials_is_fatal() {
202        let err = BinanceError::SpotHttp(BinanceSpotHttpError::MissingCredentials);
203        assert!(err.is_fatal());
204        assert!(!err.is_retryable());
205    }
206
207    #[rstest]
208    fn test_spot_http_binance_error_is_not_retryable() {
209        let err = BinanceError::SpotHttp(BinanceSpotHttpError::BinanceError {
210            code: -1021,
211            message: "Timestamp for this request was 1000ms ahead".to_string(),
212        });
213        assert!(!err.is_retryable());
214        assert!(!err.is_fatal());
215    }
216
217    #[rstest]
218    fn test_futures_http_network_error_is_retryable() {
219        let err = BinanceError::FuturesHttp(BinanceFuturesHttpError::NetworkError(
220            "connection refused".to_string(),
221        ));
222        assert!(err.is_retryable());
223        assert!(!err.is_fatal());
224    }
225
226    #[rstest]
227    fn test_futures_http_missing_credentials_is_fatal() {
228        let err = BinanceError::FuturesHttp(BinanceFuturesHttpError::MissingCredentials);
229        assert!(err.is_fatal());
230        assert!(!err.is_retryable());
231    }
232
233    #[rstest]
234    fn test_ws_auth_error_is_fatal() {
235        let err = BinanceError::WebSocket(BinanceWsError::AuthenticationError(
236            "invalid key".to_string(),
237        ));
238        assert!(err.is_fatal());
239        assert!(!err.is_retryable());
240    }
241
242    #[rstest]
243    fn test_ws_network_error_is_retryable() {
244        let err =
245            BinanceError::WebSocket(BinanceWsError::NetworkError("connection lost".to_string()));
246        assert!(err.is_retryable());
247        assert!(!err.is_fatal());
248    }
249
250    #[rstest]
251    fn test_config_error_is_fatal() {
252        let err = BinanceError::Config("invalid product type".to_string());
253        assert!(err.is_fatal());
254        assert!(!err.is_retryable());
255    }
256
257    #[rstest]
258    fn test_spot_http_auth_error_code_is_fatal() {
259        let err = BinanceError::SpotHttp(BinanceSpotHttpError::BinanceError {
260            code: -2015,
261            message: "Invalid API-key, IP, or permissions for action".to_string(),
262        });
263        assert!(err.is_fatal());
264        assert!(!err.is_retryable());
265    }
266
267    #[rstest]
268    fn test_futures_http_auth_error_code_is_fatal() {
269        let err = BinanceError::FuturesHttp(BinanceFuturesHttpError::BinanceError {
270            code: -2015,
271            message: "Invalid API-key".to_string(),
272        });
273        assert!(err.is_fatal());
274        assert!(!err.is_retryable());
275    }
276
277    #[rstest]
278    fn test_spot_http_invalid_signature_is_fatal() {
279        let err = BinanceError::SpotHttp(BinanceSpotHttpError::BinanceError {
280            code: -1022,
281            message: "Signature for this request is not valid".to_string(),
282        });
283        assert!(err.is_fatal());
284    }
285
286    #[rstest]
287    fn test_spot_http_rate_limit_is_retryable() {
288        let err = BinanceError::SpotHttp(BinanceSpotHttpError::BinanceError {
289            code: -1015,
290            message: "Too many new orders".to_string(),
291        });
292        assert!(err.is_retryable());
293        assert!(!err.is_fatal());
294    }
295
296    #[rstest]
297    fn test_futures_http_rate_limit_is_retryable() {
298        let err = BinanceError::FuturesHttp(BinanceFuturesHttpError::BinanceError {
299            code: -1003,
300            message: "Too many requests".to_string(),
301        });
302        assert!(err.is_retryable());
303        assert!(!err.is_fatal());
304    }
305
306    #[rstest]
307    fn test_spot_http_unexpected_status_429_is_retryable() {
308        let err = BinanceError::SpotHttp(BinanceSpotHttpError::UnexpectedStatus {
309            status: 429,
310            body: "rate limited".to_string(),
311        });
312        assert!(err.is_retryable());
313    }
314
315    #[rstest]
316    fn test_spot_http_unexpected_status_500_is_retryable() {
317        let err = BinanceError::SpotHttp(BinanceSpotHttpError::UnexpectedStatus {
318            status: 500,
319            body: "internal server error".to_string(),
320        });
321        assert!(err.is_retryable());
322    }
323
324    #[rstest]
325    fn test_spot_http_unexpected_status_401_is_fatal() {
326        let err = BinanceError::SpotHttp(BinanceSpotHttpError::UnexpectedStatus {
327            status: 401,
328            body: "unauthorized".to_string(),
329        });
330        assert!(err.is_fatal());
331        assert!(!err.is_retryable());
332    }
333
334    #[rstest]
335    fn test_display_formatting() {
336        let err = BinanceError::SpotHttp(BinanceSpotHttpError::BinanceError {
337            code: -1100,
338            message: "Illegal characters found".to_string(),
339        });
340        let msg = err.to_string();
341        assert!(msg.contains("Spot HTTP error"));
342        assert!(msg.contains("-1100"));
343    }
344}