1use std::time::Duration;
4
5#[derive(thiserror::Error, Debug)]
7pub enum ClientError {
8 #[error("Connection failed: {0}")]
9 Connect(#[from] ConnectError),
10
11 #[error("Handshake failed: {0}")]
12 Handshake(#[from] HandshakeError),
13
14 #[error("Send failed: {0}")]
15 Send(#[from] SendError),
16
17 #[error("Receive failed: {0}")]
18 Receive(#[from] ReceiveError),
19
20 #[error("Supervisor error: {0}")]
21 Supervisor(#[from] SupervisorError),
22
23 #[error("Extension error: {0}")]
24 Extension(#[from] ExtensionError),
25
26 #[error("Invalid configuration: {0}")]
27 Config(String),
28
29 #[error("Client is already running")]
30 AlreadyRunning,
31
32 #[error("Graceful shutdown timed out after {0:?}")]
34 ShutdownTimeout(Duration),
35
36 #[error("{context}: {source}")]
38 Context {
39 context: String,
41 #[source]
43 source: Box<ClientError>,
44 },
45}
46
47#[derive(thiserror::Error, Debug, Clone)]
49pub enum ConnectError {
50 #[error("Invalid URL: {0}")]
51 InvalidUrl(String),
52
53 #[error("Invalid URI: {0}")]
54 InvalidUri(String),
55
56 #[error("DNS resolution failed: {0}")]
57 DnsResolution(String),
58
59 #[error("TCP connection failed: {0}")]
60 TcpConnect(String),
61
62 #[error("TCP failed: {0}")]
63 TcpFailed(String),
64
65 #[error("TLS handshake failed: {0}")]
66 TlsHandshake(String),
67
68 #[error("TLS configuration error: {0}")]
69 Tls(String),
70
71 #[error("WebSocket upgrade failed: {0}")]
72 WebSocketUpgrade(String),
73
74 #[error("WebSocket connection failed: {0}")]
75 WebSocketFailed(String),
76
77 #[error("Handshake failed: {0}")]
78 HandshakeFailed(String),
79
80 #[error("Connection timeout after {0:?}")]
81 Timeout(Duration),
82
83 #[error("Connection refused")]
84 Refused,
85
86 #[error("IO error: {0}")]
87 Io(String),
88}
89
90impl ConnectError {
91 #[must_use]
93 pub const fn is_retryable(&self) -> bool {
94 match self {
95 Self::InvalidUrl(_) | Self::InvalidUri(_) | Self::TlsHandshake(_) | Self::Tls(_) => {
97 false
98 } Self::DnsResolution(_)
102 | Self::TcpConnect(_)
103 | Self::TcpFailed(_)
104 | Self::WebSocketUpgrade(_)
105 | Self::WebSocketFailed(_)
106 | Self::HandshakeFailed(_)
107 | Self::Timeout(_)
108 | Self::Refused
109 | Self::Io(_) => true,
110 }
111 }
112
113 #[must_use]
115 pub const fn suggested_delay(&self) -> Option<Duration> {
116 match self {
117 Self::DnsResolution(_) => Some(Duration::from_millis(500)),
118 Self::Timeout(_) => Some(Duration::from_secs(5)),
119 Self::Refused => Some(Duration::from_secs(2)),
120 _ => None, }
122 }
123}
124
125impl From<std::io::Error> for ConnectError {
126 fn from(e: std::io::Error) -> Self {
127 Self::Io(e.to_string())
128 }
129}
130
131impl From<tungstenite::Error> for ConnectError {
132 fn from(e: tungstenite::Error) -> Self {
133 Self::WebSocketUpgrade(e.to_string())
134 }
135}
136
137#[derive(thiserror::Error, Debug, Clone)]
139pub enum HandshakeError {
140 #[error("Handshake failed: {0}")]
141 Failed(String),
142
143 #[error("Authentication failed: {0}")]
144 AuthFailed(String),
145
146 #[error("Handshake timeout after {0:?}")]
147 Timeout(Duration),
148
149 #[error("Protocol error: {0}")]
150 Protocol(String),
151
152 #[error("WebSocket error: {0}")]
153 WebSocket(String),
154}
155
156impl HandshakeError {
157 #[must_use]
159 pub const fn is_retryable(&self) -> bool {
160 match self {
161 Self::AuthFailed(_) | Self::Protocol(_) => false,
163 Self::Failed(_) | Self::Timeout(_) | Self::WebSocket(_) => true,
165 }
166 }
167}
168
169impl From<tungstenite::Error> for HandshakeError {
170 fn from(e: tungstenite::Error) -> Self {
171 Self::WebSocket(e.to_string())
172 }
173}
174
175#[derive(thiserror::Error, Debug, Clone)]
177pub enum SendError {
178 #[error("Not connected")]
179 NotConnected,
180
181 #[error("Channel closed")]
182 ChannelClosed,
183
184 #[error("Send buffer is full")]
185 ChannelFull,
186
187 #[error("Send timed out after {0:?}")]
188 Timeout(Duration),
189
190 #[error("Message too large: {size} bytes (max: {max})")]
191 MessageTooLarge { size: usize, max: usize },
192
193 #[error("WebSocket error: {0}")]
194 WebSocket(String),
195}
196
197impl From<tungstenite::Error> for SendError {
198 fn from(e: tungstenite::Error) -> Self {
199 Self::WebSocket(e.to_string())
200 }
201}
202
203#[derive(thiserror::Error, Debug, Clone)]
205pub enum ReceiveError {
206 #[error("Stream closed")]
207 StreamClosed,
208
209 #[error("Receive timeout after {0:?}")]
210 Timeout(Duration),
211
212 #[error("WebSocket error: {0}")]
213 WebSocket(String),
214}
215
216impl From<tungstenite::Error> for ReceiveError {
217 fn from(e: tungstenite::Error) -> Self {
218 Self::WebSocket(e.to_string())
219 }
220}
221
222#[derive(thiserror::Error, Debug, Clone)]
224pub enum SupervisorError {
225 #[error("Max retries exceeded after {attempts} attempts")]
226 MaxRetriesExceeded { attempts: u32 },
227
228 #[error("Shutdown requested")]
229 Shutdown,
230
231 #[error("Fatal error: {0}")]
232 Fatal(String),
233}
234
235#[derive(thiserror::Error, Debug, Clone)]
237pub enum ExtensionError {
238 #[error("Extension '{name}' failed: {message}")]
239 Failed { name: String, message: String },
240
241 #[error("Extension '{name}' initialization failed: {message}")]
242 InitFailed { name: String, message: String },
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
247pub enum DisconnectReason {
248 Normal,
250 Error(String),
252 Timeout,
254 Shutdown,
256 ServerClosed {
258 code: Option<u16>,
259 reason: Option<String>,
260 },
261}
262
263impl std::fmt::Display for DisconnectReason {
264 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 match self {
266 Self::Normal => write!(f, "normal closure"),
267 Self::Error(e) => write!(f, "error: {e}"),
268 Self::Timeout => write!(f, "timeout"),
269 Self::Shutdown => write!(f, "shutdown"),
270 Self::ServerClosed { code, reason } => {
271 write!(f, "server closed")?;
272 if let Some(c) = code {
273 write!(f, " (code: {c})")?;
274 }
275 if let Some(r) = reason {
276 write!(f, ": {r}")?;
277 }
278 Ok(())
279 }
280 }
281 }
282}
283
284pub type ClientResult<T> = Result<T, ClientError>;
286
287pub trait ErrorContext<T> {
289 fn with_context(self, context: impl Into<String>) -> Result<T, ClientError>;
291}
292
293impl<T, E: Into<ClientError>> ErrorContext<T> for Result<T, E> {
294 fn with_context(self, context: impl Into<String>) -> Result<T, ClientError> {
295 self.map_err(|e| ClientError::Context {
296 context: context.into(),
297 source: Box::new(e.into()),
298 })
299 }
300}