1use crate::ret_code;
2
3#[derive(Debug)]
5pub enum Error {
6 Api {
8 code: i64,
10 msg: String,
12 },
13 Http {
16 status: u16,
18 body: String,
20 },
21 InvalidHeaderValue(reqwest::header::InvalidHeaderValue),
23 Io(std::io::Error),
25 MissingCredentials,
28 Msg(String),
30 RateLimited {
33 retry_after_ms: u64,
35 },
36 RateLimitUnsatisfiable {
40 cost: u32,
42 burst: u32,
44 },
45 Reqwest(reqwest::Error),
47 SerdeJson(serde_json::Error),
49 TooManyPages {
51 max: usize,
53 },
54 SerdeUrlEncoded(serde_urlencoded::ser::Error),
56 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 pub fn api_code(&self) -> Option<i64> {
100 match self {
101 Error::Api { code, .. } => Some(*code),
102 _ => None,
103 }
104 }
105
106 pub fn is_timestamp_error(&self) -> bool {
109 self.api_code() == Some(ret_code::TIMESTAMP_OUT_OF_WINDOW)
110 }
111
112 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 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 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}