Skip to main content

bybit_client/
error.rs

1//! Error types for the Bybit client library.
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6/// Main error type for the Bybit client.
7#[derive(Error, Debug)]
8pub enum BybitError {
9    /// API returned an error response.
10    #[error("API error (code={code}): {message}")]
11    Api {
12        code: i32,
13        message: String,
14        #[source]
15        source: Option<Box<dyn std::error::Error + Send + Sync>>,
16    },
17
18    /// HTTP request failed.
19    #[error("HTTP error: {0}")]
20    Http(#[from] reqwest::Error),
21
22    /// Failed to serialize request parameters.
23    #[error("Serialization error: {0}")]
24    Serialization(#[from] serde_json::Error),
25
26    /// Failed to build URL.
27    #[error("URL error: {0}")]
28    Url(#[from] url::ParseError),
29
30    /// Authentication error.
31    #[error("Authentication error: {0}")]
32    Auth(String),
33
34    /// Invalid parameter provided.
35    #[error("Invalid parameter: {0}")]
36    InvalidParameter(String),
37
38    /// WebSocket error.
39    #[error("WebSocket error: {0}")]
40    WebSocket(String),
41
42    /// Rate limit exceeded.
43    #[error("Rate limit exceeded: {message}")]
44    RateLimit {
45        message: String,
46        retry_after_ms: Option<u64>,
47    },
48
49    /// Request timeout.
50    #[error("Request timeout")]
51    Timeout,
52
53    /// Configuration error.
54    #[error("Configuration error: {0}")]
55    Config(String),
56}
57
58impl BybitError {
59    /// Create a new API error from response data.
60    pub fn api_error(code: i32, message: impl Into<String>) -> Self {
61        BybitError::Api {
62            code,
63            message: message.into(),
64            source: None,
65        }
66    }
67
68    /// Check if this error is retryable.
69    pub fn is_retryable(&self) -> bool {
70        match self {
71            BybitError::Http(e) => e.is_timeout() || e.is_connect(),
72            BybitError::RateLimit { .. } => true,
73            BybitError::Timeout => true,
74            BybitError::Api { code, .. } => {
75                matches!(code, 10002 | 10006 | 30034 | 30035 | 130035 | 130150)
76            }
77            _ => false,
78        }
79    }
80
81    /// Get the API error code if this is an API error.
82    pub fn api_code(&self) -> Option<i32> {
83        match self {
84            BybitError::Api { code, .. } => Some(*code),
85            _ => None,
86        }
87    }
88}
89
90/// API response structure from Bybit V5.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct ApiResponse<T> {
94    /// Return code. 0 means success.
95    pub ret_code: i32,
96    /// Return message.
97    pub ret_msg: String,
98    /// Response result data.
99    pub result: T,
100    /// Extended info (usually empty).
101    #[serde(default)]
102    pub ret_ext_info: serde_json::Value,
103    /// Server timestamp in milliseconds.
104    #[serde(default)]
105    pub time: u64,
106}
107
108impl<T> ApiResponse<T> {
109    /// Check if the response indicates success.
110    pub fn is_success(&self) -> bool {
111        self.ret_code == 0
112    }
113
114    /// Convert to a Result, returning an error if ret_code is non-zero.
115    pub fn into_result(self) -> Result<T, BybitError> {
116        if self.is_success() {
117            Ok(self.result)
118        } else {
119            Err(BybitError::api_error(self.ret_code, self.ret_msg))
120        }
121    }
122}
123
124/// Paginated list response.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase")]
127pub struct ListResult<T> {
128    /// Category (if applicable).
129    #[serde(default)]
130    pub category: Option<String>,
131    /// List of items.
132    pub list: Vec<T>,
133    /// Cursor for next page (if paginated).
134    #[serde(default)]
135    pub next_page_cursor: Option<String>,
136}
137
138impl<T> ListResult<T> {
139    /// Check if there are more pages.
140    pub fn has_next_page(&self) -> bool {
141        self.next_page_cursor
142            .as_ref()
143            .map(|c| !c.is_empty())
144            .unwrap_or(false)
145    }
146}
147
148/// Known API error codes.
149pub mod error_codes {
150    /// Success
151    pub const SUCCESS: i32 = 0;
152
153    /// Parameters missing or wrong
154    pub const PARAMS_MISSING_OR_WRONG: i32 = 10001;
155
156    /// Request timeout
157    pub const REQUEST_TIMEOUT: i32 = 10002;
158
159    /// Invalid API key or permissions
160    pub const INVALID_API_KEY: i32 = 10003;
161
162    /// Signature not valid
163    pub const INVALID_SIGNATURE: i32 = 10004;
164
165    /// Incorrect API key permissions
166    pub const INCORRECT_PERMISSIONS: i32 = 10005;
167
168    /// Too many requests (IP rate limit)
169    pub const IP_RATE_LIMIT: i32 = 10006;
170
171    /// Incorrect API request IP
172    pub const INCORRECT_IP: i32 = 10010;
173
174    /// Account not unified
175    pub const ACCOUNT_NOT_UNIFIED: i32 = 10020;
176
177    /// Order not found or too late to cancel
178    pub const ORDER_NOT_FOUND: i32 = 20001;
179
180    /// Insufficient balance for order
181    pub const INSUFFICIENT_BALANCE: i32 = 30031;
182
183    /// V5: Order not found
184    pub const V5_ORDER_NOT_FOUND: i32 = 110001;
185
186    /// V5: Insufficient balance
187    pub const V5_INSUFFICIENT_BALANCE: i32 = 110007;
188
189    /// V5: Position not found
190    pub const V5_POSITION_NOT_FOUND: i32 = 110009;
191
192    /// V5: Order quantity exceeds limit
193    pub const V5_QTY_EXCEEDS_LIMIT: i32 = 110012;
194
195    /// V5: Order price out of range
196    pub const V5_PRICE_OUT_OF_RANGE: i32 = 110013;
197
198    /// V5: Reduce only order would increase position
199    pub const V5_REDUCE_ONLY_VIOLATION: i32 = 110017;
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn test_api_response_success() {
208        let json = r#"{
209            "retCode": 0,
210            "retMsg": "OK",
211            "result": {"value": 123},
212            "time": 1234567890
213        }"#;
214
215        let response: ApiResponse<serde_json::Value> = match serde_json::from_str(json) {
216            Ok(value) => value,
217            Err(err) => panic!("Failed to parse response JSON: {}", err),
218        };
219        assert!(response.is_success());
220        assert_eq!(response.result["value"], 123);
221    }
222
223    #[test]
224    fn test_api_response_error() {
225        let json = r#"{
226            "retCode": 10001,
227            "retMsg": "Param error",
228            "result": {},
229            "time": 1234567890
230        }"#;
231
232        let response: ApiResponse<serde_json::Value> = match serde_json::from_str(json) {
233            Ok(value) => value,
234            Err(err) => panic!("Failed to parse response JSON: {}", err),
235        };
236        assert!(!response.is_success());
237
238        let err = match response.into_result() {
239            Ok(_) => panic!("Expected error response"),
240            Err(err) => err,
241        };
242        assert_eq!(err.api_code(), Some(10001));
243    }
244
245    #[test]
246    fn test_error_retryable() {
247        let api_err = BybitError::api_error(10002, "timeout");
248        assert!(api_err.is_retryable());
249
250        let param_err = BybitError::api_error(10001, "param error");
251        assert!(!param_err.is_retryable());
252    }
253}