1use std::{error::Error, fmt, time::Duration};
4
5use crate::tungstenite::{self, http::header::RETRY_AFTER, protocol::CloseFrame};
6
7pub struct WsError(Box<Repr>);
25
26#[derive(Debug, thiserror::Error)]
27enum Repr {
28 #[error("{0}")]
31 Transport(#[source] tungstenite::Error),
32 #[error("no WebSocket connection within {after:?}")]
33 ConnectTimeout { after: Duration },
34 #[error("the server closed the WebSocket connection{}", describe_close(*.code, .reason))]
35 Closed { code: Option<u16>, reason: String },
36 #[error("no answer within {after:?}")]
37 NoAnswer { after: Duration },
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45#[non_exhaustive]
46pub enum Kind<'a> {
47 Io,
49 Protocol,
52 ConnectionClosed,
54 Closed {
59 code: Option<u16>,
61 reason: &'a str,
63 },
64 Refused {
66 status: u16,
68 retry_after: Option<&'a str>,
71 },
72 Url,
74 HttpFormat,
77 Tls,
81 AttackAttempt,
83 Capacity,
85 AlreadyClosed,
87 Unsupported,
92 ConnectTimeout {
94 after: Duration,
96 },
97 NoAnswer {
99 after: Duration,
101 },
102 Other,
105}
106
107impl WsError {
108 pub fn connect_timeout(after: Duration) -> Self {
110 Self(Box::new(Repr::ConnectTimeout { after }))
111 }
112
113 pub fn closed(code: Option<u16>, reason: impl Into<String>) -> Self {
117 Self(Box::new(Repr::Closed {
118 code,
119 reason: reason.into(),
120 }))
121 }
122
123 pub fn from_close_frame(frame: Option<&CloseFrame>) -> Self {
125 match frame {
126 Some(frame) => Self::closed(Some(u16::from(frame.code)), frame.reason.as_str()),
127 None => Self::closed(None, String::new()),
128 }
129 }
130
131 pub fn no_answer(after: Duration) -> Self {
133 Self(Box::new(Repr::NoAnswer { after }))
134 }
135
136 pub fn kind(&self) -> Kind<'_> {
138 use tungstenite::Error as Ws;
139 match &*self.0 {
140 Repr::Transport(err) => match err {
141 Ws::Io(err) if err.kind() == std::io::ErrorKind::Unsupported => Kind::Unsupported,
142 Ws::Io(_) => Kind::Io,
143 Ws::Protocol(_) => Kind::Protocol,
144 Ws::ConnectionClosed => Kind::ConnectionClosed,
145 Ws::Http(response) => Kind::Refused {
146 status: response.status().as_u16(),
147 retry_after: response
148 .headers()
149 .get(RETRY_AFTER)
150 .and_then(|value| value.to_str().ok()),
151 },
152 Ws::Url(_) => Kind::Url,
153 Ws::HttpFormat(_) => Kind::HttpFormat,
154 Ws::Tls(_) => Kind::Tls,
155 Ws::AttackAttempt => Kind::AttackAttempt,
156 Ws::Capacity(_) => Kind::Capacity,
157 Ws::AlreadyClosed => Kind::AlreadyClosed,
158 _ => Kind::Other,
161 },
162 Repr::ConnectTimeout { after } => Kind::ConnectTimeout { after: *after },
163 Repr::Closed { code, reason } => Kind::Closed {
164 code: *code,
165 reason,
166 },
167 Repr::NoAnswer { after } => Kind::NoAnswer { after: *after },
168 }
169 }
170}
171
172impl From<tungstenite::Error> for WsError {
173 fn from(err: tungstenite::Error) -> Self {
174 Self(Box::new(Repr::Transport(err)))
175 }
176}
177
178impl fmt::Debug for WsError {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 f.debug_tuple("WsError").field(&self.0).finish()
181 }
182}
183
184impl fmt::Display for WsError {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 fmt::Display::fmt(&self.0, f)
187 }
188}
189
190impl Error for WsError {
191 fn source(&self) -> Option<&(dyn Error + 'static)> {
192 self.0.source()
193 }
194}
195
196fn describe_close(code: Option<u16>, reason: &str) -> String {
198 match (code, reason.is_empty()) {
199 (Some(code), true) => format!(" with code {code}"),
200 (Some(code), false) => format!(" with code {code}: {reason}"),
201 (None, true) => " without a status code".to_owned(),
202 (None, false) => format!(": {reason}"),
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use tungstenite::{
209 error::{CapacityError, ProtocolError, TlsError, UrlError},
210 http,
211 protocol::frame::coding::CloseCode,
212 Message,
213 };
214
215 use super::*;
216
217 fn refused(status: u16, retry_after: Option<&str>) -> WsError {
218 let mut response = http::Response::builder().status(status);
219 if let Some(value) = retry_after {
220 response = response.header("Retry-After", value);
221 }
222 WsError::from(tungstenite::Error::Http(response.body(None).unwrap()))
223 }
224
225 #[test]
226 fn every_transport_error_has_its_kind() {
227 let bad_header = http::header::HeaderName::from_bytes(b"in valid").unwrap_err();
228 let rows = [
229 (
230 tungstenite::Error::Io(std::io::ErrorKind::UnexpectedEof.into()),
231 Kind::Io,
232 ),
233 (
234 tungstenite::Error::Io(std::io::ErrorKind::Unsupported.into()),
235 Kind::Unsupported,
236 ),
237 (
238 tungstenite::Error::Protocol(ProtocolError::ResetWithoutClosingHandshake),
239 Kind::Protocol,
240 ),
241 (tungstenite::Error::ConnectionClosed, Kind::ConnectionClosed),
242 (tungstenite::Error::Url(UrlError::NoHostName), Kind::Url),
243 (
244 tungstenite::Error::HttpFormat(bad_header.into()),
245 Kind::HttpFormat,
246 ),
247 (tungstenite::Error::Tls(TlsError::InvalidDnsName), Kind::Tls),
248 (tungstenite::Error::AttackAttempt, Kind::AttackAttempt),
249 (
250 tungstenite::Error::Capacity(CapacityError::TooManyHeaders),
251 Kind::Capacity,
252 ),
253 (tungstenite::Error::AlreadyClosed, Kind::AlreadyClosed),
254 (
255 tungstenite::Error::WriteBufferFull(Message::Close(None)),
256 Kind::Other,
257 ),
258 (tungstenite::Error::Utf8, Kind::Other),
259 ];
260 for (err, kind) in rows {
261 let err = WsError::from(err);
262 assert_eq!(err.kind(), kind, "{err:?}");
263 assert!(err.source().is_some(), "{err:?} lost its transport error");
264 }
265 }
266
267 #[test]
268 fn a_refused_upgrade_keeps_its_status_and_raw_retry_after() {
269 assert_eq!(
270 refused(429, Some("7")).kind(),
271 Kind::Refused {
272 status: 429,
273 retry_after: Some("7")
274 }
275 );
276 assert_eq!(
277 refused(503, None).kind(),
278 Kind::Refused {
279 status: 503,
280 retry_after: None
281 }
282 );
283 assert_eq!(
285 refused(429, Some(" 0 ")).kind(),
286 Kind::Refused {
287 status: 429,
288 retry_after: Some(" 0 ")
289 }
290 );
291 }
292
293 #[test]
294 fn the_kits_own_failures_have_their_kinds_and_no_source() {
295 let after = Duration::from_secs(10);
296 let rows = [
297 (
298 WsError::connect_timeout(after),
299 Kind::ConnectTimeout { after },
300 ),
301 (WsError::no_answer(after), Kind::NoAnswer { after }),
302 (
303 WsError::closed(Some(1008), "policy"),
304 Kind::Closed {
305 code: Some(1008),
306 reason: "policy",
307 },
308 ),
309 ];
310 for (err, kind) in rows {
311 assert_eq!(err.kind(), kind);
312 assert!(err.source().is_none(), "{err:?}");
313 }
314 }
315
316 #[test]
317 fn a_close_frame_is_captured_with_its_code_and_reason() {
318 let frame = CloseFrame {
319 code: CloseCode::Error,
320 reason: "overloaded".into(),
321 };
322 assert_eq!(
323 WsError::from_close_frame(Some(&frame)).kind(),
324 Kind::Closed {
325 code: Some(1011),
326 reason: "overloaded"
327 }
328 );
329 assert_eq!(
330 WsError::from_close_frame(None).kind(),
331 Kind::Closed {
332 code: None,
333 reason: ""
334 }
335 );
336 }
337
338 #[test]
339 fn messages_name_what_happened() {
340 assert_eq!(
341 WsError::closed(Some(1001), "going away").to_string(),
342 "the server closed the WebSocket connection with code 1001: going away"
343 );
344 assert_eq!(
345 WsError::closed(None, "").to_string(),
346 "the server closed the WebSocket connection without a status code"
347 );
348 assert_eq!(
349 WsError::connect_timeout(Duration::from_secs(10)).to_string(),
350 "no WebSocket connection within 10s"
351 );
352 assert_eq!(
353 WsError::no_answer(Duration::from_millis(200)).to_string(),
354 "no answer within 200ms"
355 );
356 assert_eq!(
357 refused(503, None).to_string(),
358 "HTTP error: 503 Service Unavailable"
359 );
360 }
361
362 #[test]
363 fn errors_cross_threads() {
364 fn assert_send_sync<T: Send + Sync + 'static>() {}
365 assert_send_sync::<WsError>();
366 }
367}