nntp_proxy/session/
error_classification.rs

1//! Error classification utilities for routing errors
2//!
3//! Provides helpers to classify errors wrapped in anyhow::Error
4
5use crate::connection_error::ConnectionError;
6use std::io::ErrorKind;
7
8/// Classify an anyhow error and determine appropriate handling
9pub struct ErrorClassifier;
10
11impl ErrorClassifier {
12    /// Check if error is a client disconnect (broken pipe)
13    pub fn is_client_disconnect(error: &anyhow::Error) -> bool {
14        // First check if it's a ConnectionError
15        if let Some(conn_err) = error.downcast_ref::<ConnectionError>() {
16            return conn_err.is_client_disconnect();
17        }
18
19        // Check raw IO error
20        if let Some(io_err) = error.downcast_ref::<std::io::Error>() {
21            return io_err.kind() == ErrorKind::BrokenPipe;
22        }
23
24        false
25    }
26
27    /// Check if error is an authentication failure
28    pub fn is_authentication_error(error: &anyhow::Error) -> bool {
29        // Check for ConnectionError::AuthenticationFailed
30        if let Some(conn_err) = error.downcast_ref::<ConnectionError>() {
31            return conn_err.is_authentication_error();
32        }
33
34        // Check error message as fallback
35        let error_str = error.to_string();
36        error_str.contains("Auth failed") || error_str.contains("Authentication Failed")
37    }
38
39    /// Check if error is a network/connectivity issue
40    pub fn is_network_error(error: &anyhow::Error) -> bool {
41        if let Some(conn_err) = error.downcast_ref::<ConnectionError>() {
42            return conn_err.is_network_error();
43        }
44        false
45    }
46
47    /// Check if we should skip sending error response to client
48    pub fn should_skip_client_error_response(error: &anyhow::Error) -> bool {
49        Self::is_client_disconnect(error)
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn test_is_client_disconnect_with_io_error() {
59        let io_err = std::io::Error::new(ErrorKind::BrokenPipe, "broken pipe");
60        let err: anyhow::Error = io_err.into();
61        assert!(ErrorClassifier::is_client_disconnect(&err));
62    }
63
64    #[test]
65    fn test_is_client_disconnect_with_connection_error() {
66        let io_err = std::io::Error::new(ErrorKind::BrokenPipe, "broken pipe");
67        let conn_err = ConnectionError::IoError(io_err);
68        let err: anyhow::Error = conn_err.into();
69        assert!(ErrorClassifier::is_client_disconnect(&err));
70    }
71
72    #[test]
73    fn test_is_authentication_error_with_connection_error() {
74        let conn_err = ConnectionError::AuthenticationFailed {
75            backend: "test".to_string(),
76            response: "502 failed".to_string(),
77        };
78        let err: anyhow::Error = conn_err.into();
79        assert!(ErrorClassifier::is_authentication_error(&err));
80    }
81
82    #[test]
83    fn test_is_authentication_error_with_message() {
84        let err = anyhow::anyhow!("Auth failed: invalid credentials");
85        assert!(ErrorClassifier::is_authentication_error(&err));
86    }
87
88    #[test]
89    fn test_should_skip_client_error_response() {
90        let io_err = std::io::Error::new(ErrorKind::BrokenPipe, "broken");
91        let err: anyhow::Error = io_err.into();
92        assert!(ErrorClassifier::should_skip_client_error_response(&err));
93
94        let other_err = anyhow::anyhow!("some other error");
95        assert!(!ErrorClassifier::should_skip_client_error_response(
96            &other_err
97        ));
98    }
99
100    #[test]
101    fn test_client_disconnect_classification() {
102        // Verify broken pipe is detected as client disconnect
103        let broken_pipe = std::io::Error::new(ErrorKind::BrokenPipe, "pipe");
104        let err: anyhow::Error = broken_pipe.into();
105        assert!(ErrorClassifier::is_client_disconnect(&err));
106        assert!(ErrorClassifier::should_skip_client_error_response(&err));
107
108        // Verify other errors are NOT client disconnects
109        let reset = std::io::Error::new(ErrorKind::ConnectionReset, "reset");
110        let err: anyhow::Error = reset.into();
111        assert!(!ErrorClassifier::is_client_disconnect(&err));
112        assert!(!ErrorClassifier::should_skip_client_error_response(&err));
113    }
114
115    #[test]
116    fn test_error_classification_layering() {
117        // Test that we can distinguish between different error types
118        // for proper logging at different levels
119
120        // 1. Client disconnect (broken pipe) - should be DEBUG in streaming layer
121        let broken_pipe = std::io::Error::new(ErrorKind::BrokenPipe, "pipe");
122        let err: anyhow::Error = broken_pipe.into();
123        assert!(ErrorClassifier::is_client_disconnect(&err));
124        assert!(!ErrorClassifier::is_authentication_error(&err));
125        assert!(!ErrorClassifier::is_network_error(&err));
126
127        // 2. Auth failure - should be ERROR
128        let auth_fail = ConnectionError::AuthenticationFailed {
129            backend: "test".to_string(),
130            response: "nope".to_string(),
131        };
132        let err: anyhow::Error = auth_fail.into();
133        assert!(!ErrorClassifier::is_client_disconnect(&err));
134        assert!(ErrorClassifier::is_authentication_error(&err));
135        assert!(!ErrorClassifier::is_network_error(&err));
136
137        // 3. Network error - should be WARN
138        let net_err = ConnectionError::TcpConnect {
139            host: "test".to_string(),
140            port: 119,
141            source: std::io::Error::new(ErrorKind::ConnectionRefused, "refused"),
142        };
143        let err: anyhow::Error = net_err.into();
144        assert!(!ErrorClassifier::is_client_disconnect(&err));
145        assert!(!ErrorClassifier::is_authentication_error(&err));
146        assert!(ErrorClassifier::is_network_error(&err));
147    }
148}