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)]
10pub enum ConnectionError {
11    /// TCP connection failed
12    TcpConnect {
13        host: String,
14        port: u16,
15        source: std::io::Error,
16    },
17
18    /// DNS resolution failed
19    DnsResolution {
20        address: String,
21        source: std::io::Error,
22    },
23
24    /// Socket configuration failed (buffer sizes, keepalive, etc.)
25    SocketConfig {
26        operation: String,
27        source: std::io::Error,
28    },
29
30    /// Backend authentication failed
31    AuthenticationFailed { backend: String, response: String },
32
33    /// Invalid or unexpected greeting from backend
34    InvalidGreeting { backend: String, greeting: String },
35
36    /// Connection pool exhausted
37    PoolExhausted { backend: String, max_size: usize },
38
39    /// Connection is stale or broken
40    StaleConnection { backend: String, reason: String },
41
42    /// I/O error during communication
43    IoError(std::io::Error),
44
45    /// TLS handshake failed
46    TlsHandshake {
47        backend: String,
48        source: Box<dyn std::error::Error + Send + Sync>,
49    },
50
51    /// Certificate verification failed
52    CertificateVerification { backend: String, reason: String },
53}
54
55impl fmt::Display for ConnectionError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        match self {
58            Self::TcpConnect { host, port, source } => {
59                write!(f, "Failed to connect to {}:{}: {}", host, port, source)
60            }
61            Self::DnsResolution { address, source } => {
62                write!(f, "Failed to resolve DNS for {}: {}", address, source)
63            }
64            Self::SocketConfig { operation, source } => {
65                write!(f, "Failed to configure socket ({}): {}", operation, source)
66            }
67            Self::AuthenticationFailed { backend, response } => {
68                write!(
69                    f,
70                    "Authentication failed for backend '{}': {}",
71                    backend, response
72                )
73            }
74            Self::InvalidGreeting { backend, greeting } => {
75                write!(
76                    f,
77                    "Invalid greeting from backend '{}': {}",
78                    backend, greeting
79                )
80            }
81            Self::PoolExhausted { backend, max_size } => {
82                write!(
83                    f,
84                    "Connection pool exhausted for backend '{}' (max size: {})",
85                    backend, max_size
86                )
87            }
88            Self::StaleConnection { backend, reason } => {
89                write!(f, "Stale connection to backend '{}': {}", backend, reason)
90            }
91            Self::IoError(e) => write!(f, "I/O error: {}", e),
92            Self::TlsHandshake { backend, source } => {
93                write!(
94                    f,
95                    "TLS handshake failed for backend '{}': {}",
96                    backend, source
97                )
98            }
99            Self::CertificateVerification { backend, reason } => {
100                write!(
101                    f,
102                    "Certificate verification failed for backend '{}': {}",
103                    backend, reason
104                )
105            }
106        }
107    }
108}
109
110impl std::error::Error for ConnectionError {
111    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
112        match self {
113            Self::TcpConnect { source, .. } => Some(source),
114            Self::DnsResolution { source, .. } => Some(source),
115            Self::SocketConfig { source, .. } => Some(source),
116            Self::IoError(e) => Some(e),
117            Self::TlsHandshake { source, .. } => Some(source.as_ref()),
118            _ => None,
119        }
120    }
121}
122
123impl From<std::io::Error> for ConnectionError {
124    fn from(err: std::io::Error) -> Self {
125        Self::IoError(err)
126    }
127}
128
129// Note: No need for From<ConnectionError> for anyhow::Error
130// anyhow has a blanket impl for all types implementing std::error::Error
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use std::error::Error;
136
137    #[test]
138    fn test_tcp_connect_error() {
139        let err = ConnectionError::TcpConnect {
140            host: "example.com".to_string(),
141            port: 119,
142            source: std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused"),
143        };
144
145        let msg = err.to_string();
146        assert!(msg.contains("example.com"));
147        assert!(msg.contains("119"));
148        assert!(msg.contains("refused"));
149    }
150
151    #[test]
152    fn test_authentication_failed_error() {
153        let err = ConnectionError::AuthenticationFailed {
154            backend: "news.example.com".to_string(),
155            response: "502 Authentication failed".to_string(),
156        };
157
158        let msg = err.to_string();
159        assert!(msg.contains("news.example.com"));
160        assert!(msg.contains("502"));
161    }
162
163    #[test]
164    fn test_pool_exhausted_error() {
165        let err = ConnectionError::PoolExhausted {
166            backend: "backend1".to_string(),
167            max_size: 20,
168        };
169
170        let msg = err.to_string();
171        assert!(msg.contains("backend1"));
172        assert!(msg.contains("20"));
173    }
174
175    #[test]
176    fn test_from_io_error() {
177        let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
178        let conn_err: ConnectionError = io_err.into();
179
180        assert!(matches!(conn_err, ConnectionError::IoError(_)));
181    }
182
183    #[test]
184    fn test_error_source() {
185        let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset");
186        let err = ConnectionError::TcpConnect {
187            host: "test.com".to_string(),
188            port: 119,
189            source: io_err,
190        };
191
192        assert!(err.source().is_some());
193    }
194
195    #[test]
196    fn test_invalid_greeting_error() {
197        let err = ConnectionError::InvalidGreeting {
198            backend: "news.server.com".to_string(),
199            greeting: "500 Server error".to_string(),
200        };
201
202        let msg = err.to_string();
203        assert!(msg.contains("Invalid greeting"));
204        assert!(msg.contains("news.server.com"));
205    }
206
207    #[test]
208    fn test_stale_connection_error() {
209        let err = ConnectionError::StaleConnection {
210            backend: "backend2".to_string(),
211            reason: "Connection closed by peer".to_string(),
212        };
213
214        let msg = err.to_string();
215        assert!(msg.contains("Stale"));
216        assert!(msg.contains("backend2"));
217    }
218}