use crate::coding;
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SessionError {
#[error("no error")]
Cancel,
#[error("internal error")]
Internal,
#[error("unauthorized")]
Unauthorized,
#[error("protocol violation")]
ProtocolViolation,
#[error("key-value formatting error")]
KeyValueFormatting,
#[error("goaway timeout")]
GoawayTimeout,
#[error("control message timeout")]
Timeout,
#[error("version negotiation failed")]
Version,
#[error("app code={0}")]
App(u16),
#[error("unknown code={0}")]
Unknown(u32),
}
impl SessionError {
pub fn to_code(&self) -> u32 {
match self {
Self::Cancel => 0x0,
Self::Internal => 0x1,
Self::Unauthorized => 0x2,
Self::ProtocolViolation => 0x3,
Self::KeyValueFormatting => 0x6,
Self::GoawayTimeout => 0x10,
Self::Timeout => 0x11,
Self::Version => 0x15,
Self::App(app) => *app as u32 + 64,
Self::Unknown(code) => *code,
}
}
pub fn from_code(code: u32) -> Self {
match code {
0x0 => Self::Cancel,
0x1 => Self::Internal,
0x2 => Self::Unauthorized,
0x3 => Self::ProtocolViolation,
0x6 => Self::KeyValueFormatting,
0x10 => Self::GoawayTimeout,
0x11 => Self::Timeout,
0x15 => Self::Version,
code @ 64.. => match u16::try_from(code - 64) {
Ok(app) => Self::App(app),
Err(_) => Self::Unknown(code),
},
code => Self::Unknown(code),
}
}
}
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum StreamError {
#[error("session closed: {0}")]
Session(#[from] SessionError),
#[error("internal error")]
Internal,
#[error("cancelled")]
Cancel,
#[error("delivery timeout")]
DeliveryTimeout,
#[error("control timeout")]
ControlTimeout,
#[error("going away")]
GoingAway,
#[error("too far behind")]
TooFarBehind,
#[error("malformed track")]
MalformedTrack,
#[error("not found")]
NotFound,
#[error("unroutable")]
Unroutable,
#[error("old")]
Old,
#[error("evicted")]
Evicted,
#[error("wrong frame size")]
WrongSize,
#[error("frame too large")]
FrameTooLarge,
#[error("group too large")]
GroupTooLarge,
#[error("frame timestamp doesn't match track timescale")]
TimestampMismatch,
#[error("app code={0}")]
App(u16),
#[error("unknown code={0}")]
Unknown(u32),
}
impl StreamError {
pub fn to_code(&self) -> u32 {
match self {
Self::Internal => 0x0,
Self::Cancel => 0x1,
Self::DeliveryTimeout => 0x2,
Self::Session(_) => 0x3,
Self::GoingAway => 0x4,
Self::TooFarBehind => 0x5,
Self::MalformedTrack => 0x12,
Self::ControlTimeout => 0x31,
Self::GroupTooLarge => 0x32,
Self::NotFound => 0x33,
Self::Old => 0x34,
Self::Evicted => 0x35,
Self::Unroutable => 0x36,
Self::WrongSize => 0x37,
Self::FrameTooLarge => 0x38,
Self::TimestampMismatch => 0x39,
Self::App(app) => *app as u32 + 64,
Self::Unknown(code) => *code,
}
}
pub fn from_code(code: u32) -> Self {
match code {
0x0 => Self::Internal,
0x1 => Self::Cancel,
0x2 => Self::DeliveryTimeout,
0x3 => Self::Session(SessionError::Internal),
0x4 => Self::GoingAway,
0x5 => Self::TooFarBehind,
0x12 => Self::MalformedTrack,
0x31 => Self::ControlTimeout,
0x32 => Self::GroupTooLarge,
0x33 => Self::NotFound,
0x34 => Self::Old,
0x35 => Self::Evicted,
0x36 => Self::Unroutable,
0x37 => Self::WrongSize,
0x38 => Self::FrameTooLarge,
0x39 => Self::TimestampMismatch,
code @ 64.. => match u16::try_from(code - 64) {
Ok(app) => Self::App(app),
Err(_) => Self::Unknown(code),
},
code => Self::Unknown(code),
}
}
}
#[derive(thiserror::Error, Debug, Clone)]
#[non_exhaustive]
pub enum Error {
#[error("transport: {0}")]
Transport(String),
#[error(transparent)]
Decode(#[from] coding::DecodeError),
#[error("unsupported versions")]
Version,
#[error("unexpected stream type")]
UnexpectedStream,
#[error(transparent)]
BoundsExceeded(#[from] coding::BoundsExceeded),
#[error("invalid path: {0}")]
InvalidPath(#[from] crate::InvalidPattern),
#[error("duplicate")]
Duplicate,
#[error("cancelled")]
Cancel,
#[error("timeout")]
Timeout,
#[error("old")]
Old,
#[error("app code={0}")]
App(u16),
#[error("not found")]
NotFound,
#[error("unroutable")]
Unroutable,
#[error("wrong frame size")]
WrongSize,
#[error("protocol violation")]
ProtocolViolation,
#[error("unauthorized")]
Unauthorized,
#[error("unexpected message")]
UnexpectedMessage,
#[error("unsupported")]
Unsupported,
#[error(transparent)]
Encode(#[from] coding::EncodeError),
#[error("too many parameters")]
TooManyParameters,
#[error("unknown ALPN: {0}")]
UnknownAlpn(String),
#[error("dropped")]
Dropped,
#[error("closed")]
Closed,
#[error("lagged")]
Lagged,
#[error("frame too large")]
FrameTooLarge,
#[error("group too large")]
GroupTooLarge,
#[error("frame already open")]
FrameOpen,
#[error("frame timestamp doesn't match track timescale")]
TimestampMismatch,
#[error("evicted")]
Evicted,
#[error("going away")]
GoingAway,
#[error("goaway timeout")]
GoawayTimeout,
#[error("malformed track")]
MalformedTrack,
#[error("session closed")]
SessionClosed,
#[error(transparent)]
Session(SessionError),
#[error(transparent)]
Stream(StreamError),
#[error("remote error: code={0}")]
Remote(u32),
}
impl Error {
pub fn session(&self) -> Option<&SessionError> {
match self {
Self::Session(err) => Some(err),
_ => None,
}
}
pub fn stream(&self) -> Option<&StreamError> {
match self {
Self::Stream(err) => Some(err),
_ => None,
}
}
pub fn from_transport(err: impl web_transport_trait::Error) -> Self {
if let Some((code, _reason)) = err.session_error() {
return SessionError::from_code(code).into();
}
if let Some(code) = err.stream_error() {
return StreamError::from_code(code).into();
}
Self::Transport(err.to_string())
}
}
impl From<SessionError> for Error {
fn from(err: SessionError) -> Self {
Self::Session(err)
}
}
impl From<StreamError> for Error {
fn from(err: StreamError) -> Self {
Self::Stream(err)
}
}
impl From<&Error> for SessionError {
fn from(err: &Error) -> Self {
match err {
Error::Session(err) => err.clone(),
Error::Stream(StreamError::App(app)) => Self::App(*app),
Error::Stream(_) => Self::Internal,
Error::Cancel | Error::Closed | Error::GoingAway | Error::SessionClosed => Self::Cancel,
Error::Unauthorized => Self::Unauthorized,
Error::Version | Error::UnknownAlpn(_) => Self::Version,
Error::TooManyParameters => Self::KeyValueFormatting,
Error::GoawayTimeout => Self::GoawayTimeout,
Error::Timeout => Self::Timeout,
Error::ProtocolViolation
| Error::UnexpectedMessage
| Error::UnexpectedStream
| Error::Duplicate
| Error::Decode(_)
| Error::Encode(_)
| Error::WrongSize
| Error::BoundsExceeded(_)
| Error::InvalidPath(_) => Self::ProtocolViolation,
Error::App(app) => Self::App(*app),
Error::Remote(_) => Self::Internal,
_ => Self::Internal,
}
}
}
impl From<&Error> for StreamError {
fn from(err: &Error) -> Self {
match err {
Error::Stream(err) => err.clone(),
Error::Session(SessionError::App(app)) => Self::App(*app),
Error::Session(_) => Self::Internal,
Error::Cancel | Error::Closed => Self::Cancel,
Error::SessionClosed => Self::Session(SessionError::Cancel),
Error::Old => Self::Old,
Error::Evicted => Self::Evicted,
Error::Lagged => Self::TooFarBehind,
Error::NotFound => Self::NotFound,
Error::Unroutable => Self::Unroutable,
Error::WrongSize => Self::WrongSize,
Error::FrameTooLarge => Self::FrameTooLarge,
Error::GroupTooLarge => Self::GroupTooLarge,
Error::TimestampMismatch => Self::TimestampMismatch,
Error::Timeout => Self::DeliveryTimeout,
Error::GoingAway => Self::GoingAway,
Error::Decode(_) | Error::BoundsExceeded(_) | Error::InvalidPath(_) | Error::MalformedTrack => {
Self::MalformedTrack
}
Error::App(app) => Self::App(*app),
Error::Remote(_) => Self::Internal,
Error::UnexpectedStream => Self::Internal,
Error::Unauthorized
| Error::Version
| Error::UnknownAlpn(_)
| Error::TooManyParameters
| Error::GoawayTimeout
| Error::ProtocolViolation
| Error::UnexpectedMessage => Self::Session(SessionError::from(err)),
_ => Self::Internal,
}
}
}
impl web_transport_trait::Error for Error {
fn session_error(&self) -> Option<(u32, String)> {
None
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn session_codes_round_trip() {
let registered = [
SessionError::Cancel,
SessionError::Internal,
SessionError::Unauthorized,
SessionError::ProtocolViolation,
SessionError::KeyValueFormatting,
SessionError::GoawayTimeout,
SessionError::Timeout,
SessionError::Version,
SessionError::App(0),
SessionError::App(404),
];
for err in registered {
assert_eq!(
SessionError::from_code(err.to_code()),
err,
"{err:?} did not round trip"
);
}
assert_eq!(SessionError::Unauthorized.to_code(), 0x2);
assert_eq!(SessionError::GoawayTimeout.to_code(), 0x10);
assert_eq!(SessionError::Version.to_code(), 0x15);
for code in [0x1f, 0x20, 0x21, 0x22, 0x2f] {
assert_eq!(SessionError::from_code(code), SessionError::Unknown(code));
}
assert_eq!(
SessionError::from(&Error::UnexpectedStream),
SessionError::ProtocolViolation
);
assert_eq!(StreamError::from(&Error::UnexpectedStream), StreamError::Internal);
}
#[test]
fn stream_codes_round_trip() {
let registered = [
StreamError::Internal,
StreamError::Cancel,
StreamError::DeliveryTimeout,
StreamError::ControlTimeout,
StreamError::GoingAway,
StreamError::TooFarBehind,
StreamError::MalformedTrack,
StreamError::GroupTooLarge,
StreamError::NotFound,
StreamError::Old,
StreamError::Evicted,
StreamError::Unroutable,
StreamError::WrongSize,
StreamError::FrameTooLarge,
StreamError::TimestampMismatch,
StreamError::App(7),
];
for err in registered {
assert_eq!(StreamError::from_code(err.to_code()), err, "{err:?} did not round trip");
}
for (err, code) in [
(StreamError::ControlTimeout, 0x31),
(StreamError::GroupTooLarge, 0x32),
(StreamError::NotFound, 0x33),
(StreamError::Old, 0x34),
(StreamError::Evicted, 0x35),
(StreamError::Unroutable, 0x36),
(StreamError::WrongSize, 0x37),
(StreamError::FrameTooLarge, 0x38),
(StreamError::TimestampMismatch, 0x39),
] {
assert_eq!(err.to_code(), code, "{err:?} moved off its assigned code");
}
for code in 0x20..0x30 {
assert_eq!(StreamError::from_code(code), StreamError::Unknown(code));
}
assert_eq!(StreamError::Cancel.to_code(), 0x1);
assert_eq!(StreamError::from_code(0x0), StreamError::Internal);
assert_eq!(SessionError::from_code(0x0), SessionError::Cancel);
assert_eq!(StreamError::Session(SessionError::Unauthorized).to_code(), 0x3);
assert_eq!(
StreamError::from_code(0x3),
StreamError::Session(SessionError::Internal)
);
}
#[test]
fn relaying_a_code_does_not_change_registries() {
let relayed = StreamError::from(&Error::from(StreamError::from_code(0x3)));
assert_eq!(relayed.to_code(), 0x3);
for code in [
0x0, 0x1, 0x2, 0x4, 0x5, 0x12, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
] {
let relayed = StreamError::from(&Error::from(StreamError::from_code(code)));
assert_eq!(relayed.to_code(), code, "stream {code:#x} changed across a relay");
}
assert_eq!(
StreamError::from(&Error::from(StreamError::from_code(64 + 7))).to_code(),
64 + 7
);
assert_eq!(
SessionError::from(&Error::from(SessionError::from_code(64 + 7))).to_code(),
64 + 7
);
for code in [0x4, 0x5, 0x1f] {
let crossed = StreamError::from(&Error::from(SessionError::from_code(code)));
assert_eq!(
crossed,
StreamError::Internal,
"session {code:#x} leaked into the stream space"
);
}
}
#[test]
fn from_transport_selects_the_matching_registry() {
#[derive(Debug, thiserror::Error)]
#[error("failed")]
struct Failed {
session: Option<u32>,
stream: Option<u32>,
}
impl web_transport_trait::Error for Failed {
fn session_error(&self) -> Option<(u32, String)> {
self.session.map(|code| (code, "closed".to_string()))
}
fn stream_error(&self) -> Option<u32> {
self.stream
}
}
let session = |code| {
Error::from_transport(Failed {
session: Some(code),
stream: None,
})
};
let stream = |code| {
Error::from_transport(Failed {
session: None,
stream: Some(code),
})
};
assert!(matches!(session(0x2), Error::Session(SessionError::Unauthorized)));
assert_eq!(session(0x2).session(), Some(&SessionError::Unauthorized));
assert!(matches!(session(0x0), Error::Session(SessionError::Cancel)));
assert!(matches!(stream(0x1), Error::Stream(StreamError::Cancel)));
assert_eq!(stream(0x1).stream(), Some(&StreamError::Cancel));
assert!(matches!(stream(0x0), Error::Stream(StreamError::Internal)));
assert!(matches!(stream(0x5), Error::Stream(StreamError::TooFarBehind)));
assert!(matches!(stream(0x32), Error::Stream(StreamError::GroupTooLarge)));
assert!(matches!(stream(0x34), Error::Stream(StreamError::Old)));
assert!(matches!(stream(0x38), Error::Stream(StreamError::FrameTooLarge)));
assert!(matches!(stream(0x31), Error::Stream(StreamError::ControlTimeout)));
assert!(matches!(stream(0x22), Error::Stream(StreamError::Unknown(0x22))));
assert!(matches!(session(0x22), Error::Session(SessionError::Unknown(0x22))));
assert!(matches!(
Error::from_transport(Failed {
session: None,
stream: None
}),
Error::Transport(_)
));
}
}