use std::fmt;
use std::time::Duration;
use tokio_tungstenite::tungstenite;
#[derive(Debug)]
pub enum DisconnectReason {
CleanClose,
GoingAway { reason: String },
ProtocolRejection { code: u16, reason: String },
TransportError {
source: Box<dyn std::error::Error + Send + Sync>,
},
StaleConnection { silence_duration: Duration },
ReceiverDropped,
HeartbeatFailed {
source: Box<dyn std::error::Error + Send + Sync>,
},
}
impl DisconnectReason {
pub fn is_retryable(&self) -> bool {
!matches!(self, Self::ProtocolRejection { .. } | Self::ReceiverDropped)
}
pub fn suggested_delay_factor(&self) -> f64 {
match self {
Self::CleanClose => 0.0,
Self::GoingAway { .. } => 1.0,
Self::ProtocolRejection { .. } => 0.0, Self::TransportError { .. } => 2.0,
Self::StaleConnection { .. } => 1.0,
Self::ReceiverDropped => 0.0, Self::HeartbeatFailed { .. } => 1.0,
}
}
}
impl fmt::Display for DisconnectReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CleanClose => write!(f, "server closed normally (1000)"),
Self::GoingAway { reason } => write!(f, "server going away (1001): {reason}"),
Self::ProtocolRejection { code, reason } => {
write!(f, "protocol rejection ({code}): {reason}")
}
Self::TransportError { source } => write!(f, "transport error: {source}"),
Self::StaleConnection {
silence_duration: d,
} => {
write!(f, "stale connection (no data for {d:?})")
}
Self::ReceiverDropped => write!(f, "receiver dropped (intentional shutdown)"),
Self::HeartbeatFailed { source } => {
write!(f, "heartbeat write failed: {source}")
}
}
}
}
pub fn classify_close_frame(
frame: Option<tokio_tungstenite::tungstenite::protocol::CloseFrame<'static>>,
) -> DisconnectReason {
let Some(frame) = frame else {
return DisconnectReason::TransportError {
source: "server closed without a close frame".into(),
};
};
let code = frame.code;
let reason = frame.reason.to_string();
use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
match code {
CloseCode::Normal => DisconnectReason::CleanClose,
CloseCode::Away => DisconnectReason::GoingAway { reason },
CloseCode::Protocol => {
if reason.contains("reset without closing")
|| reason.contains("Connection reset")
{
DisconnectReason::TransportError {
source: format!("server closed with code 1002: {reason}").into(),
}
} else {
DisconnectReason::ProtocolRejection {
code: code.into(),
reason,
}
}
}
CloseCode::Unsupported | CloseCode::Policy => {
DisconnectReason::ProtocolRejection {
code: code.into(),
reason,
}
}
_ => DisconnectReason::TransportError {
source: format!("server closed with code {}: {}", u16::from(code), reason)
.into(),
},
}
}
pub fn classify_tungstenite_error(
error: tokio_tungstenite::tungstenite::Error,
) -> DisconnectReason {
use tokio_tungstenite::tungstenite::Error as TError;
if let TError::Protocol(ref msg) = error {
let msg_str = msg.to_string();
if msg_str.contains("reset without closing")
|| msg_str.contains("Connection reset")
{
return DisconnectReason::TransportError {
source: Box::new(error),
};
}
return DisconnectReason::ProtocolRejection {
code: 1002,
reason: msg_str,
};
}
DisconnectReason::TransportError {
source: Box::new(error),
}
}
#[derive(Debug)]
pub enum WssExitReason {
StreamEnded,
ReceiverDropped,
ServerClose(Option<tungstenite::protocol::CloseFrame<'static>>),
Transport(tungstenite::Error),
HeartbeatWriteFailed(tungstenite::Error),
ConnectionFailed(String),
}
impl fmt::Display for WssExitReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::StreamEnded => write!(f, "stream ended"),
Self::ReceiverDropped => write!(f, "receiver dropped"),
Self::ServerClose(frame) => match frame {
Some(cf) => {
write!(f, "server close ({}): {}", u16::from(cf.code), cf.reason)
}
None => write!(f, "server close (no frame)"),
},
Self::Transport(e) => write!(f, "transport error: {e}"),
Self::HeartbeatWriteFailed(e) => write!(f, "heartbeat write failed: {e}"),
Self::ConnectionFailed(msg) => write!(f, "connection failed: {msg}"),
}
}
}
impl From<WssExitReason> for DisconnectReason {
fn from(reason: WssExitReason) -> Self {
match reason {
WssExitReason::StreamEnded => classify_close_frame(None),
WssExitReason::ReceiverDropped => DisconnectReason::ReceiverDropped,
WssExitReason::ServerClose(frame) => classify_close_frame(frame),
WssExitReason::Transport(e) => classify_tungstenite_error(e),
WssExitReason::HeartbeatWriteFailed(e) => DisconnectReason::HeartbeatFailed {
source: Box::new(e),
},
WssExitReason::ConnectionFailed(msg) => {
DisconnectReason::TransportError { source: msg.into() }
}
}
}
}