nntp_proxy/
connection_error.rs1use std::io::ErrorKind;
4use thiserror::Error;
5
6pub const DISCONNECT_KINDS: &[ErrorKind] = &[ErrorKind::BrokenPipe, ErrorKind::ConnectionReset];
11
12pub const CONNECTION_ERROR_KINDS: &[ErrorKind] = &[
17 ErrorKind::BrokenPipe,
18 ErrorKind::ConnectionReset,
19 ErrorKind::ConnectionAborted,
20 ErrorKind::UnexpectedEof,
21];
22
23#[inline]
25#[must_use]
26pub fn is_disconnect_kind(kind: ErrorKind) -> bool {
27 DISCONNECT_KINDS.contains(&kind)
28}
29
30#[inline]
32#[must_use]
33pub fn is_connection_error_kind(kind: ErrorKind) -> bool {
34 CONNECTION_ERROR_KINDS.contains(&kind)
35}
36
37#[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}