Skip to main content

nautilus_network/
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//! Network error types.
17
18use std::{fmt::Display, io};
19
20use thiserror::Error;
21
22/// Error type for send operations in network clients.
23#[derive(Clone, Error, Debug)]
24pub enum SendError {
25    /// The send input is invalid.
26    #[error("send failed: invalid input ({0})")]
27    InvalidInput(String),
28    /// The writer has reached the message limit for this send.
29    #[error("send failed: writer buffer full")]
30    BufferFull,
31    /// The client has been closed or is disconnecting.
32    #[error("send failed: client closed or disconnecting")]
33    Closed,
34    /// Timed out waiting for the client to become active.
35    #[error("send failed: timeout waiting for active state")]
36    Timeout,
37    /// Timed out while writing to the transport, so delivery is undetermined.
38    ///
39    /// Unlike [`SendError::Timeout`], which reports that a send never started, the write was
40    /// cancelled after it began: the peer may or may not have received the message. Callers must
41    /// not treat this as a plain retry.
42    #[error("send failed: timed out writing to transport, delivery undetermined")]
43    WriteTimeout,
44    /// The connection changed before an ownership-bound message reached the writer.
45    #[error("send failed: connection changed before write")]
46    ConnectionChanged,
47    /// Failed to send because the writer channel is closed.
48    #[error("send failed: broken pipe ({0})")]
49    BrokenPipe(String),
50}
51
52/// Result type for client configuration validation.
53pub type NetworkConfigResult<T> = Result<T, NetworkConfigError>;
54
55/// A validation error for a network client configuration.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum NetworkConfigError {
58    /// A field value is empty or outside its accepted range.
59    Invalid { field: String, reason: String },
60    /// Multiple validation errors were collected.
61    Multiple { errors: Vec<Self> },
62}
63
64impl NetworkConfigError {
65    /// Creates a [`NetworkConfigError::Invalid`] for `field` with the given `reason`.
66    pub fn invalid(field: impl Into<String>, reason: impl Into<String>) -> Self {
67        Self::Invalid {
68            field: field.into(),
69            reason: reason.into(),
70        }
71    }
72
73    /// Converts collected errors into a single result.
74    ///
75    /// Returns `Ok(())` when `errors` is empty, the sole error when one was collected, or a
76    /// [`NetworkConfigError::Multiple`] otherwise.
77    pub(crate) fn collect(mut errors: Vec<Self>) -> NetworkConfigResult<()> {
78        match errors.len() {
79            0 => Ok(()),
80            1 => Err(errors.remove(0)),
81            _ => Err(Self::Multiple { errors }),
82        }
83    }
84}
85
86impl Display for NetworkConfigError {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        match self {
89            Self::Invalid { field, reason } => write!(f, "invalid {field}: {reason}"),
90            Self::Multiple { errors } => {
91                for (index, error) in errors.iter().enumerate() {
92                    if index > 0 {
93                        write!(f, "; ")?;
94                    }
95                    write!(f, "{error}")?;
96                }
97                Ok(())
98            }
99        }
100    }
101}
102
103impl std::error::Error for NetworkConfigError {}
104
105pub(crate) fn is_connection_drop_io_error(err: &io::Error) -> bool {
106    matches!(
107        err.kind(),
108        io::ErrorKind::BrokenPipe
109            | io::ErrorKind::ConnectionAborted
110            | io::ErrorKind::ConnectionReset
111            | io::ErrorKind::NotConnected
112            | io::ErrorKind::TimedOut
113            | io::ErrorKind::UnexpectedEof
114    )
115}
116
117#[cfg(test)]
118mod tests {
119    use rstest::rstest;
120
121    use super::*;
122
123    #[rstest]
124    #[case(io::ErrorKind::BrokenPipe, true)]
125    #[case(io::ErrorKind::ConnectionAborted, true)]
126    #[case(io::ErrorKind::ConnectionReset, true)]
127    #[case(io::ErrorKind::NotConnected, true)]
128    #[case(io::ErrorKind::TimedOut, true)]
129    #[case(io::ErrorKind::UnexpectedEof, true)]
130    #[case(io::ErrorKind::InvalidInput, false)]
131    #[case(io::ErrorKind::PermissionDenied, false)]
132    fn connection_drop_io_error_classification(
133        #[case] kind: io::ErrorKind,
134        #[case] expected: bool,
135    ) {
136        let err = io::Error::from(kind);
137
138        assert_eq!(is_connection_drop_io_error(&err), expected);
139    }
140
141    #[rstest]
142    fn test_invalid_display() {
143        let err = NetworkConfigError::invalid("url", "must not be empty");
144
145        assert_eq!(err.to_string(), "invalid url: must not be empty");
146    }
147
148    #[rstest]
149    fn test_multiple_display_joins_errors() {
150        let err = NetworkConfigError::Multiple {
151            errors: vec![
152                NetworkConfigError::invalid("url", "must not be empty"),
153                NetworkConfigError::invalid("idle_timeout_ms", "must be positive, was 0"),
154            ],
155        };
156
157        assert_eq!(
158            err.to_string(),
159            "invalid url: must not be empty; invalid idle_timeout_ms: must be positive, was 0"
160        );
161    }
162
163    #[rstest]
164    fn test_collect_returns_bare_error_for_single() {
165        let errors = vec![NetworkConfigError::invalid("url", "must not be empty")];
166
167        let result = NetworkConfigError::collect(errors);
168
169        assert!(matches!(result, Err(NetworkConfigError::Invalid { field, .. }) if field == "url"));
170    }
171}