stream-tungstenite 0.6.1

A streaming implementation of the Tungstenite WebSocket protocol
Documentation
//! Unified error types for the WebSocket client library.

use std::time::Duration;

/// Top-level client error
#[derive(thiserror::Error, Debug)]
pub enum ClientError {
    #[error("Connection failed: {0}")]
    Connect(#[from] ConnectError),

    #[error("Handshake failed: {0}")]
    Handshake(#[from] HandshakeError),

    #[error("Send failed: {0}")]
    Send(#[from] SendError),

    #[error("Receive failed: {0}")]
    Receive(#[from] ReceiveError),

    #[error("Supervisor error: {0}")]
    Supervisor(#[from] SupervisorError),

    #[error("Extension error: {0}")]
    Extension(#[from] ExtensionError),

    #[error("Invalid configuration: {0}")]
    Config(String),

    #[error("Client is already running")]
    AlreadyRunning,

    /// Graceful shutdown did not complete within the specified timeout
    #[error("Graceful shutdown timed out after {0:?}")]
    ShutdownTimeout(Duration),

    /// Error with additional context (preserves source error)
    #[error("{context}: {source}")]
    Context {
        /// Human-readable context message
        context: String,
        /// Original error as the source of this error
        #[source]
        source: Box<ClientError>,
    },
}

/// Connection-related errors
#[derive(thiserror::Error, Debug, Clone)]
pub enum ConnectError {
    #[error("Invalid URL: {0}")]
    InvalidUrl(String),

    #[error("Invalid URI: {0}")]
    InvalidUri(String),

    #[error("DNS resolution failed: {0}")]
    DnsResolution(String),

    #[error("TCP connection failed: {0}")]
    TcpConnect(String),

    #[error("TCP failed: {0}")]
    TcpFailed(String),

    #[error("TLS handshake failed: {0}")]
    TlsHandshake(String),

    #[error("TLS configuration error: {0}")]
    Tls(String),

    #[error("WebSocket upgrade failed: {0}")]
    WebSocketUpgrade(String),

    #[error("WebSocket connection failed: {0}")]
    WebSocketFailed(String),

    #[error("Handshake failed: {0}")]
    HandshakeFailed(String),

    #[error("Connection timeout after {0:?}")]
    Timeout(Duration),

    #[error("Connection refused")]
    Refused,

    #[error("IO error: {0}")]
    Io(String),
}

impl ConnectError {
    /// Whether this error is retryable
    #[must_use]
    pub const fn is_retryable(&self) -> bool {
        match self {
            // Not retryable - configuration/permanent errors
            Self::InvalidUrl(_) | Self::InvalidUri(_) | Self::TlsHandshake(_) | Self::Tls(_) => {
                false
            } // Certificate issues won't fix themselves

            // Retryable - transient errors
            Self::DnsResolution(_)
            | Self::TcpConnect(_)
            | Self::TcpFailed(_)
            | Self::WebSocketUpgrade(_)
            | Self::WebSocketFailed(_)
            | Self::HandshakeFailed(_)
            | Self::Timeout(_)
            | Self::Refused
            | Self::Io(_) => true,
        }
    }

    /// Suggested retry delay for this error type
    #[must_use]
    pub const fn suggested_delay(&self) -> Option<Duration> {
        match self {
            Self::DnsResolution(_) => Some(Duration::from_millis(500)),
            Self::Timeout(_) => Some(Duration::from_secs(5)),
            Self::Refused => Some(Duration::from_secs(2)),
            _ => None, // Use default backoff
        }
    }
}

impl From<std::io::Error> for ConnectError {
    fn from(e: std::io::Error) -> Self {
        Self::Io(e.to_string())
    }
}

impl From<tungstenite::Error> for ConnectError {
    fn from(e: tungstenite::Error) -> Self {
        Self::WebSocketUpgrade(e.to_string())
    }
}

/// Handshake-related errors
#[derive(thiserror::Error, Debug, Clone)]
pub enum HandshakeError {
    #[error("Handshake failed: {0}")]
    Failed(String),

    #[error("Authentication failed: {0}")]
    AuthFailed(String),

    #[error("Handshake timeout after {0:?}")]
    Timeout(Duration),

    #[error("Protocol error: {0}")]
    Protocol(String),

    #[error("WebSocket error: {0}")]
    WebSocket(String),
}

impl HandshakeError {
    /// Whether this handshake error is retryable
    #[must_use]
    pub const fn is_retryable(&self) -> bool {
        match self {
            // Not retryable - auth/protocol issues are permanent
            Self::AuthFailed(_) | Self::Protocol(_) => false,
            // Retryable - transient errors
            Self::Failed(_) | Self::Timeout(_) | Self::WebSocket(_) => true,
        }
    }
}

impl From<tungstenite::Error> for HandshakeError {
    fn from(e: tungstenite::Error) -> Self {
        Self::WebSocket(e.to_string())
    }
}

/// Send-related errors
#[derive(thiserror::Error, Debug, Clone)]
pub enum SendError {
    #[error("Not connected")]
    NotConnected,

    #[error("Channel closed")]
    ChannelClosed,

    #[error("Send buffer is full")]
    ChannelFull,

    #[error("Send timed out after {0:?}")]
    Timeout(Duration),

    #[error("Message too large: {size} bytes (max: {max})")]
    MessageTooLarge { size: usize, max: usize },

    #[error("WebSocket error: {0}")]
    WebSocket(String),
}

impl From<tungstenite::Error> for SendError {
    fn from(e: tungstenite::Error) -> Self {
        Self::WebSocket(e.to_string())
    }
}

/// Receive-related errors
#[derive(thiserror::Error, Debug, Clone)]
pub enum ReceiveError {
    #[error("Stream closed")]
    StreamClosed,

    #[error("Receive timeout after {0:?}")]
    Timeout(Duration),

    #[error("WebSocket error: {0}")]
    WebSocket(String),
}

impl From<tungstenite::Error> for ReceiveError {
    fn from(e: tungstenite::Error) -> Self {
        Self::WebSocket(e.to_string())
    }
}

/// Connection supervisor errors
#[derive(thiserror::Error, Debug, Clone)]
pub enum SupervisorError {
    #[error("Max retries exceeded after {attempts} attempts")]
    MaxRetriesExceeded { attempts: u32 },

    #[error("Shutdown requested")]
    Shutdown,

    #[error("Fatal error: {0}")]
    Fatal(String),
}

/// Extension-related errors
#[derive(thiserror::Error, Debug, Clone)]
pub enum ExtensionError {
    #[error("Extension '{name}' failed: {message}")]
    Failed { name: String, message: String },

    #[error("Extension '{name}' initialization failed: {message}")]
    InitFailed { name: String, message: String },
}

/// Disconnect reason
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DisconnectReason {
    /// Normal closure
    Normal,
    /// Connection error
    Error(String),
    /// Receive timeout
    Timeout,
    /// Graceful shutdown requested
    Shutdown,
    /// Server closed connection
    ServerClosed {
        code: Option<u16>,
        reason: Option<String>,
    },
}

impl std::fmt::Display for DisconnectReason {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Normal => write!(f, "normal closure"),
            Self::Error(e) => write!(f, "error: {e}"),
            Self::Timeout => write!(f, "timeout"),
            Self::Shutdown => write!(f, "shutdown"),
            Self::ServerClosed { code, reason } => {
                write!(f, "server closed")?;
                if let Some(c) = code {
                    write!(f, " (code: {c})")?;
                }
                if let Some(r) = reason {
                    write!(f, ": {r}")?;
                }
                Ok(())
            }
        }
    }
}

/// Result type alias for client operations
pub type ClientResult<T> = Result<T, ClientError>;

/// Helper trait for converting errors with context
pub trait ErrorContext<T> {
    /// Converts this result, adding context to any error.
    fn with_context(self, context: impl Into<String>) -> Result<T, ClientError>;
}

impl<T, E: Into<ClientError>> ErrorContext<T> for Result<T, E> {
    fn with_context(self, context: impl Into<String>) -> Result<T, ClientError> {
        self.map_err(|e| ClientError::Context {
            context: context.into(),
            source: Box::new(e.into()),
        })
    }
}