Skip to main content

nntp_proxy/
connection_error.rs

1//! Connection error types for the NNTP proxy
2
3use std::io::ErrorKind;
4use thiserror::Error;
5
6/// Error kinds that indicate the client disconnected
7///
8/// These errors should not be logged as backend failures - they're normal
9/// when clients close connections.
10pub const DISCONNECT_KINDS: &[ErrorKind] = &[ErrorKind::BrokenPipe, ErrorKind::ConnectionReset];
11
12/// Error kinds that indicate the connection is broken and should not be reused
13///
14/// When these errors occur, the connection should be removed from the pool
15/// rather than returned for reuse.
16pub const CONNECTION_ERROR_KINDS: &[ErrorKind] = &[
17    ErrorKind::BrokenPipe,
18    ErrorKind::ConnectionReset,
19    ErrorKind::ConnectionAborted,
20    ErrorKind::UnexpectedEof,
21];
22
23/// Check if an I/O error kind indicates a client disconnect
24#[inline]
25#[must_use]
26pub fn is_disconnect_kind(kind: ErrorKind) -> bool {
27    DISCONNECT_KINDS.contains(&kind)
28}
29
30/// Check if an I/O error kind indicates a broken connection
31#[inline]
32#[must_use]
33pub fn is_connection_error_kind(kind: ErrorKind) -> bool {
34    CONNECTION_ERROR_KINDS.contains(&kind)
35}
36
37/// Errors that can occur during connection management
38#[derive(Debug, Error)]
39#[non_exhaustive]
40pub enum ConnectionError {
41    #[error("Authentication failed for backend '{backend}': {response}")]
42    AuthenticationFailed { backend: String, response: String },
43
44    #[error("Connection limit exceeded for backend '{backend}': {response}")]
45    ConnectionLimitExceeded { backend: String, response: String },
46
47    #[error("Invalid greeting from backend '{backend}': {greeting}")]
48    InvalidGreeting { backend: String, greeting: String },
49
50    #[error("Connection pool exhausted for backend '{backend}' (max size: {max_size})")]
51    PoolExhausted { backend: String, max_size: usize },
52
53    #[error("I/O error: {0}")]
54    IoError(#[from] std::io::Error),
55
56    #[error("TLS handshake failed for backend '{backend}': {source}")]
57    TlsHandshake {
58        backend: String,
59        #[source]
60        source: Box<dyn std::error::Error + Send + Sync>,
61    },
62
63    #[error("No DNS addresses found for {address}")]
64    DnsNoAddresses { address: String },
65
66    #[error("Password required but not configured for backend '{backend}'")]
67    PasswordRequired { backend: String },
68
69    #[error("Unexpected auth response from backend '{backend}': {response}")]
70    UnexpectedAuthResponse { backend: String, response: String },
71
72    #[error("Compression required but not supported by backend '{backend}': {response}")]
73    CompressionRequired { backend: String, response: String },
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use std::error::Error;
80
81    #[test]
82    fn test_authentication_failed_error() {
83        let err = ConnectionError::AuthenticationFailed {
84            backend: "news.example.com".to_string(),
85            response: "502 Authentication failed".to_string(),
86        };
87
88        let msg = err.to_string();
89        assert!(msg.contains("news.example.com"));
90        assert!(msg.contains("502"));
91    }
92
93    #[test]
94    fn test_pool_exhausted_error() {
95        let err = ConnectionError::PoolExhausted {
96            backend: "backend1".to_string(),
97            max_size: 20,
98        };
99
100        let msg = err.to_string();
101        assert!(msg.contains("backend1"));
102        assert!(msg.contains("20"));
103    }
104
105    #[test]
106    fn test_from_io_error() {
107        let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
108        let conn_err: ConnectionError = io_err.into();
109
110        assert!(matches!(conn_err, ConnectionError::IoError(_)));
111    }
112
113    #[test]
114    fn test_invalid_greeting_error() {
115        let err = ConnectionError::InvalidGreeting {
116            backend: "news.server.com".to_string(),
117            greeting: "500 Server error".to_string(),
118        };
119
120        let msg = err.to_string();
121        assert!(msg.contains("Invalid greeting"));
122        assert!(msg.contains("news.server.com"));
123    }
124
125    #[test]
126    fn io_error_disconnect_kinds_are_classified_correctly() {
127        let broken_pipe = ConnectionError::IoError(std::io::Error::new(
128            std::io::ErrorKind::BrokenPipe,
129            "broken pipe",
130        ));
131        assert!(
132            matches!(&broken_pipe, ConnectionError::IoError(e) if is_disconnect_kind(e.kind()))
133        );
134
135        let reset = ConnectionError::IoError(std::io::Error::new(
136            std::io::ErrorKind::ConnectionReset,
137            "reset",
138        ));
139        assert!(matches!(&reset, ConnectionError::IoError(e) if is_disconnect_kind(e.kind())));
140
141        let other = ConnectionError::IoError(std::io::Error::other("other"));
142        assert!(!matches!(&other, ConnectionError::IoError(e) if is_disconnect_kind(e.kind())));
143    }
144
145    #[test]
146    fn test_tls_handshake_error() {
147        let err = ConnectionError::TlsHandshake {
148            backend: "secure.server.com".to_string(),
149            source: Box::new(std::io::Error::other("TLS handshake failed")),
150        };
151
152        let msg = err.to_string();
153        assert!(msg.contains("TLS handshake failed"));
154        assert!(msg.contains("secure.server.com"));
155        assert!(err.source().is_some());
156    }
157
158    #[test]
159    fn test_connection_limit_exceeded_error() {
160        let err = ConnectionError::ConnectionLimitExceeded {
161            backend: "news.example.com".to_string(),
162            response: "482 Connection limit exceeded".to_string(),
163        };
164
165        let msg = err.to_string();
166        assert!(msg.contains("Connection limit exceeded"));
167        assert!(msg.contains("news.example.com"));
168        assert!(msg.contains("482"));
169    }
170}