nntp-proxy 0.5.0

High-performance NNTP proxy server with connection pooling and authentication
Documentation
//! Connection error types for the NNTP proxy

use std::io::ErrorKind;
use thiserror::Error;

/// Error kinds that indicate the client disconnected
///
/// These errors should not be logged as backend failures - they're normal
/// when clients close connections.
pub const DISCONNECT_KINDS: &[ErrorKind] = &[ErrorKind::BrokenPipe, ErrorKind::ConnectionReset];

/// Error kinds that indicate the connection is broken and should not be reused
///
/// When these errors occur, the connection should be removed from the pool
/// rather than returned for reuse.
pub const CONNECTION_ERROR_KINDS: &[ErrorKind] = &[
    ErrorKind::BrokenPipe,
    ErrorKind::ConnectionReset,
    ErrorKind::ConnectionAborted,
    ErrorKind::UnexpectedEof,
];

/// Check if an I/O error kind indicates a client disconnect
#[inline]
#[must_use]
pub fn is_disconnect_kind(kind: ErrorKind) -> bool {
    DISCONNECT_KINDS.contains(&kind)
}

/// Check if an I/O error kind indicates a broken connection
#[inline]
#[must_use]
pub fn is_connection_error_kind(kind: ErrorKind) -> bool {
    CONNECTION_ERROR_KINDS.contains(&kind)
}

/// Errors that can occur during connection management
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ConnectionError {
    #[error("Authentication failed for backend '{backend}': {response}")]
    AuthenticationFailed { backend: String, response: String },

    #[error("Connection limit exceeded for backend '{backend}': {response}")]
    ConnectionLimitExceeded { backend: String, response: String },

    #[error("Invalid greeting from backend '{backend}': {greeting}")]
    InvalidGreeting { backend: String, greeting: String },

    #[error("Connection pool exhausted for backend '{backend}' (max size: {max_size})")]
    PoolExhausted { backend: String, max_size: usize },

    #[error("I/O error: {0}")]
    IoError(#[from] std::io::Error),

    #[error("TLS handshake failed for backend '{backend}': {source}")]
    TlsHandshake {
        backend: String,
        #[source]
        source: Box<dyn std::error::Error + Send + Sync>,
    },

    #[error("No DNS addresses found for {address}")]
    DnsNoAddresses { address: String },

    #[error("Password required but not configured for backend '{backend}'")]
    PasswordRequired { backend: String },

    #[error("Unexpected auth response from backend '{backend}': {response}")]
    UnexpectedAuthResponse { backend: String, response: String },

    #[error("Compression required but not supported by backend '{backend}': {response}")]
    CompressionRequired { backend: String, response: String },
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::error::Error;

    #[test]
    fn test_authentication_failed_error() {
        let err = ConnectionError::AuthenticationFailed {
            backend: "news.example.com".to_string(),
            response: "502 Authentication failed".to_string(),
        };

        let msg = err.to_string();
        assert!(msg.contains("news.example.com"));
        assert!(msg.contains("502"));
    }

    #[test]
    fn test_pool_exhausted_error() {
        let err = ConnectionError::PoolExhausted {
            backend: "backend1".to_string(),
            max_size: 20,
        };

        let msg = err.to_string();
        assert!(msg.contains("backend1"));
        assert!(msg.contains("20"));
    }

    #[test]
    fn test_from_io_error() {
        let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
        let conn_err: ConnectionError = io_err.into();

        assert!(matches!(conn_err, ConnectionError::IoError(_)));
    }

    #[test]
    fn test_invalid_greeting_error() {
        let err = ConnectionError::InvalidGreeting {
            backend: "news.server.com".to_string(),
            greeting: "500 Server error".to_string(),
        };

        let msg = err.to_string();
        assert!(msg.contains("Invalid greeting"));
        assert!(msg.contains("news.server.com"));
    }

    #[test]
    fn io_error_disconnect_kinds_are_classified_correctly() {
        let broken_pipe = ConnectionError::IoError(std::io::Error::new(
            std::io::ErrorKind::BrokenPipe,
            "broken pipe",
        ));
        assert!(
            matches!(&broken_pipe, ConnectionError::IoError(e) if is_disconnect_kind(e.kind()))
        );

        let reset = ConnectionError::IoError(std::io::Error::new(
            std::io::ErrorKind::ConnectionReset,
            "reset",
        ));
        assert!(matches!(&reset, ConnectionError::IoError(e) if is_disconnect_kind(e.kind())));

        let other = ConnectionError::IoError(std::io::Error::other("other"));
        assert!(!matches!(&other, ConnectionError::IoError(e) if is_disconnect_kind(e.kind())));
    }

    #[test]
    fn test_tls_handshake_error() {
        let err = ConnectionError::TlsHandshake {
            backend: "secure.server.com".to_string(),
            source: Box::new(std::io::Error::other("TLS handshake failed")),
        };

        let msg = err.to_string();
        assert!(msg.contains("TLS handshake failed"));
        assert!(msg.contains("secure.server.com"));
        assert!(err.source().is_some());
    }

    #[test]
    fn test_connection_limit_exceeded_error() {
        let err = ConnectionError::ConnectionLimitExceeded {
            backend: "news.example.com".to_string(),
            response: "482 Connection limit exceeded".to_string(),
        };

        let msg = err.to_string();
        assert!(msg.contains("Connection limit exceeded"));
        assert!(msg.contains("news.example.com"));
        assert!(msg.contains("482"));
    }
}