Skip to main content

nautilus_okx/common/
failure.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//! OKX mapping from transport errors to [`CommandFailure`].
17
18use nautilus_live::execution::failure::CommandFailure;
19use nautilus_network::{error::SendError, http::HttpClientError};
20
21use crate::{
22    common::consts::{OKX_ORDER_REQUEST_TIMEOUT_CODE, should_retry_error_code},
23    http::error::OKXHttpError,
24    websocket::error::OKXWsError,
25};
26
27/// Classifies a structured OKX venue code.
28///
29/// Retryable system and rate-limit codes refuse this attempt but do not prove
30/// the command was not applied.
31#[must_use]
32pub fn classify_okx_venue_code(error_code: &str, reason: impl Into<String>) -> CommandFailure {
33    let reason = reason.into();
34
35    if error_code.is_empty()
36        || should_retry_error_code(error_code)
37        || error_code == OKX_ORDER_REQUEST_TIMEOUT_CODE
38    {
39        CommandFailure::Ambiguous(reason)
40    } else {
41        CommandFailure::VenueRejected(reason)
42    }
43}
44
45/// Classifies an OKX HTTP command failure from typed error evidence.
46#[must_use]
47pub fn classify_okx_http_failure(error: &OKXHttpError) -> CommandFailure {
48    let reason = error.to_string();
49    match error {
50        OKXHttpError::MissingCredentials
51        | OKXHttpError::ValidationError(_)
52        | OKXHttpError::RequestSerialization(_)
53        | OKXHttpError::HttpClientError(
54            HttpClientError::InvalidProxy(_) | HttpClientError::ClientBuildError(_),
55        ) => CommandFailure::NotSent(reason),
56        OKXHttpError::OkxError { error_code, .. }
57        | OKXHttpError::RetryableOkxError { error_code, .. } => {
58            classify_okx_venue_code(error_code, reason)
59        }
60        OKXHttpError::MalformedResponse(_)
61        | OKXHttpError::ResponseDecoding(_)
62        | OKXHttpError::Canceled(_)
63        | OKXHttpError::HttpClientError(_)
64        | OKXHttpError::RetryableStatus { .. }
65        | OKXHttpError::UnexpectedStatus { .. }
66        | OKXHttpError::OperationTimeout { .. }
67        | OKXHttpError::RetryBudgetExceeded(_)
68        | OKXHttpError::EmptyResponse => CommandFailure::Ambiguous(reason),
69    }
70}
71
72/// Classifies an OKX WebSocket command failure from typed error evidence.
73#[must_use]
74pub fn classify_okx_ws_failure(error: &OKXWsError) -> CommandFailure {
75    let reason = error.to_string();
76    match error {
77        OKXWsError::ClientError(_)
78        | OKXWsError::JsonError(_)
79        | OKXWsError::NoActiveClient
80        | OKXWsError::HandlerUnavailable(_)
81        | OKXWsError::TransportSend(
82            SendError::InvalidInput(_)
83            | SendError::BufferFull
84            | SendError::Closed
85            | SendError::Timeout
86            | SendError::ConnectionChanged,
87        ) => CommandFailure::NotSent(reason),
88        OKXWsError::OkxError { error_code, .. } => classify_okx_venue_code(error_code, reason),
89        OKXWsError::ParsingError(_)
90        | OKXWsError::AuthenticationError(_)
91        | OKXWsError::TungsteniteError(_)
92        | OKXWsError::TransportSend(SendError::WriteTimeout | SendError::BrokenPipe(_))
93        | OKXWsError::SendFailed(_)
94        | OKXWsError::OperationTimeout { .. } => CommandFailure::Ambiguous(reason),
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use nautilus_network::http::{HttpClientError, StatusCode};
101    use rstest::rstest;
102
103    use super::*;
104
105    #[rstest]
106    #[case::parameter_reject("51000", "Parameter state error", true)]
107    #[case::system_busy("50013", "System busy, please retry later", false)]
108    #[case::request_timeout("50004", "API endpoint request timeout", false)]
109    #[case::order_timeout("51149", "Order timed out. Please try again.", false)]
110    #[case::rate_limit("50011", "Request too frequent", false)]
111    #[case::ws_rate_limit("60014", "WebSocket requests too frequent", true)]
112    #[case::ws_internal_error("64007", "WebSocket internal error", true)]
113    #[case::invalid_signature("50113", "Invalid signature", true)]
114    #[case::missing_code("", "All operations failed", false)]
115    fn test_classify_okx_venue_code(
116        #[case] error_code: &str,
117        #[case] message: &str,
118        #[case] venue_rejected: bool,
119    ) {
120        let failure = classify_okx_venue_code(error_code, message);
121
122        if venue_rejected {
123            assert_eq!(failure, CommandFailure::VenueRejected(message.to_string()));
124        } else {
125            assert_eq!(failure, CommandFailure::Ambiguous(message.to_string()));
126        }
127    }
128
129    #[rstest]
130    fn test_classify_okx_http_permanent_venue_error_is_rejected() {
131        let error = OKXHttpError::OkxError {
132            error_code: "51000".to_string(),
133            message: "Parameter state error".to_string(),
134        };
135        let reason = error.to_string();
136
137        assert_eq!(
138            classify_okx_http_failure(&error),
139            CommandFailure::VenueRejected(reason)
140        );
141    }
142
143    #[rstest]
144    fn test_classify_okx_http_retryable_venue_error_is_ambiguous() {
145        let error = OKXHttpError::RetryableOkxError {
146            error_code: "50013".to_string(),
147            message: "System busy, please try again later".to_string(),
148            retry_after: None,
149        };
150        let reason = error.to_string();
151
152        assert_eq!(
153            classify_okx_http_failure(&error),
154            CommandFailure::Ambiguous(reason)
155        );
156    }
157
158    #[rstest]
159    fn test_classify_okx_http_missing_credentials_is_not_sent() {
160        let error = OKXHttpError::MissingCredentials;
161
162        assert_eq!(
163            classify_okx_http_failure(&error),
164            CommandFailure::NotSent(error.to_string())
165        );
166    }
167
168    #[rstest]
169    fn test_classify_okx_http_validation_is_not_sent() {
170        let error = OKXHttpError::ValidationError("invalid quantity".to_string());
171
172        assert_eq!(
173            classify_okx_http_failure(&error),
174            CommandFailure::NotSent(error.to_string())
175        );
176    }
177
178    #[rstest]
179    fn test_classify_okx_http_invalid_retry_config_is_not_sent() {
180        let error = OKXHttpError::ValidationError("invalid retry configuration".to_string());
181
182        assert_eq!(
183            classify_okx_http_failure(&error),
184            CommandFailure::NotSent(error.to_string())
185        );
186    }
187
188    #[rstest]
189    fn test_classify_okx_http_timeout_is_ambiguous() {
190        let error = OKXHttpError::OperationTimeout { timeout_ms: 1_000 };
191
192        assert_eq!(
193            classify_okx_http_failure(&error),
194            CommandFailure::Ambiguous(error.to_string())
195        );
196    }
197
198    #[rstest]
199    fn test_classify_okx_http_budget_is_ambiguous() {
200        let error = OKXHttpError::RetryBudgetExceeded("budget exceeded".to_string());
201
202        assert_eq!(
203            classify_okx_http_failure(&error),
204            CommandFailure::Ambiguous(error.to_string())
205        );
206    }
207
208    #[rstest]
209    fn test_classify_okx_http_network_is_ambiguous() {
210        let error = OKXHttpError::HttpClientError(HttpClientError::TransportError(
211            "connection reset".to_string(),
212        ));
213
214        assert_eq!(
215            classify_okx_http_failure(&error),
216            CommandFailure::Ambiguous(error.to_string())
217        );
218    }
219
220    #[rstest]
221    fn test_classify_okx_http_permanent_client_error_is_ambiguous() {
222        let error = OKXHttpError::HttpClientError(HttpClientError::Error(
223            "response body exceeds maximum".to_string(),
224        ));
225
226        assert_eq!(
227            classify_okx_http_failure(&error),
228            CommandFailure::Ambiguous(error.to_string())
229        );
230    }
231
232    #[rstest]
233    fn test_classify_okx_http_unexpected_status_is_ambiguous() {
234        let error = OKXHttpError::UnexpectedStatus {
235            status: StatusCode::INTERNAL_SERVER_ERROR,
236            body: String::new(),
237        };
238
239        assert_eq!(
240            classify_okx_http_failure(&error),
241            CommandFailure::Ambiguous(error.to_string())
242        );
243    }
244
245    #[rstest]
246    fn test_classify_okx_http_response_decoding_is_ambiguous() {
247        let error = OKXHttpError::ResponseDecoding("failed to deserialize".to_string());
248
249        assert_eq!(
250            classify_okx_http_failure(&error),
251            CommandFailure::Ambiguous(error.to_string())
252        );
253    }
254
255    #[rstest]
256    fn test_classify_okx_http_request_serialization_is_not_sent() {
257        let error = OKXHttpError::RequestSerialization("failed to serialize".to_string());
258
259        assert_eq!(
260            classify_okx_http_failure(&error),
261            CommandFailure::NotSent(error.to_string())
262        );
263    }
264
265    #[rstest]
266    fn test_classify_okx_http_empty_response_is_ambiguous() {
267        let error = OKXHttpError::EmptyResponse;
268
269        assert_eq!(
270            classify_okx_http_failure(&error),
271            CommandFailure::Ambiguous(error.to_string())
272        );
273    }
274
275    #[rstest]
276    fn test_classify_okx_ws_handler_unavailable_is_not_sent() {
277        let error = OKXWsError::HandlerUnavailable("channel closed".to_string());
278
279        assert_eq!(
280            classify_okx_ws_failure(&error),
281            CommandFailure::NotSent(error.to_string())
282        );
283    }
284
285    #[rstest]
286    fn test_classify_okx_ws_no_active_client_is_not_sent() {
287        let error = OKXWsError::NoActiveClient;
288
289        assert_eq!(
290            classify_okx_ws_failure(&error),
291            CommandFailure::NotSent(error.to_string())
292        );
293    }
294
295    #[rstest]
296    fn test_classify_okx_ws_json_encode_is_not_sent() {
297        let error = OKXWsError::JsonError("Failed to serialize order: eof".to_string());
298
299        assert_eq!(
300            classify_okx_ws_failure(&error),
301            CommandFailure::NotSent(error.to_string())
302        );
303    }
304
305    #[rstest]
306    fn test_classify_okx_ws_send_failed_is_ambiguous() {
307        let error = OKXWsError::SendFailed("connection reset".to_string());
308
309        assert_eq!(
310            classify_okx_ws_failure(&error),
311            CommandFailure::Ambiguous(error.to_string())
312        );
313    }
314
315    #[rstest]
316    #[case(SendError::Timeout)]
317    #[case(SendError::BufferFull)]
318    fn test_classify_okx_ws_pre_write_failure_is_not_sent(#[case] error: SendError) {
319        let error = OKXWsError::TransportSend(error);
320
321        assert_eq!(
322            classify_okx_ws_failure(&error),
323            CommandFailure::NotSent(error.to_string())
324        );
325    }
326
327    #[rstest]
328    fn test_classify_okx_ws_write_timeout_is_ambiguous() {
329        let error = OKXWsError::TransportSend(SendError::WriteTimeout);
330
331        assert_eq!(
332            classify_okx_ws_failure(&error),
333            CommandFailure::Ambiguous(error.to_string())
334        );
335    }
336
337    #[rstest]
338    fn test_classify_okx_ws_timeout_is_ambiguous() {
339        let error = OKXWsError::OperationTimeout { timeout_ms: 2_000 };
340
341        assert_eq!(
342            classify_okx_ws_failure(&error),
343            CommandFailure::Ambiguous(error.to_string())
344        );
345    }
346}