ccxt_core/ws_client/
error.rs

1//! WebSocket error types and classification.
2
3use crate::error::Error;
4
5/// WebSocket error classification.
6///
7/// This enum categorizes WebSocket errors into two types:
8/// - `Transient`: Temporary errors that may recover with retry (network issues, server unavailable)
9/// - `Permanent`: Errors that should not be retried (authentication failures, protocol errors)
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum WsErrorKind {
12    /// Transient errors that may recover with retry.
13    Transient,
14    /// Permanent errors that should not be retried.
15    Permanent,
16}
17
18impl WsErrorKind {
19    /// Returns `true` if this is a transient error that may recover with retry.
20    #[inline]
21    #[must_use]
22    pub fn is_transient(self) -> bool {
23        matches!(self, Self::Transient)
24    }
25
26    /// Returns `true` if this is a permanent error that should not be retried.
27    #[inline]
28    #[must_use]
29    pub fn is_permanent(self) -> bool {
30        matches!(self, Self::Permanent)
31    }
32}
33
34impl std::fmt::Display for WsErrorKind {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match self {
37            Self::Transient => write!(f, "Transient"),
38            Self::Permanent => write!(f, "Permanent"),
39        }
40    }
41}
42
43/// Extended WebSocket error with classification.
44#[derive(Debug)]
45pub struct WsError {
46    kind: WsErrorKind,
47    message: String,
48    source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
49}
50
51impl WsError {
52    /// Creates a new `WsError` with the specified kind and message.
53    pub fn new(kind: WsErrorKind, message: impl Into<String>) -> Self {
54        Self {
55            kind,
56            message: message.into(),
57            source: None,
58        }
59    }
60
61    /// Creates a new `WsError` with a source error.
62    pub fn with_source<E>(kind: WsErrorKind, message: impl Into<String>, source: E) -> Self
63    where
64        E: std::error::Error + Send + Sync + 'static,
65    {
66        Self {
67            kind,
68            message: message.into(),
69            source: Some(Box::new(source)),
70        }
71    }
72
73    /// Creates a transient error.
74    pub fn transient(message: impl Into<String>) -> Self {
75        Self::new(WsErrorKind::Transient, message)
76    }
77
78    /// Creates a transient error with a source.
79    pub fn transient_with_source<E>(message: impl Into<String>, source: E) -> Self
80    where
81        E: std::error::Error + Send + Sync + 'static,
82    {
83        Self::with_source(WsErrorKind::Transient, message, source)
84    }
85
86    /// Creates a permanent error.
87    pub fn permanent(message: impl Into<String>) -> Self {
88        Self::new(WsErrorKind::Permanent, message)
89    }
90
91    /// Creates a permanent error with a source.
92    pub fn permanent_with_source<E>(message: impl Into<String>, source: E) -> Self
93    where
94        E: std::error::Error + Send + Sync + 'static,
95    {
96        Self::with_source(WsErrorKind::Permanent, message, source)
97    }
98
99    /// Returns the error kind.
100    #[inline]
101    #[must_use]
102    pub fn kind(&self) -> WsErrorKind {
103        self.kind
104    }
105
106    /// Returns the error message.
107    #[inline]
108    #[must_use]
109    pub fn message(&self) -> &str {
110        &self.message
111    }
112
113    /// Returns `true` if this is a transient error.
114    #[inline]
115    #[must_use]
116    pub fn is_transient(&self) -> bool {
117        self.kind.is_transient()
118    }
119
120    /// Returns `true` if this is a permanent error.
121    #[inline]
122    #[must_use]
123    pub fn is_permanent(&self) -> bool {
124        self.kind.is_permanent()
125    }
126
127    /// Returns the source error, if any.
128    #[must_use]
129    pub fn source(&self) -> Option<&(dyn std::error::Error + Send + Sync + 'static)> {
130        self.source.as_deref()
131    }
132
133    /// Classifies a tungstenite WebSocket error.
134    pub fn from_tungstenite(err: &tokio_tungstenite::tungstenite::Error) -> Self {
135        use tokio_tungstenite::tungstenite::Error as TungError;
136
137        match err {
138            TungError::Io(io_err) => {
139                let message = format!("IO error: {io_err}");
140                Self::transient_with_source(
141                    message,
142                    std::io::Error::new(io_err.kind(), io_err.to_string()),
143                )
144            }
145            TungError::ConnectionClosed => Self::transient("Connection closed by server"),
146            TungError::AlreadyClosed => Self::transient("Connection already closed"),
147            TungError::Protocol(protocol_err) => {
148                Self::permanent(format!("Protocol error: {protocol_err}"))
149            }
150            TungError::Utf8(_) => Self::permanent("UTF-8 encoding error in WebSocket message"),
151            TungError::Http(response) => {
152                let status = response.status();
153                let status_code = status.as_u16();
154                if status_code == 401 || status_code == 403 {
155                    Self::permanent(format!("Authentication error: HTTP {status}"))
156                } else if status.is_server_error() {
157                    Self::transient(format!("Server error: HTTP {status}"))
158                } else {
159                    Self::permanent(format!("HTTP error: {status}"))
160                }
161            }
162            TungError::HttpFormat(http_err) => {
163                Self::permanent(format!("HTTP format error: {http_err}"))
164            }
165            TungError::Url(url_err) => Self::permanent(format!("Invalid URL: {url_err}")),
166            TungError::Tls(tls_err) => Self::transient(format!("TLS error: {tls_err}")),
167            TungError::Capacity(capacity_err) => {
168                Self::permanent(format!("Capacity error: {capacity_err}"))
169            }
170            TungError::WriteBufferFull(msg) => {
171                Self::transient(format!("Write buffer full: {msg:?}"))
172            }
173            TungError::AttackAttempt => Self::permanent("Potential attack detected"),
174        }
175    }
176
177    /// Classifies a generic error and wraps it in a `WsError`.
178    pub fn from_error(err: &Error) -> Self {
179        if err.as_authentication().is_some() {
180            return Self::permanent(format!("Authentication error: {err}"));
181        }
182        if err.as_cancelled().is_some() {
183            return Self::permanent(format!("Operation cancelled: {err}"));
184        }
185        if err.as_resource_exhausted().is_some() {
186            return Self::permanent(format!("Resource exhausted: {err}"));
187        }
188        Self::transient(format!("Error: {err}"))
189    }
190}
191
192impl std::fmt::Display for WsError {
193    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194        write!(f, "[{}] {}", self.kind, self.message)
195    }
196}
197
198impl std::error::Error for WsError {
199    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
200        self.source
201            .as_ref()
202            .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
203    }
204}