Skip to main content

nautilus_architect_ax/http/
error.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Error structures and enumerations for the AX Exchange HTTP integration.
17
18use nautilus_network::http::HttpClientError;
19use serde::{Deserialize, Serialize};
20use thiserror::Error;
21
22/// Build error for query parameter validation.
23#[derive(Debug, Clone, Error)]
24pub enum AxBuildError {
25    /// Missing required symbol.
26    #[error("Missing required symbol")]
27    MissingSymbol,
28    /// Invalid limit value.
29    #[error("Invalid limit: {0}")]
30    InvalidLimit(String),
31    /// Invalid time range: `start` should be less than `end`.
32    #[error("Invalid time range: start ({start}) must be less than end ({end})")]
33    InvalidTimeRange { start: i64, end: i64 },
34    /// Missing required order identifier.
35    #[error("Missing required order identifier")]
36    MissingOrderId,
37}
38
39/// Represents the JSON structure of an error response returned by the AX Exchange API.
40///
41/// Note: The exact error response format will be updated as we learn more about
42/// the AX Exchange API error structure.
43#[derive(Clone, Debug, Deserialize, Serialize)]
44pub struct AxErrorResponse {
45    /// Error code or type.
46    #[serde(default)]
47    pub error: Option<String>,
48    /// A human-readable explanation of the error condition.
49    #[serde(default)]
50    pub message: Option<String>,
51    /// HTTP status code.
52    #[serde(default)]
53    pub status: Option<u16>,
54}
55
56/// A typed error enumeration for the Ax HTTP client.
57#[derive(Debug, Clone, Error)]
58pub enum AxHttpError {
59    /// Error variant when credentials are missing but the request is authenticated.
60    #[error("Missing credentials for authenticated request")]
61    MissingCredentials,
62    /// Error variant when session token is not set (not yet authenticated).
63    #[error("Session token not set (not authenticated)")]
64    MissingSessionToken,
65    /// Errors returned directly by AX Exchange API.
66    #[error("AX Exchange API error: {message}")]
67    ApiError { message: String },
68    /// Failure during JSON serialization/deserialization.
69    #[error("JSON error: {0}")]
70    JsonError(String),
71    /// Parameter validation error.
72    #[error("Parameter validation error: {0}")]
73    ValidationError(String),
74    /// Build error for query parameters.
75    #[error("Build error: {0}")]
76    BuildError(#[from] AxBuildError),
77    /// Request was canceled, typically due to shutdown or disconnect.
78    #[error("Request canceled: {0}")]
79    Canceled(String),
80    /// Generic network error (for retries, cancellations, etc).
81    #[error("Network error: {0}")]
82    NetworkError(String),
83    /// Any unknown HTTP status or unexpected response from Ax.
84    #[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    /// Returns `true` if the error is transient and the request should be retried.
118    ///
119    /// Retries on network errors, rate limiting (429), and server errors (5xx).
120    #[must_use]
121    pub fn is_retryable(&self) -> bool {
122        match self {
123            Self::NetworkError(_) => true,
124            Self::UnexpectedStatus { status, .. } => *status == 429 || *status >= 500,
125            Self::MissingCredentials
126            | Self::MissingSessionToken
127            | Self::ApiError { .. }
128            | Self::JsonError(_)
129            | Self::ValidationError(_)
130            | Self::BuildError(_)
131            | Self::Canceled(_) => false,
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use rstest::rstest;
139
140    use super::*;
141
142    #[rstest]
143    fn test_architect_build_error_display() {
144        let error = AxBuildError::MissingSymbol;
145        assert_eq!(error.to_string(), "Missing required symbol");
146
147        let error = AxBuildError::InvalidLimit("must be positive".to_string());
148        assert_eq!(error.to_string(), "Invalid limit: must be positive");
149
150        let error = AxBuildError::InvalidTimeRange {
151            start: 100,
152            end: 50,
153        };
154        assert_eq!(
155            error.to_string(),
156            "Invalid time range: start (100) must be less than end (50)"
157        );
158    }
159
160    #[rstest]
161    fn test_architect_http_error_from_json_error() {
162        let json_err = serde_json::from_str::<serde_json::Value>("invalid json")
163            .expect_err("Should fail to parse");
164        let http_err = AxHttpError::from(json_err);
165
166        assert!(matches!(http_err, AxHttpError::JsonError(_)));
167    }
168
169    #[rstest]
170    fn test_architect_http_error_from_string() {
171        let error = AxHttpError::from("Test validation error".to_string());
172        assert_eq!(
173            error.to_string(),
174            "Parameter validation error: Test validation error"
175        );
176    }
177
178    #[rstest]
179    fn test_architect_error_response_to_http_error() {
180        let error_response = AxErrorResponse {
181            error: Some("INVALID_REQUEST".to_string()),
182            message: Some("Invalid parameter".to_string()),
183            status: Some(400),
184        };
185
186        let http_error = AxHttpError::from(error_response);
187        assert_eq!(
188            http_error.to_string(),
189            "AX Exchange API error: Invalid parameter"
190        );
191    }
192}