Skip to main content

bybit/
error.rs

1use crate::ret_code;
2
3/// Errors returned by [`http::Client`](crate::http::Client).
4#[derive(Debug)]
5pub enum Error {
6    /// Bybit answered with a non-zero `retCode` (see [`ret_code`](crate::ret_code)).
7    Api {
8        /// Bybit `retCode`.
9        code: i64,
10        /// Bybit `retMsg`.
11        msg: String,
12    },
13    /// A non-2xx HTTP response whose body is not a Bybit error envelope
14    /// (e.g. an HTML page from a CDN or gateway). `body` may be truncated.
15    Http {
16        /// HTTP status code.
17        status: u16,
18        /// Response body, truncated to 1 KiB.
19        body: String,
20    },
21    /// A header value (API key, signature, referer) is not a valid HTTP header.
22    InvalidHeaderValue(reqwest::header::InvalidHeaderValue),
23    /// I/O error.
24    Io(std::io::Error),
25    /// A private endpoint was called on a client created without
26    /// `api_key`/`api_secret`.
27    MissingCredentials,
28    /// Other error with a message.
29    Msg(String),
30    /// The local rate limiter rejected the request before it was sent.
31    /// `retry_after_ms` is the estimated wait until capacity is available.
32    RateLimited {
33        /// Estimated wait until the bucket has capacity, milliseconds.
34        retry_after_ms: u64,
35    },
36    /// The local rate limiter can never let this request through: its cost
37    /// exceeds the bucket's burst, or the bucket does not refill. Retrying
38    /// will not help; adjust [`RateLimiterConfig`](crate::http::RateLimiterConfig).
39    RateLimitUnsatisfiable {
40        /// Cost of the request (e.g. number of orders in a batch).
41        cost: u32,
42        /// Burst capacity of the bucket.
43        burst: u32,
44    },
45    /// Transport error (connection, timeout, TLS, ...).
46    Reqwest(reqwest::Error),
47    /// JSON (de)serialization error.
48    SerdeJson(serde_json::Error),
49    /// A `*_all` pagination helper fetched `max` pages without reaching the end.
50    TooManyPages {
51        /// The page limit, [`MAX_PAGES`](crate::http::MAX_PAGES).
52        max: usize,
53    },
54    /// Query string serialization error.
55    SerdeUrlEncoded(serde_urlencoded::ser::Error),
56    /// JSON deserialization error with the path of the offending field.
57    SerdePathToError(serde_path_to_error::Error<serde_json::Error>),
58}
59
60impl std::fmt::Display for Error {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        match self {
63            Error::Api { code, msg } => write!(f, "Bybit API error: code: {code}, message: {msg}"),
64            Error::Http { status, body } => write!(f, "HTTP error: status: {status}, body: {body}"),
65            Error::InvalidHeaderValue(error) => write!(f, "invalid header value: {error}"),
66            Error::Io(error) => write!(f, "I/O error: {error}"),
67            Error::MissingCredentials => {
68                write!(
69                    f,
70                    "api_key and api_secret are required for private endpoints"
71                )
72            }
73            Error::Msg(msg) => write!(f, "{msg}"),
74            Error::RateLimited { retry_after_ms } => {
75                write!(f, "rate limited: retry after {retry_after_ms} ms")
76            }
77            Error::RateLimitUnsatisfiable { cost, burst } => write!(
78                f,
79                "rate limit can never be satisfied: request cost {cost}, bucket burst {burst}"
80            ),
81            Error::Reqwest(error) => write!(f, "reqwest error: {error}"),
82            Error::TooManyPages { max } => {
83                write!(f, "pagination did not finish after {max} pages")
84            }
85            Error::SerdeJson(error) => write!(f, "serde_json error: {error}"),
86            Error::SerdeUrlEncoded(error) => write!(f, "serde_urlencoded error: {error}"),
87            Error::SerdePathToError(error) => write!(
88                f,
89                "serde_path_to_error error: path: {}, msg: {}",
90                error.path(),
91                error.inner()
92            ),
93        }
94    }
95}
96
97impl Error {
98    /// The Bybit `retCode` of an [`Error::Api`] (see [`ret_code`](crate::ret_code)).
99    pub fn api_code(&self) -> Option<i64> {
100        match self {
101            Error::Api { code, .. } => Some(*code),
102            _ => None,
103        }
104    }
105
106    /// Bybit rejected the request timestamp (`retCode` 10002): the local clock
107    /// is off; see [`http::Client::sync_time`](crate::http::Client::sync_time).
108    pub fn is_timestamp_error(&self) -> bool {
109        self.api_code() == Some(ret_code::TIMESTAMP_OUT_OF_WINDOW)
110    }
111
112    /// A rate limit was hit: locally ([`Error::RateLimited`]), by Bybit
113    /// (`retCode` 10006/10018) or as HTTP 429.
114    pub fn is_rate_limited(&self) -> bool {
115        match self {
116            Error::RateLimited { .. } => true,
117            Error::Api { code, .. } => {
118                matches!(*code, ret_code::RATE_LIMITED | ret_code::IP_RATE_LIMITED)
119            }
120            Error::Http { status, .. } => *status == 429,
121            _ => false,
122        }
123    }
124
125    /// The request was refused because the value is already set (e.g.
126    /// "leverage not modified"); usually this can be treated as success.
127    pub fn is_not_modified(&self) -> bool {
128        matches!(
129            self.api_code(),
130            Some(ret_code::POSITION_MODE_NOT_MODIFIED | ret_code::LEVERAGE_NOT_MODIFIED)
131        )
132    }
133
134    /// Sending the same request again later may succeed: network errors and
135    /// timeouts, rate limits, server errors (HTTP 5xx, `retCode` 10016) and
136    /// timestamp errors (after [`sync_time`](crate::http::Client::sync_time)).
137    ///
138    /// For orders a timeout does not prove the order was not placed: check
139    /// it (e.g. by `orderLinkId`) before placing it again.
140    pub fn is_retryable(&self) -> bool {
141        match self {
142            Error::Reqwest(e) => e.is_timeout() || e.is_connect() || e.is_request(),
143            Error::Http { status, .. } => *status == 429 || *status >= 500,
144            Error::Api { code, .. } => matches!(
145                *code,
146                ret_code::RATE_LIMITED
147                    | ret_code::IP_RATE_LIMITED
148                    | ret_code::SERVER_ERROR
149                    | ret_code::TIMESTAMP_OUT_OF_WINDOW
150            ),
151            Error::RateLimited { .. } | Error::Io(_) => true,
152            _ => false,
153        }
154    }
155}
156
157impl std::error::Error for Error {
158    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
159        match self {
160            Error::InvalidHeaderValue(error) => Some(error),
161            Error::Io(error) => Some(error),
162            Error::Reqwest(error) => Some(error),
163            Error::SerdeJson(error) => Some(error),
164            Error::SerdeUrlEncoded(error) => Some(error),
165            Error::SerdePathToError(error) => Some(error),
166            Error::Api { .. }
167            | Error::Http { .. }
168            | Error::MissingCredentials
169            | Error::Msg(_)
170            | Error::RateLimited { .. }
171            | Error::RateLimitUnsatisfiable { .. }
172            | Error::TooManyPages { .. } => None,
173        }
174    }
175}
176
177impl From<std::io::Error> for Error {
178    fn from(err: std::io::Error) -> Self {
179        Error::Io(err)
180    }
181}
182
183impl From<String> for Error {
184    fn from(msg: String) -> Self {
185        Error::Msg(msg)
186    }
187}
188
189impl From<&str> for Error {
190    fn from(msg: &str) -> Self {
191        Error::Msg(msg.to_string())
192    }
193}
194
195impl From<super::http::APIErrorResponse> for Error {
196    fn from(resp: super::http::APIErrorResponse) -> Self {
197        Self::Api {
198            code: resp.ret_code,
199            msg: resp.ret_msg,
200        }
201    }
202}
203
204impl From<reqwest::header::InvalidHeaderValue> for Error {
205    fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
206        Error::InvalidHeaderValue(err)
207    }
208}
209
210impl From<reqwest::Error> for Error {
211    fn from(err: reqwest::Error) -> Self {
212        Error::Reqwest(err)
213    }
214}
215
216impl From<serde_json::Error> for Error {
217    fn from(err: serde_json::Error) -> Self {
218        Error::SerdeJson(err)
219    }
220}
221
222impl From<serde_urlencoded::ser::Error> for Error {
223    fn from(err: serde_urlencoded::ser::Error) -> Self {
224        Error::SerdeUrlEncoded(err)
225    }
226}
227
228impl From<serde_path_to_error::Error<serde_json::Error>> for Error {
229    fn from(err: serde_path_to_error::Error<serde_json::Error>) -> Self {
230        Error::SerdePathToError(err)
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    fn api(code: i64) -> Error {
239        Error::Api {
240            code,
241            msg: String::new(),
242        }
243    }
244
245    #[test]
246    fn classifies_api_errors() {
247        assert_eq!(api(110001).api_code(), Some(ret_code::ORDER_NOT_FOUND));
248        assert_eq!(Error::MissingCredentials.api_code(), None);
249
250        assert!(api(10002).is_timestamp_error());
251        assert!(!api(10001).is_timestamp_error());
252
253        assert!(api(10006).is_rate_limited());
254        assert!(api(10018).is_rate_limited());
255        assert!(Error::RateLimited { retry_after_ms: 1 }.is_rate_limited());
256        assert!(
257            Error::Http {
258                status: 429,
259                body: String::new()
260            }
261            .is_rate_limited()
262        );
263        assert!(!api(10001).is_rate_limited());
264
265        assert!(api(110043).is_not_modified());
266        assert!(api(110025).is_not_modified());
267        assert!(!api(110001).is_not_modified());
268    }
269
270    #[test]
271    fn retryable_errors() {
272        for retryable in [
273            api(10002),
274            api(10006),
275            api(10016),
276            api(10018),
277            Error::Http {
278                status: 503,
279                body: String::new(),
280            },
281            Error::Http {
282                status: 429,
283                body: String::new(),
284            },
285            Error::RateLimited { retry_after_ms: 1 },
286        ] {
287            assert!(retryable.is_retryable(), "{retryable:?}");
288        }
289        for permanent in [
290            api(10001),
291            api(10003),
292            api(110007),
293            Error::Http {
294                status: 403,
295                body: String::new(),
296            },
297            Error::MissingCredentials,
298            Error::RateLimitUnsatisfiable { cost: 2, burst: 1 },
299            Error::TooManyPages { max: 1 },
300        ] {
301            assert!(!permanent.is_retryable(), "{permanent:?}");
302        }
303    }
304}