nntp_proxy/
connection_error.rs

1//! Connection error types for the NNTP proxy
2//!
3//! This module provides detailed error types for connection management,
4//! making it easier to diagnose and handle different failure scenarios.
5
6use std::fmt;
7
8/// Errors that can occur during connection management
9#[derive(Debug)]
10#[non_exhaustive]
11pub enum ConnectionError {
12    /// TCP connection failed
13    TcpConnect {
14        host: String,
15        port: u16,
16        source: std::io::Error,
17    },
18
19    /// DNS resolution failed
20    DnsResolution {
21        address: String,
22        source: std::io::Error,
23    },
24
25    /// Socket configuration failed (buffer sizes, keepalive, etc.)
26    SocketConfig {
27        operation: String,
28        source: std::io::Error,
29    },
30
31    /// Backend authentication failed
32    AuthenticationFailed { backend: String, response: String },
33
34    /// Invalid or unexpected greeting from backend
35    InvalidGreeting { backend: String, greeting: String },
36
37    /// Connection pool exhausted
38    PoolExhausted { backend: String, max_size: usize },
39
40    /// Connection is stale or broken
41    StaleConnection { backend: String, reason: String },
42
43    /// I/O error during communication
44    IoError(std::io::Error),
45
46    /// TLS handshake failed
47    TlsHandshake {
48        backend: String,
49        source: Box<dyn std::error::Error + Send + Sync>,
50    },
51
52    /// Certificate verification failed
53    CertificateVerification { backend: String, reason: String },
54}
55
56impl fmt::Display for ConnectionError {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        match self {
59            Self::TcpConnect { host, port, source } => {
60                write!(f, "Failed to connect to {}:{}: {}", host, port, source)
61            }
62            Self::DnsResolution { address, source } => {
63                write!(f, "Failed to resolve DNS for {}: {}", address, source)
64            }
65            Self::SocketConfig { operation, source } => {
66                write!(f, "Failed to configure socket ({}): {}", operation, source)
67            }
68            Self::AuthenticationFailed { backend, response } => {
69                write!(
70                    f,
71                    "Authentication failed for backend '{}': {}",
72                    backend, response
73                )
74            }
75            Self::InvalidGreeting { backend, greeting } => {
76                write!(
77                    f,
78                    "Invalid greeting from backend '{}': {}",
79                    backend, greeting
80                )
81            }
82            Self::PoolExhausted { backend, max_size } => {
83                write!(
84                    f,
85                    "Connection pool exhausted for backend '{}' (max size: {})",
86                    backend, max_size
87                )
88            }
89            Self::StaleConnection { backend, reason } => {
90                write!(f, "Stale connection to backend '{}': {}", backend, reason)
91            }
92            Self::IoError(e) => write!(f, "I/O error: {}", e),
93            Self::TlsHandshake { backend, source } => {
94                write!(
95                    f,
96                    "TLS handshake failed for backend '{}': {}",
97                    backend, source
98                )
99            }
100            Self::CertificateVerification { backend, reason } => {
101                write!(
102                    f,
103                    "Certificate verification failed for backend '{}': {}",
104                    backend, reason
105                )
106            }
107        }
108    }
109}
110
111impl std::error::Error for ConnectionError {
112    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
113        match self {
114            Self::TcpConnect { source, .. } => Some(source),
115            Self::DnsResolution { source, .. } => Some(source),
116            Self::SocketConfig { source, .. } => Some(source),
117            Self::IoError(e) => Some(e),
118            Self::TlsHandshake { source, .. } => Some(source.as_ref()),
119            _ => None,
120        }
121    }
122}
123
124impl ConnectionError {
125    /// Check if this is a client disconnection (broken pipe)
126    #[must_use]
127    pub fn is_client_disconnect(&self) -> bool {
128        matches!(self, Self::IoError(e) if e.kind() == std::io::ErrorKind::BrokenPipe)
129    }
130
131    /// Check if this is an authentication error
132    #[must_use]
133    pub const fn is_authentication_error(&self) -> bool {
134        matches!(self, Self::AuthenticationFailed { .. })
135    }
136
137    /// Check if this is a network connectivity error
138    #[must_use]
139    pub const fn is_network_error(&self) -> bool {
140        matches!(self, Self::TcpConnect { .. } | Self::DnsResolution { .. })
141    }
142}
143
144impl From<std::io::Error> for ConnectionError {
145    fn from(err: std::io::Error) -> Self {
146        Self::IoError(err)
147    }
148}
149
150// Note: No need for From<ConnectionError> for anyhow::Error
151// anyhow has a blanket impl for all types implementing std::error::Error
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use std::error::Error;
157
158    #[test]
159    fn test_tcp_connect_error() {
160        let err = ConnectionError::TcpConnect {
161            host: "example.com".to_string(),
162            port: 119,
163            source: std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused"),
164        };
165
166        let msg = err.to_string();
167        assert!(msg.contains("example.com"));
168        assert!(msg.contains("119"));
169        assert!(msg.contains("refused"));
170    }
171
172    #[test]
173    fn test_authentication_failed_error() {
174        let err = ConnectionError::AuthenticationFailed {
175            backend: "news.example.com".to_string(),
176            response: "502 Authentication failed".to_string(),
177        };
178
179        let msg = err.to_string();
180        assert!(msg.contains("news.example.com"));
181        assert!(msg.contains("502"));
182    }
183
184    #[test]
185    fn test_pool_exhausted_error() {
186        let err = ConnectionError::PoolExhausted {
187            backend: "backend1".to_string(),
188            max_size: 20,
189        };
190
191        let msg = err.to_string();
192        assert!(msg.contains("backend1"));
193        assert!(msg.contains("20"));
194    }
195
196    #[test]
197    fn test_from_io_error() {
198        let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
199        let conn_err: ConnectionError = io_err.into();
200
201        assert!(matches!(conn_err, ConnectionError::IoError(_)));
202    }
203
204    #[test]
205    fn test_error_source() {
206        let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset");
207        let err = ConnectionError::TcpConnect {
208            host: "test.com".to_string(),
209            port: 119,
210            source: io_err,
211        };
212
213        assert!(err.source().is_some());
214    }
215
216    #[test]
217    fn test_invalid_greeting_error() {
218        let err = ConnectionError::InvalidGreeting {
219            backend: "news.server.com".to_string(),
220            greeting: "500 Server error".to_string(),
221        };
222
223        let msg = err.to_string();
224        assert!(msg.contains("Invalid greeting"));
225        assert!(msg.contains("news.server.com"));
226    }
227
228    #[test]
229    fn test_stale_connection_error() {
230        let err = ConnectionError::StaleConnection {
231            backend: "backend2".to_string(),
232            reason: "Connection closed by peer".to_string(),
233        };
234
235        let msg = err.to_string();
236        assert!(msg.contains("Stale"));
237        assert!(msg.contains("backend2"));
238    }
239
240    #[test]
241    fn test_is_client_disconnect() {
242        let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken pipe");
243        let err = ConnectionError::IoError(io_err);
244        assert!(err.is_client_disconnect());
245
246        let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset");
247        let err = ConnectionError::IoError(io_err);
248        assert!(!err.is_client_disconnect());
249    }
250
251    #[test]
252    fn test_is_authentication_error() {
253        let err = ConnectionError::AuthenticationFailed {
254            backend: "test".to_string(),
255            response: "failed".to_string(),
256        };
257        assert!(err.is_authentication_error());
258
259        let err = ConnectionError::IoError(std::io::Error::other("test"));
260        assert!(!err.is_authentication_error());
261    }
262
263    #[test]
264    fn test_is_network_error() {
265        let err = ConnectionError::TcpConnect {
266            host: "test.com".to_string(),
267            port: 119,
268            source: std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused"),
269        };
270        assert!(err.is_network_error());
271
272        let err = ConnectionError::DnsResolution {
273            address: "test.com".to_string(),
274            source: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
275        };
276        assert!(err.is_network_error());
277
278        let err = ConnectionError::AuthenticationFailed {
279            backend: "test".to_string(),
280            response: "failed".to_string(),
281        };
282        assert!(!err.is_network_error());
283    }
284}