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}
95
96impl From<String> for OKXHttpError {
97 fn from(error: String) -> Self {
98 Self::ValidationError(error)
99 }
100}
101
102impl From<serde_json::Error> for OKXHttpError {
105 fn from(error: serde_json::Error) -> Self {
106 Self::JsonError(error.to_string())
107 }
108}
109
110impl OKXHttpError {
111 #[must_use]
113 pub fn is_retryable(&self) -> bool {
114 match self {
115 Self::HttpClientError(_) => true,
116 Self::UnexpectedStatus { status, .. } => {
117 status.as_u16() >= 500 || status.as_u16() == 429
118 }
119 Self::OkxError { error_code, .. } => should_retry_error_code(error_code),
120 _ => false,
121 }
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use rstest::rstest;
128
129 use super::*;
130
131 #[rstest]
132 #[case(OKXHttpError::HttpClientError(HttpClientError::Error("timeout".to_string())), true)]
133 #[case(OKXHttpError::UnexpectedStatus { status: StatusCode::INTERNAL_SERVER_ERROR, body: String::new() }, true)]
134 #[case(OKXHttpError::UnexpectedStatus { status: StatusCode::TOO_MANY_REQUESTS, body: String::new() }, true)]
135 #[case(OKXHttpError::UnexpectedStatus { status: StatusCode::FORBIDDEN, body: String::new() }, false)]
136 #[case(OKXHttpError::OkxError { error_code: "50001".to_string(), message: String::new() }, true)]
137 #[case(OKXHttpError::OkxError { error_code: "50011".to_string(), message: String::new() }, true)]
138 #[case(OKXHttpError::OkxError { error_code: "51000".to_string(), message: String::new() }, false)]
139 #[case(OKXHttpError::JsonError("bad".to_string()), false)]
140 #[case(OKXHttpError::ValidationError("bad".to_string()), false)]
141 #[case(OKXHttpError::MissingCredentials, false)]
142 #[case(OKXHttpError::Canceled("shutdown".to_string()), false)]
143 fn test_is_retryable(#[case] error: OKXHttpError, #[case] expected: bool) {
144 assert_eq!(error.is_retryable(), expected);
145 }
146}