Skip to main content

nautilus_network/transport/
error.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Neutral error type for the WebSocket transport abstraction.
17
18use std::io;
19
20use thiserror::Error;
21
22use super::message::CloseFrame;
23
24/// A backend-agnostic WebSocket transport error.
25///
26/// Each backend translates its native error type into this enum via `From` impls
27/// so the higher layers operate against a single error surface.
28#[derive(Debug, Error)]
29pub enum TransportError {
30    /// Underlying I/O error from the socket.
31    #[error("I/O error: {0}")]
32    Io(#[from] io::Error),
33
34    /// HTTP upgrade handshake failed.
35    #[error("handshake failed: {0}")]
36    Handshake(String),
37
38    /// URL was invalid or unsupported.
39    #[error("invalid URL: {0}")]
40    InvalidUrl(String),
41
42    /// TLS-layer failure during connect or stream operation.
43    #[error("TLS error: {0}")]
44    Tls(String),
45
46    /// WebSocket protocol violation reported by the peer or detected locally.
47    #[error("protocol error: {0}")]
48    Protocol(String),
49
50    /// Peer sent a close frame and the connection is closing.
51    #[error("connection closed by peer")]
52    ClosedByPeer(Option<CloseFrame>),
53
54    /// Connection closed without a close frame (abnormal).
55    #[error("connection closed")]
56    ConnectionClosed,
57
58    /// Connection reset by peer.
59    #[error("connection reset")]
60    ConnectionReset,
61
62    /// Message exceeded the configured maximum size.
63    #[error("message too large")]
64    MessageTooLarge,
65
66    /// Frame exceeded the configured maximum size.
67    #[error("frame too large")]
68    FrameTooLarge,
69
70    /// UTF-8 validation failed on a text frame.
71    ///
72    /// Only emitted by backends that validate (e.g. `tokio-tungstenite`); the
73    /// in-house HFT backend does not validate and will not produce this.
74    #[error("invalid UTF-8 in text frame")]
75    InvalidUtf8,
76
77    /// Backend returned an error not covered by other variants. Carries a
78    /// short description; consumers should treat as fatal.
79    #[error("transport error: {0}")]
80    Other(String),
81}
82
83impl TransportError {
84    /// Returns `true` if the error indicates the connection is no longer usable.
85    ///
86    /// `InvalidUrl` is the only non-fatal variant: a bad URL is a caller-side
87    /// configuration mistake that does not damage an existing connection.
88    /// Everything else (including `Io` and the catch-all `Other`) implies the
89    /// underlying transport cannot be reused.
90    #[must_use]
91    pub fn is_fatal(&self) -> bool {
92        !matches!(self, Self::InvalidUrl(_))
93    }
94
95    /// Returns `true` for connection-closed style errors.
96    #[must_use]
97    pub fn is_closed(&self) -> bool {
98        matches!(
99            self,
100            Self::ConnectionClosed | Self::ConnectionReset | Self::ClosedByPeer(_)
101        )
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use rstest::rstest;
108
109    use super::*;
110
111    #[rstest]
112    fn io_error_is_fatal() {
113        let err = TransportError::Io(io::Error::other("boom"));
114        assert!(err.is_fatal());
115        assert!(!err.is_closed());
116    }
117
118    #[rstest]
119    fn other_error_is_fatal() {
120        let err = TransportError::Other("unexpected".into());
121        assert!(err.is_fatal());
122        assert!(!err.is_closed());
123    }
124
125    #[rstest]
126    fn invalid_url_is_not_fatal() {
127        let err = TransportError::InvalidUrl("ws://".into());
128        assert!(!err.is_fatal());
129        assert!(!err.is_closed());
130    }
131
132    #[rstest]
133    fn closed_variants_are_closed_and_fatal() {
134        let err = TransportError::ConnectionClosed;
135        assert!(err.is_fatal());
136        assert!(err.is_closed());
137
138        let err = TransportError::ConnectionReset;
139        assert!(err.is_fatal());
140        assert!(err.is_closed());
141
142        let err = TransportError::ClosedByPeer(Some(CloseFrame::new(1000, "bye")));
143        assert!(err.is_fatal());
144        assert!(err.is_closed());
145    }
146
147    #[rstest]
148    fn protocol_error_is_fatal() {
149        let err = TransportError::Protocol("bad opcode".into());
150        assert!(err.is_fatal());
151        assert!(!err.is_closed());
152    }
153
154    #[rstest]
155    fn capacity_and_handshake_variants_are_fatal() {
156        for err in [
157            TransportError::MessageTooLarge,
158            TransportError::FrameTooLarge,
159            TransportError::InvalidUtf8,
160            TransportError::Tls("bad".into()),
161            TransportError::Handshake("bad".into()),
162        ] {
163            assert!(err.is_fatal(), "expected fatal: {err:?}");
164            assert!(!err.is_closed(), "expected not closed: {err:?}");
165        }
166    }
167}