nautilus_architect_ax/http/
error.rs1use nautilus_network::http::HttpClientError;
19use serde::{Deserialize, Serialize};
20use thiserror::Error;
21
22#[derive(Debug, Clone, Error)]
24pub enum AxBuildError {
25 #[error("Missing required symbol")]
27 MissingSymbol,
28 #[error("Invalid limit: {0}")]
30 InvalidLimit(String),
31 #[error("Invalid time range: start ({start}) must be less than end ({end})")]
33 InvalidTimeRange { start: i64, end: i64 },
34 #[error("Missing required order identifier")]
36 MissingOrderId,
37}
38
39#[derive(Clone, Debug, Deserialize, Serialize)]
44pub struct AxErrorResponse {
45 #[serde(default)]
47 pub error: Option<String>,
48 #[serde(default)]
50 pub message: Option<String>,
51 #[serde(default)]
53 pub status: Option<u16>,
54}
55
56#[derive(Debug, Clone, Error)]
58pub enum AxHttpError {
59 #[error("Missing credentials for authenticated request")]
61 MissingCredentials,
62 #[error("Session token not set (not authenticated)")]
64 MissingSessionToken,
65 #[error("AX Exchange API error: {message}")]
67 ApiError { message: String },
68 #[error("JSON error: {0}")]
70 JsonError(String),
71 #[error("Parameter validation error: {0}")]
73 ValidationError(String),
74 #[error("Build error: {0}")]
76 BuildError(#[from] AxBuildError),
77 #[error("Request canceled: {0}")]
79 Canceled(String),
80 #[error("Network error: {0}")]
82 NetworkError(String),
83 #[error("Unexpected HTTP status code {status}: {body}")]
85 UnexpectedStatus { status: u16, body: String },
86}
87
88impl From<HttpClientError> for AxHttpError {
89 fn from(error: HttpClientError) -> Self {
90 Self::NetworkError(error.to_string())
91 }
92}
93
94impl From<String> for AxHttpError {
95 fn from(error: String) -> Self {
96 Self::ValidationError(error)
97 }
98}
99
100impl From<serde_json::Error> for AxHttpError {
101 fn from(error: serde_json::Error) -> Self {
102 Self::JsonError(error.to_string())
103 }
104}
105
106impl From<AxErrorResponse> for AxHttpError {
107 fn from(error: AxErrorResponse) -> Self {
108 let message = error
109 .message
110 .or(error.error)
111 .unwrap_or_else(|| "Unknown error".to_string());
112 Self::ApiError { message }
113 }
114}
115
116impl AxHttpError {
117 #[must_use]
121 pub fn is_retryable(&self) -> bool {
122 crate::common::retry::should_retry_http(self)
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use rstest::rstest;
129
130 use super::*;
131
132 #[rstest]
133 fn test_architect_build_error_display() {
134 let error = AxBuildError::MissingSymbol;
135 assert_eq!(error.to_string(), "Missing required symbol");
136
137 let error = AxBuildError::InvalidLimit("must be positive".to_string());
138 assert_eq!(error.to_string(), "Invalid limit: must be positive");
139
140 let error = AxBuildError::InvalidTimeRange {
141 start: 100,
142 end: 50,
143 };
144 assert_eq!(
145 error.to_string(),
146 "Invalid time range: start (100) must be less than end (50)"
147 );
148 }
149
150 #[rstest]
151 fn test_architect_http_error_from_json_error() {
152 let json_err = serde_json::from_str::<serde_json::Value>("invalid json")
153 .expect_err("Should fail to parse");
154 let http_err = AxHttpError::from(json_err);
155
156 assert!(matches!(http_err, AxHttpError::JsonError(_)));
157 }
158
159 #[rstest]
160 fn test_architect_http_error_from_string() {
161 let error = AxHttpError::from("Test validation error".to_string());
162 assert_eq!(
163 error.to_string(),
164 "Parameter validation error: Test validation error"
165 );
166 }
167
168 #[rstest]
169 fn test_architect_error_response_to_http_error() {
170 let error_response = AxErrorResponse {
171 error: Some("INVALID_REQUEST".to_string()),
172 message: Some("Invalid parameter".to_string()),
173 status: Some(400),
174 };
175
176 let http_error = AxHttpError::from(error_response);
177 assert_eq!(
178 http_error.to_string(),
179 "AX Exchange API error: Invalid parameter"
180 );
181 }
182
183 #[rstest]
184 #[case(AxHttpError::NetworkError("boom".to_string()), true)]
185 #[case(AxHttpError::UnexpectedStatus { status: 500, body: String::new() }, true)]
186 #[case(AxHttpError::UnexpectedStatus { status: 502, body: String::new() }, true)]
187 #[case(AxHttpError::UnexpectedStatus { status: 503, body: String::new() }, true)]
188 #[case(AxHttpError::UnexpectedStatus { status: 599, body: String::new() }, true)]
189 #[case(AxHttpError::UnexpectedStatus { status: 600, body: String::new() }, true)]
190 #[case(AxHttpError::UnexpectedStatus { status: 429, body: String::new() }, true)]
191 #[case(AxHttpError::UnexpectedStatus { status: 408, body: String::new() }, false)]
192 #[case(AxHttpError::UnexpectedStatus { status: 425, body: String::new() }, false)]
193 #[case(AxHttpError::UnexpectedStatus { status: 400, body: String::new() }, false)]
194 #[case(AxHttpError::UnexpectedStatus { status: 401, body: String::new() }, false)]
195 #[case(AxHttpError::UnexpectedStatus { status: 404, body: String::new() }, false)]
196 #[case(AxHttpError::MissingCredentials, false)]
197 #[case(AxHttpError::MissingSessionToken, false)]
198 #[case(AxHttpError::ApiError { message: "bad".to_string() }, false)]
199 #[case(AxHttpError::JsonError("bad".to_string()), false)]
200 #[case(AxHttpError::ValidationError("bad".to_string()), false)]
201 #[case(AxHttpError::BuildError(AxBuildError::MissingSymbol), false)]
202 #[case(AxHttpError::Canceled("shutdown".to_string()), false)]
203 fn test_is_retryable(#[case] error: AxHttpError, #[case] expected: bool) {
204 assert_eq!(error.is_retryable(), expected);
205 }
206}