nautilus_derive/websocket/
error.rs1use std::time::Duration;
19
20use serde_json::Value;
21use thiserror::Error;
22
23use crate::signing::auth::AuthError;
24
25pub type Result<T> = std::result::Result<T, DeriveWsError>;
27
28#[derive(Debug, Error)]
30pub enum DeriveWsError {
31 #[error("transport error: {0}")]
33 Transport(String),
34
35 #[error("serde error: {0}")]
37 Serde(#[from] serde_json::Error),
38
39 #[error("JSON-RPC error {code}: {message}")]
41 JsonRpc {
42 code: i64,
44 message: String,
46 data: Option<Value>,
48 },
49
50 #[error("pacing reservation expired before dispatch, retry after {retry_after:?}")]
52 RateLimited {
53 retry_after: Duration,
55 },
56
57 #[error("request `{method}` cancelled before response was received")]
59 RequestCancelled {
60 method: String,
62 },
63
64 #[error("request `{method}` timed out before response was received")]
67 Timeout {
68 method: String,
70 },
71
72 #[error("auth error: {0}")]
74 Auth(#[from] AuthError),
75
76 #[error("authentication failed for `{operation}`: {reason}")]
78 Authentication {
79 operation: String,
81 reason: String,
83 },
84
85 #[error("subscription failed: {details}")]
87 Subscription {
88 details: String,
90 },
91
92 #[error("missing credentials for `{operation}`")]
94 MissingCredentials {
95 operation: String,
97 },
98
99 #[error("WebSocket client is not connected")]
101 NotConnected,
102}
103
104impl DeriveWsError {
105 #[must_use]
107 pub fn transport(msg: impl Into<String>) -> Self {
108 Self::Transport(msg.into())
109 }
110
111 pub(crate) fn redacted_message(&self) -> String {
112 match self {
113 Self::JsonRpc { code, .. } => format!("JSON-RPC error {code}"),
114 Self::RateLimited { retry_after } => {
115 format!("rate limited, retry after {retry_after:?}")
116 }
117 Self::Transport(_) => "transport error".to_string(),
118 Self::Serde(_) => "JSON error".to_string(),
119 Self::RequestCancelled { .. } => "request cancelled".to_string(),
120 Self::Timeout { .. } => "request timed out".to_string(),
121 Self::Auth(_) | Self::Authentication { .. } => "authentication error".to_string(),
122 Self::Subscription { .. } => "subscription failed".to_string(),
123 Self::MissingCredentials { .. } => "missing credentials".to_string(),
124 Self::NotConnected => "not connected".to_string(),
125 }
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use rstest::rstest;
132 use serde_json::json;
133
134 use super::*;
135
136 #[rstest]
137 fn test_transport_constructor_carries_message() {
138 let err = DeriveWsError::transport("broken pipe");
139 assert!(err.to_string().contains("broken pipe"));
140 }
141
142 #[rstest]
143 fn test_jsonrpc_error_renders_code_and_message() {
144 let err = DeriveWsError::JsonRpc {
145 code: -32601,
146 message: "Method not found".to_string(),
147 data: Some(json!({"method": "public/foo"})),
148 };
149
150 let rendered = err.to_string();
151 assert!(rendered.contains("-32601"));
152 assert!(rendered.contains("Method not found"));
153 }
154
155 #[rstest]
156 fn test_missing_credentials_names_operation() {
157 let err = DeriveWsError::MissingCredentials {
158 operation: "public/login".to_string(),
159 };
160
161 assert!(err.to_string().contains("public/login"));
162 }
163
164 #[rstest]
165 fn test_authentication_error_names_operation_and_reason() {
166 let err = DeriveWsError::Authentication {
167 operation: "private/order".to_string(),
168 reason: "session recovery failed".to_string(),
169 };
170
171 assert!(err.to_string().contains("private/order"));
172 assert!(err.to_string().contains("session recovery failed"));
173 }
174}