nautilus_network/transport/
error.rs1use std::io;
19
20use thiserror::Error;
21
22use super::message::CloseFrame;
23
24#[derive(Debug, Error)]
29pub enum TransportError {
30 #[error("I/O error: {0}")]
32 Io(#[from] io::Error),
33
34 #[error("handshake failed: {0}")]
36 Handshake(String),
37
38 #[error("WebSocket upgrade rejected with status {0}")]
40 UpgradeRejected(u16),
41
42 #[error("proxy CONNECT rejected with status {0}")]
44 ProxyConnectRejected(u16),
45
46 #[error("invalid URL: {0}")]
48 InvalidUrl(String),
49
50 #[error("TLS error: {0}")]
52 Tls(String),
53
54 #[error("protocol error: {0}")]
56 Protocol(String),
57
58 #[error("connection closed by peer")]
60 ClosedByPeer(Option<CloseFrame>),
61
62 #[error("connection closed")]
64 ConnectionClosed,
65
66 #[error("connection reset")]
68 ConnectionReset,
69
70 #[error("message too large")]
72 MessageTooLarge,
73
74 #[error("frame too large")]
76 FrameTooLarge,
77
78 #[error("invalid UTF-8 in text frame")]
83 InvalidUtf8,
84
85 #[error("transport error: {0}")]
88 Other(String),
89}
90
91impl TransportError {
92 #[must_use]
96 pub fn redacted_message(&self) -> String {
97 match self {
98 Self::Io(e) => format!("I/O error: {:?}", e.kind()),
99 Self::Handshake(_) => "handshake failed".to_string(),
100 Self::InvalidUrl(_) => "invalid URL".to_string(),
101 Self::Tls(_) => "TLS error".to_string(),
102 Self::Protocol(_) => "protocol error".to_string(),
103 Self::Other(_) => "transport error".to_string(),
104 Self::UpgradeRejected(_)
105 | Self::ProxyConnectRejected(_)
106 | Self::ClosedByPeer(_)
107 | Self::ConnectionClosed
108 | Self::ConnectionReset
109 | Self::MessageTooLarge
110 | Self::FrameTooLarge
111 | Self::InvalidUtf8 => self.to_string(),
112 }
113 }
114
115 #[must_use]
122 pub fn is_fatal(&self) -> bool {
123 !matches!(self, Self::InvalidUrl(_))
124 }
125
126 #[must_use]
128 pub fn is_closed(&self) -> bool {
129 matches!(
130 self,
131 Self::ConnectionClosed | Self::ConnectionReset | Self::ClosedByPeer(_)
132 )
133 }
134}
135
136pub(crate) const fn retryable_status(status: u16) -> bool {
138 matches!(status, 408 | 425 | 429 | 500..=599)
139}
140
141#[cfg(test)]
142mod tests {
143 use rstest::rstest;
144
145 use super::*;
146
147 #[rstest]
148 #[case(
149 TransportError::Io(io::Error::new(io::ErrorKind::ConnectionRefused, "SYN-secret")),
150 "I/O error: ConnectionRefused"
151 )]
152 #[case(
153 TransportError::UpgradeRejected(401),
154 "WebSocket upgrade rejected with status 401"
155 )]
156 #[case(
157 TransportError::ProxyConnectRejected(407),
158 "proxy CONNECT rejected with status 407"
159 )]
160 #[case(TransportError::Handshake("SYN-secret".to_string()), "handshake failed")]
161 #[case(TransportError::InvalidUrl("SYN-secret".to_string()), "invalid URL")]
162 #[case(TransportError::Tls("SYN-secret".to_string()), "TLS error")]
163 #[case(TransportError::Protocol("SYN-secret".to_string()), "protocol error")]
164 #[case(
165 TransportError::ClosedByPeer(Some(CloseFrame::new(1000, "SYN-secret"))),
166 "connection closed by peer"
167 )]
168 #[case(TransportError::ConnectionClosed, "connection closed")]
169 #[case(TransportError::ConnectionReset, "connection reset")]
170 #[case(TransportError::MessageTooLarge, "message too large")]
171 #[case(TransportError::FrameTooLarge, "frame too large")]
172 #[case(TransportError::InvalidUtf8, "invalid UTF-8 in text frame")]
173 #[case(TransportError::Other("SYN-secret".to_string()), "transport error")]
174 fn test_redacted_message_preserves_classification(
175 #[case] error: TransportError,
176 #[case] expected: &str,
177 ) {
178 assert_eq!(error.redacted_message(), expected);
179 }
180
181 #[rstest]
182 fn io_error_is_fatal() {
183 let err = TransportError::Io(io::Error::other("boom"));
184 assert!(err.is_fatal());
185 assert!(!err.is_closed());
186 }
187
188 #[rstest]
189 fn other_error_is_fatal() {
190 let err = TransportError::Other("unexpected".into());
191 assert!(err.is_fatal());
192 assert!(!err.is_closed());
193 }
194
195 #[rstest]
196 fn invalid_url_is_not_fatal() {
197 let err = TransportError::InvalidUrl("ws://".into());
198 assert!(!err.is_fatal());
199 assert!(!err.is_closed());
200 }
201
202 #[rstest]
203 fn closed_variants_are_closed_and_fatal() {
204 let err = TransportError::ConnectionClosed;
205 assert!(err.is_fatal());
206 assert!(err.is_closed());
207
208 let err = TransportError::ConnectionReset;
209 assert!(err.is_fatal());
210 assert!(err.is_closed());
211
212 let err = TransportError::ClosedByPeer(Some(CloseFrame::new(1000, "bye")));
213 assert!(err.is_fatal());
214 assert!(err.is_closed());
215 }
216
217 #[rstest]
218 fn protocol_error_is_fatal() {
219 let err = TransportError::Protocol("bad opcode".into());
220 assert!(err.is_fatal());
221 assert!(!err.is_closed());
222 }
223
224 #[rstest]
225 fn capacity_and_handshake_variants_are_fatal() {
226 for err in [
227 TransportError::MessageTooLarge,
228 TransportError::FrameTooLarge,
229 TransportError::InvalidUtf8,
230 TransportError::Tls("bad".into()),
231 TransportError::Handshake("bad".into()),
232 TransportError::UpgradeRejected(429),
233 TransportError::ProxyConnectRejected(503),
234 ] {
235 assert!(err.is_fatal(), "expected fatal: {err:?}");
236 assert!(!err.is_closed(), "expected not closed: {err:?}");
237 }
238 }
239}