use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use crate::protocol::SUBPROTOCOL_UNSUPPORTED;
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("client closed")]
Closed,
#[error("not connected")]
NotConnected,
#[error("already connected")]
AlreadyConnected,
#[error("no protocols to offer")]
NoProtocols,
#[error("no transport: set one with ClientBuilder::transport or enable the websocket feature")]
NoTransport,
#[error("subscription rejected: {key}")]
Rejected {
key: String,
},
#[error("unsubscribed")]
Unsubscribed,
#[error(
"unsupported subprotocol: {}",
describe_subprotocol(negotiated, offered)
)]
UnsupportedSubprotocol {
negotiated: String,
offered: Vec<String>,
},
#[error("server disconnected{}", describe_reason(reason.as_ref()))]
Disconnected {
reason: Option<DisconnectReason>,
reconnect: bool,
},
#[error("gave up connecting after {attempts} attempts: {last}")]
GaveUp {
attempts: u32,
#[source]
last: Box<Error>,
},
#[error("no frame in {after:?}")]
Stale {
after: Duration,
},
#[error("server refused the upgrade with {status}")]
Handshake {
status: http::StatusCode,
},
#[error("server closed the connection: {}", describe_close(*code, reason))]
Close {
code: u16,
reason: String,
},
#[error("message of {size} bytes exceeds the maximum of {limit}")]
MessageTooBig {
size: usize,
limit: usize,
},
#[error("transport: {0}")]
Transport(#[source] Arc<dyn std::error::Error + Send + Sync>),
#[error("building headers: {0}")]
Headers(#[source] Arc<dyn std::error::Error + Send + Sync>),
#[error("invalid header {name}: {source}")]
InvalidHeader {
name: String,
#[source]
source: Arc<http::Error>,
},
#[error("data for {action:?} must encode to a JSON object")]
DataNotAnObject {
action: String,
},
#[error("json: {0}")]
Json(#[source] Arc<serde_json::Error>),
}
impl Error {
pub fn transport(source: impl std::error::Error + Send + Sync + 'static) -> Error {
Error::Transport(Arc::new(source))
}
pub fn headers(source: impl std::error::Error + Send + Sync + 'static) -> Error {
Error::Headers(Arc::new(source))
}
pub fn cause(&self) -> Option<&(dyn std::error::Error + Send + Sync + 'static)> {
match self {
Error::Transport(cause) | Error::Headers(cause) => Some(&**cause),
Error::GaveUp { last, .. } => last.cause(),
_ => None,
}
}
}
impl From<serde_json::Error> for Error {
fn from(source: serde_json::Error) -> Error {
Error::Json(Arc::new(source))
}
}
fn describe_subprotocol(negotiated: &str, offered: &[String]) -> String {
if negotiated == SUBPROTOCOL_UNSUPPORTED {
format!("the server speaks none of {}", offered.join(", "))
} else {
format!("{negotiated:?}")
}
}
fn describe_reason(reason: Option<&DisconnectReason>) -> String {
match reason {
Some(reason) => format!(": {reason}"),
None => String::new(),
}
}
fn describe_close(code: u16, reason: &str) -> String {
if reason.is_empty() {
code.to_string()
} else {
format!("{code} {reason}")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DisconnectReason {
Unauthorized,
InvalidRequest,
ServerRestart,
Remote,
Other(String),
}
impl DisconnectReason {
pub fn parse(reason: &str) -> DisconnectReason {
match reason {
"unauthorized" => DisconnectReason::Unauthorized,
"invalid_request" => DisconnectReason::InvalidRequest,
"server_restart" => DisconnectReason::ServerRestart,
"remote" => DisconnectReason::Remote,
other => DisconnectReason::Other(other.to_string()),
}
}
pub fn as_str(&self) -> &str {
match self {
DisconnectReason::Unauthorized => "unauthorized",
DisconnectReason::InvalidRequest => "invalid_request",
DisconnectReason::ServerRestart => "server_restart",
DisconnectReason::Remote => "remote",
DisconnectReason::Other(other) => other,
}
}
}
impl fmt::Display for DisconnectReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "a test that can't have what it asked for has nothing left to assert"
)]
mod tests {
use std::error::Error as _;
use std::io;
use super::*;
#[test]
fn disconnect_reasons_round_trip() {
for (wire, reason) in [
("unauthorized", DisconnectReason::Unauthorized),
("invalid_request", DisconnectReason::InvalidRequest),
("server_restart", DisconnectReason::ServerRestart),
("remote", DisconnectReason::Remote),
("moved", DisconnectReason::Other("moved".to_string())),
] {
assert_eq!(reason, DisconnectReason::parse(wire));
assert_eq!(wire, DisconnectReason::parse(wire).to_string());
}
}
#[test]
fn a_disconnect_names_its_reason_when_the_server_gave_one() {
assert_eq!(
"server disconnected: unauthorized",
Error::Disconnected {
reason: Some(DisconnectReason::Unauthorized),
reconnect: false,
}
.to_string()
);
assert_eq!(
"server disconnected",
Error::Disconnected {
reason: None,
reconnect: false,
}
.to_string()
);
}
#[test]
fn unsupported_subprotocol_describes_what_was_offered() {
let sentinel = Error::UnsupportedSubprotocol {
negotiated: SUBPROTOCOL_UNSUPPORTED.to_string(),
offered: vec!["actioncable-v1-json".to_string(), "v2".to_string()],
};
let unknown = Error::UnsupportedSubprotocol {
negotiated: "actioncable-v9-telepathy".to_string(),
offered: vec!["actioncable-v1-json".to_string()],
};
assert_eq!(
"unsupported subprotocol: the server speaks none of actioncable-v1-json, v2",
sentinel.to_string()
);
assert_eq!(
"unsupported subprotocol: \"actioncable-v9-telepathy\"",
unknown.to_string()
);
}
#[test]
fn a_close_reads_as_the_frame_the_server_sent() {
assert_eq!(
"server closed the connection: 4401 unauthorized",
Error::Close {
code: 4401,
reason: "unauthorized".to_string(),
}
.to_string()
);
assert_eq!(
"server closed the connection: 1005",
Error::Close {
code: 1005,
reason: String::new(),
}
.to_string()
);
}
#[test]
fn a_handshake_failure_reads_as_the_status_line() {
let refused = Error::Handshake {
status: http::StatusCode::NOT_FOUND,
};
assert_eq!(
"server refused the upgrade with 404 Not Found",
refused.to_string()
);
}
#[test]
fn giving_up_keeps_what_the_last_attempt_failed_on() {
let gave_up = Error::GaveUp {
attempts: 2,
last: Box::new(Error::transport(io::Error::other("connection refused"))),
};
assert_eq!(
"gave up connecting after 2 attempts: transport: connection refused",
gave_up.to_string()
);
assert_eq!(
"connection refused",
gave_up
.cause()
.expect("the last attempt's own error")
.to_string()
);
assert!(gave_up.source().is_some(), "the chain still prints");
}
#[test]
fn a_wrapped_error_can_be_recognized_by_its_own_type() {
let signed_out = Error::headers(io::Error::other("sign in again"));
let cause = signed_out
.cause()
.and_then(<dyn std::error::Error + Send + Sync>::downcast_ref::<io::Error>)
.expect("the provider's own error");
assert_eq!("sign in again", cause.to_string());
assert_eq!(None, Error::Closed.cause().map(ToString::to_string));
}
}