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