use crate::engine::EngineAction;
use crate::packet::{Frame, Handshake, Packet};
use bytestring::ByteString;
use miette::Diagnostic;
use thiserror::Error;
use tokio::sync::{mpsc, oneshot};
use tokio::time::error::Elapsed;
#[derive(Debug, Error, Diagnostic)]
#[error(transparent)]
#[diagnostic(transparent)]
pub enum Error {
Transport(#[from] TransportError),
Engine(#[from] EngineError),
}
#[derive(Debug, Error, Diagnostic)]
pub enum EngineError {
#[error("outbound frame channel closed")]
#[diagnostic(
code(eioc::engine::send_transport),
help("the transport task exited; check for prior transport errors")
)]
SendTransport(#[from] mpsc::error::SendError<Frame>),
#[error("inbound message sink closed")]
#[diagnostic(
code(eioc::engine::send_sink),
help("the consumer of inbound messages was dropped")
)]
SendSink(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("failed to receive Engine.IO handshake")]
#[diagnostic(
code(eioc::engine::recv_handshake),
help(
"the transport task exited before completing the handshake; check the transport for prior errors"
)
)]
RecvHandshake(#[from] oneshot::error::RecvError),
#[error("heartbeat timeout")]
#[diagnostic(
code(eioc::engine::heartbeat_timeout),
help("the server stopped responding; check the network or server load")
)]
HeartbeatTimeout(#[from] Elapsed),
#[error("unexpected packet {0:?}")]
#[diagnostic(
code(eioc::engine::server),
help(
"the server sent a packet that violates the Engine.IO state machine; likely a server bug or version mismatch"
)
)]
Server(Packet),
}
#[derive(Debug, Error, Diagnostic)]
pub enum TransportError {
#[error(transparent)]
#[diagnostic(transparent)]
WebSocket(#[from] WebSocketError),
#[error(transparent)]
#[diagnostic(transparent)]
Polling(#[from] PollingError),
#[error("engine action channel closed")]
#[diagnostic(
code(eioc::transport::send_engine),
help("the engine task exited; check for prior engine errors")
)]
SendEngine(#[from] mpsc::error::SendError<EngineAction>),
#[error("failed to send handshake to engine task")]
#[diagnostic(
code(eioc::transport::send_handshake),
help(
"the engine task exited before the handshake arrived; check the engine for prior errors"
)
)]
SendHandshake(Handshake),
#[error("expected Open packet as first frame")]
#[diagnostic(
code(eioc::transport::open),
help(
"the server did not send the expected Open packet; check the server implementation and Engine.IO version"
)
)]
Open(Frame),
}
#[derive(Debug, Error, Diagnostic)]
pub enum WebSocketError {
#[error(transparent)]
#[diagnostic(code(eioc::transport::websocket::tungstenite))]
Tungstenite(#[from] tokio_tungstenite::tungstenite::Error),
#[error("WebSocket stream closed unexpectedly")]
#[diagnostic(
code(eioc::transport::websocket::closed),
help("the server closed the TCP connection without sending a WebSocket close frame")
)]
Closed,
#[error("probe Pong not received")]
#[diagnostic(
code(eioc::transport::websocket::probe),
help(
"the server did not respond to the probe Ping with a Pong; check the server implementation and Engine.IO version"
)
)]
Probe(Frame),
#[error(transparent)]
#[diagnostic(transparent)]
Packet(#[from] PacketError),
}
#[derive(Debug, Error, Diagnostic)]
pub enum PollingError {
#[error(transparent)]
#[diagnostic(code(eioc::transport::polling::reqwest))]
Reqwest(#[from] reqwest::Error),
#[error("failed to decode base64 attachment")]
#[diagnostic(code(eioc::transport::polling::base64))]
Base64(#[from] base64::DecodeError),
#[error("unexpected polling response: {0}")]
#[diagnostic(
code(eioc::transport::polling::response),
help(
"the server returned an unexpected body; verify the server implementation and Engine.IO version"
)
)]
Response(String),
#[error(transparent)]
#[diagnostic(transparent)]
Packet(#[from] PacketError),
}
#[derive(Debug, Error, Diagnostic)]
#[allow(missing_docs)]
pub enum PacketError {
#[error("empty packet")]
#[diagnostic(code(eioc::packet::empty))]
Empty,
#[error("invalid type id {id}")]
#[diagnostic(code(eioc::packet::invalid_id))]
InvalidId { id: char },
#[error("failed to parse Open payload")]
#[diagnostic(code(eioc::packet::handshake))]
Handshake(#[from] serde_json::Error),
#[error("unexpected payload for type id {id}")]
#[diagnostic(code(eioc::packet::payload))]
Payload { id: char, payload: ByteString },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn packet_error_empty_display() {
assert_eq!(PacketError::Empty.to_string(), "empty packet");
}
#[test]
fn packet_error_invalid_id_display() {
assert_eq!(
PacketError::InvalidId { id: '9' }.to_string(),
"invalid type id 9"
);
}
#[test]
fn packet_error_payload_display() {
let e = PacketError::Payload {
id: '1',
payload: "extra".into(),
};
assert_eq!(e.to_string(), "unexpected payload for type id 1");
}
#[test]
fn engine_error_server_display() {
let e = EngineError::Server(crate::packet::Packet::Upgrade);
assert_eq!(e.to_string(), "unexpected packet Upgrade");
}
#[test]
fn polling_error_response_display() {
let e = PollingError::Response("forbidden".to_string());
assert_eq!(e.to_string(), "unexpected polling response: forbidden");
}
#[test]
fn transport_error_from_polling() {
let e: TransportError = PollingError::Response("x".into()).into();
assert!(matches!(e, TransportError::Polling(_)));
}
#[test]
fn error_from_engine_error() {
let e: Error = EngineError::Server(crate::packet::Packet::Close).into();
assert!(matches!(e, Error::Engine(_)));
}
#[test]
fn error_from_transport_error() {
let e: Error =
TransportError::Open(crate::packet::Frame::Packet(crate::packet::Packet::Close)).into();
assert!(matches!(e, Error::Transport(_)));
}
}