nautilus_lighter/http/
error.rs1use std::time::Duration;
19
20use nautilus_network::http::HttpClientError;
21use thiserror::Error;
22
23pub type LighterHttpResult<T> = Result<T, LighterHttpError>;
25
26#[derive(Debug, Clone, Error)]
28pub enum LighterHttpError {
29 #[error("network error: {0}")]
31 Network(String),
32 #[error("HTTP {status}: {body}")]
34 Http {
35 status: u16,
36 body: String,
37 retry_after: Option<Duration>,
38 },
39 #[error("rate limit exceeded: {message}")]
41 RateLimit {
42 message: String,
43 retry_after: Option<Duration>,
44 },
45 #[error("venue error {code}: {message}")]
47 Venue { code: i64, message: String },
48 #[error("{data_type} history incomplete after {pages} pages")]
50 HistoryIncomplete {
51 data_type: &'static str,
52 pages: usize,
53 },
54 #[error("parse error: {0}")]
56 Parse(String),
57}
58
59impl LighterHttpError {
60 pub(super) const fn retry_after(&self) -> Option<Duration> {
61 match self {
62 Self::Http { retry_after, .. } | Self::RateLimit { retry_after, .. } => *retry_after,
63 _ => None,
64 }
65 }
66}
67
68impl From<HttpClientError> for LighterHttpError {
69 fn from(error: HttpClientError) -> Self {
70 Self::Network(error.to_string())
71 }
72}
73
74impl From<serde_json::Error> for LighterHttpError {
75 fn from(error: serde_json::Error) -> Self {
76 Self::Parse(error.to_string())
77 }
78}
79
80impl From<anyhow::Error> for LighterHttpError {
81 fn from(error: anyhow::Error) -> Self {
82 Self::Parse(error.to_string())
83 }
84}
85
86#[must_use]
91pub fn should_retry_lighter_http_error(error: &LighterHttpError) -> bool {
92 match error {
93 LighterHttpError::Network(_) | LighterHttpError::RateLimit { .. } => true,
94 LighterHttpError::Http { status, .. } => *status >= 500,
95 LighterHttpError::Venue { .. }
96 | LighterHttpError::HistoryIncomplete { .. }
97 | LighterHttpError::Parse(_) => false,
98 }
99}
100
101#[must_use]
103pub fn create_lighter_http_timeout_error(msg: String) -> LighterHttpError {
104 LighterHttpError::Network(msg)
105}
106
107#[cfg(test)]
108mod tests {
109 use rstest::rstest;
110
111 use super::*;
112
113 #[rstest]
114 #[case::network_retries(LighterHttpError::Network("dns failure".into()), true)]
115 #[case::rate_limit_retries(LighterHttpError::RateLimit { message: "429".into(), retry_after: None }, true)]
116 #[case::server_5xx_retries(LighterHttpError::Http { status: 503, body: "busy".into(), retry_after: None }, true)]
117 #[case::server_500_retries(LighterHttpError::Http { status: 500, body: "boom".into(), retry_after: None }, true)]
118 #[case::client_400_does_not_retry(LighterHttpError::Http { status: 400, body: "bad".into(), retry_after: None }, false)]
119 #[case::client_404_does_not_retry(LighterHttpError::Http { status: 404, body: "missing".into(), retry_after: None }, false)]
120 #[case::venue_does_not_retry(LighterHttpError::Venue { code: 20001, message: "invalid".into() }, false)]
121 #[case::incomplete_history_does_not_retry(
122 LighterHttpError::HistoryIncomplete {
123 data_type: "funding rate",
124 pages: 500,
125 },
126 false
127 )]
128 #[case::parse_does_not_retry(LighterHttpError::Parse("bad json".into()), false)]
129 fn test_should_retry_lighter_http_error(
130 #[case] error: LighterHttpError,
131 #[case] expected: bool,
132 ) {
133 assert_eq!(should_retry_lighter_http_error(&error), expected);
134 }
135}