1use std::fmt;
7
8#[derive(Debug)]
10#[non_exhaustive]
11pub enum ConnectionError {
12 TcpConnect {
14 host: String,
15 port: u16,
16 source: std::io::Error,
17 },
18
19 DnsResolution {
21 address: String,
22 source: std::io::Error,
23 },
24
25 SocketConfig {
27 operation: String,
28 source: std::io::Error,
29 },
30
31 AuthenticationFailed { backend: String, response: String },
33
34 InvalidGreeting { backend: String, greeting: String },
36
37 PoolExhausted { backend: String, max_size: usize },
39
40 StaleConnection { backend: String, reason: String },
42
43 IoError(std::io::Error),
45
46 TlsHandshake {
48 backend: String,
49 source: Box<dyn std::error::Error + Send + Sync>,
50 },
51
52 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 #[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 #[must_use]
133 pub const fn is_authentication_error(&self) -> bool {
134 matches!(self, Self::AuthenticationFailed { .. })
135 }
136
137 #[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#[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}