Skip to main content

nautilus_derive/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 types for the Derive HTTP client.
17
18use std::time::Duration;
19
20use nautilus_network::{http::HttpClientError, retry::RetryError};
21use serde_json::Value;
22use thiserror::Error;
23
24use crate::signing::auth::AuthError;
25
26/// Result alias for HTTP operations.
27pub type Result<T> = std::result::Result<T, DeriveHttpError>;
28
29/// Errors raised by the Derive HTTP client.
30#[derive(Debug, Error)]
31pub enum DeriveHttpError {
32    /// Network/transport failure with undefined venue outcome.
33    #[error("transport error: {0}")]
34    Transport(String),
35
36    /// HTTP-level failure (non-2xx without a JSON-RPC body, or 2xx without
37    /// the expected envelope).
38    #[error("HTTP {status}: {message}")]
39    Http {
40        /// HTTP status code.
41        status: u16,
42        /// Truncated body text or status reason.
43        message: String,
44        /// Minimum delay supplied by the response's `Retry-After` header.
45        retry_after: Option<Duration>,
46    },
47
48    /// JSON-RPC error envelope returned by the venue.
49    #[error("JSON-RPC error {code}: {message}")]
50    JsonRpc {
51        /// Venue-defined error code.
52        code: i64,
53        /// Human-readable error message.
54        message: String,
55        /// Optional structured diagnostic payload.
56        data: Option<Value>,
57        /// Minimum delay supplied by the response's `Retry-After` header.
58        retry_after: Option<Duration>,
59    },
60
61    /// Successful envelope was missing the `result` field.
62    #[error("missing `result` in JSON-RPC response for `{method}`")]
63    MissingResult {
64        /// Method that returned an empty envelope.
65        method: String,
66    },
67
68    /// Response body could not be decoded as JSON-RPC.
69    #[error("decode error: {0}")]
70    Decode(String),
71
72    /// JSON (de)serialization failed for a request or response payload.
73    #[error("serde error: {0}")]
74    Serde(#[from] serde_json::Error),
75
76    /// Retry cancellation, timeout, budget, or configuration failure.
77    #[error("retry error: {0}")]
78    Retry(#[from] RetryError),
79
80    /// Auth header construction failed (e.g. clock skew, signer error).
81    #[error("auth error: {0}")]
82    Auth(#[from] AuthError),
83
84    /// Private endpoint invoked without credentials configured on the client.
85    #[error("missing credentials for private endpoint `{method}`")]
86    MissingCredentials {
87        /// Method that requires authentication.
88        method: String,
89    },
90}
91
92impl DeriveHttpError {
93    /// Constructs a [`DeriveHttpError::Transport`] error.
94    #[must_use]
95    pub fn transport(msg: impl Into<String>) -> Self {
96        Self::Transport(msg.into())
97    }
98
99    /// Constructs a [`DeriveHttpError::Http`] error.
100    #[must_use]
101    pub fn http(status: u16, message: impl Into<String>) -> Self {
102        Self::Http {
103            status,
104            message: message.into(),
105            retry_after: None,
106        }
107    }
108
109    /// Constructs a [`DeriveHttpError::Decode`] error.
110    #[must_use]
111    pub fn decode(msg: impl Into<String>) -> Self {
112        Self::Decode(msg.into())
113    }
114
115    /// Returns the response's minimum retry delay, when present.
116    #[must_use]
117    pub const fn retry_after(&self) -> Option<Duration> {
118        match self {
119            Self::Http { retry_after, .. } | Self::JsonRpc { retry_after, .. } => *retry_after,
120            _ => None,
121        }
122    }
123
124    /// Returns `true` for transport errors, including timeouts.
125    /// This does not prove a request was unsent; callers reconciling order state
126    /// should treat the outcome as unknown rather than rejected.
127    #[must_use]
128    pub fn is_transport_error(&self) -> bool {
129        matches!(self, Self::Transport(_))
130    }
131}
132
133impl From<HttpClientError> for DeriveHttpError {
134    fn from(value: HttpClientError) -> Self {
135        Self::Transport(value.to_string())
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use rstest::rstest;
142    use serde_json::json;
143
144    use super::*;
145
146    #[rstest]
147    fn test_transport_is_transport_error() {
148        assert!(DeriveHttpError::transport("conn reset").is_transport_error());
149    }
150
151    #[rstest]
152    fn test_http_is_not_transport_error() {
153        assert!(!DeriveHttpError::http(503, "service unavailable").is_transport_error());
154    }
155
156    #[rstest]
157    fn test_jsonrpc_error_carries_code_and_data() {
158        let err = DeriveHttpError::JsonRpc {
159            code: -32602,
160            message: "Invalid params".to_string(),
161            data: Some(json!({"field": "currency"})),
162            retry_after: None,
163        };
164
165        let text = err.to_string();
166        assert!(text.contains("-32602"));
167        assert!(text.contains("Invalid params"));
168        assert!(!err.is_transport_error());
169    }
170
171    #[rstest]
172    fn test_missing_credentials_names_method() {
173        let err = DeriveHttpError::MissingCredentials {
174            method: "private/order".to_string(),
175        };
176
177        assert!(err.to_string().contains("private/order"));
178    }
179
180    #[rstest]
181    fn test_http_client_error_maps_to_transport() {
182        let upstream = HttpClientError::Error("boom".to_string());
183        let mapped: DeriveHttpError = upstream.into();
184        assert!(mapped.is_transport_error());
185    }
186}