nautilus_okx/http/
error.rs1use nautilus_network::http::{HttpClientError, StatusCode};
24use serde::Deserialize;
25use thiserror::Error;
26
27use crate::common::consts::should_retry_error_code;
28
29#[derive(Debug, Error)]
31pub enum BuildError {
32 #[error("Missing required instrument ID")]
34 MissingInstId,
35 #[error("Missing required bar interval")]
37 MissingBar,
38 #[error("Cannot specify both 'after' and 'before' cursors")]
40 BothCursors,
41 #[error(
43 "Invalid time range: after_ms ({after_ms}) must be greater than before_ms ({before_ms})"
44 )]
45 InvalidTimeRange { after_ms: i64, before_ms: i64 },
46 #[error("Cursor timestamp appears to be in nanoseconds (> 13 digits)")]
48 CursorIsNanoseconds,
49 #[error("Limit exceeds maximum of 300")]
51 LimitTooHigh,
52}
53
54#[derive(Clone, Debug, Deserialize)]
56pub struct OKXErrorResponse {
57 pub error: OKXErrorMessage,
59}
60
61#[derive(Clone, Debug, Deserialize)]
63pub struct OKXErrorMessage {
64 pub message: String,
66 pub name: String,
68}
69
70#[derive(Debug, Error)]
72pub enum OKXHttpError {
73 #[error("Missing credentials for authenticated request")]
75 MissingCredentials,
76 #[error("OKX error {error_code}: {message}")]
78 OkxError { error_code: String, message: String },
79 #[error("JSON error: {0}")]
81 JsonError(String),
82 #[error("Parameter validation error: {0}")]
84 ValidationError(String),
85 #[error("Request canceled: {0}")]
87 Canceled(String),
88 #[error("Network error: {0}")]
90 HttpClientError(#[from] HttpClientError),
91 #[error("Unexpected HTTP status code {status}: {body}")]
93 UnexpectedStatus { status: StatusCode, body: String },
94 #[error("Operation timed out after {timeout_ms}ms")]
96 OperationTimeout { timeout_ms: u64 },
97 #[error("Retry budget exceeded: {0}")]
99 RetryBudgetExceeded(String),
100 #[error("Empty response")]
102 EmptyResponse,
103}
104
105impl From<String> for OKXHttpError {
106 fn from(error: String) -> Self {
107 Self::ValidationError(error)
108 }
109}
110
111impl From<serde_json::Error> for OKXHttpError {
114 fn from(error: serde_json::Error) -> Self {
115 Self::JsonError(error.to_string())
116 }
117}
118
119impl OKXHttpError {
120 #[must_use]
122 pub fn is_order_not_found(&self) -> bool {
123 matches!(
124 self,
125 Self::OkxError { error_code, .. } if error_code == "51603"
126 )
127 }
128
129 #[must_use]
131 pub fn is_retryable(&self) -> bool {
132 match self {
133 Self::HttpClientError(_) | Self::OperationTimeout { .. } => true,
134 Self::UnexpectedStatus { status, .. } => {
135 status.as_u16() >= 500 || status.as_u16() == 429
136 }
137 Self::OkxError { error_code, .. } => should_retry_error_code(error_code),
138 _ => false,
139 }
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use rstest::rstest;
146
147 use super::*;
148
149 #[rstest]
150 #[case(OKXHttpError::HttpClientError(HttpClientError::Error("timeout".to_string())), true)]
151 #[case(OKXHttpError::UnexpectedStatus { status: StatusCode::INTERNAL_SERVER_ERROR, body: String::new() }, true)]
152 #[case(OKXHttpError::UnexpectedStatus { status: StatusCode::TOO_MANY_REQUESTS, body: String::new() }, true)]
153 #[case(OKXHttpError::UnexpectedStatus { status: StatusCode::FORBIDDEN, body: String::new() }, false)]
154 #[case(OKXHttpError::OkxError { error_code: "50001".to_string(), message: String::new() }, true)]
155 #[case(OKXHttpError::OkxError { error_code: "50011".to_string(), message: String::new() }, true)]
156 #[case(OKXHttpError::OkxError { error_code: "51000".to_string(), message: String::new() }, false)]
157 #[case(OKXHttpError::JsonError("bad".to_string()), false)]
158 #[case(OKXHttpError::ValidationError("bad".to_string()), false)]
159 #[case(OKXHttpError::MissingCredentials, false)]
160 #[case(OKXHttpError::Canceled("shutdown".to_string()), false)]
161 #[case(OKXHttpError::OperationTimeout { timeout_ms: 1_000 }, true)]
162 #[case(OKXHttpError::RetryBudgetExceeded("budget".to_string()), false)]
163 #[case(OKXHttpError::EmptyResponse, false)]
164 fn test_is_retryable(#[case] error: OKXHttpError, #[case] expected: bool) {
165 assert_eq!(error.is_retryable(), expected);
166 }
167
168 #[rstest]
169 #[case(OKXHttpError::OkxError {
170 error_code: "51603".to_string(),
171 message: "Order does not exist".to_string(),
172 }, true)]
173 #[case(OKXHttpError::OkxError {
174 error_code: "51000".to_string(),
175 message: "Parameter error".to_string(),
176 }, false)]
177 #[case(OKXHttpError::ValidationError("bad".to_string()), false)]
178 fn test_is_order_not_found(#[case] error: OKXHttpError, #[case] expected: bool) {
179 assert_eq!(error.is_order_not_found(), expected);
180 }
181}