Skip to main content

moq_net/
error.rs

1use crate::coding;
2
3/// A list of possible errors that can occur during the session.
4#[derive(thiserror::Error, Debug, Clone)]
5#[non_exhaustive]
6pub enum Error {
7	/// The underlying QUIC/WebTransport connection failed; carries the backend's message.
8	#[error("transport: {0}")]
9	Transport(String),
10
11	/// A message off the wire could not be parsed.
12	#[error(transparent)]
13	Decode(#[from] coding::DecodeError),
14
15	/// Version negotiation failed, or the negotiated version lacks a requested feature
16	/// (e.g. a FETCH against a version without fetch support). Mostly a connect-time
17	/// error, but the feature-gap case can surface mid-session, so it can't simply move
18	/// to a connect-only error type.
19	#[error("unsupported versions")]
20	Version,
21
22	/// A required extension was not present
23	#[error("extension required")]
24	RequiredExtension,
25
26	/// An unexpected stream type was received
27	#[error("unexpected stream type")]
28	UnexpectedStream,
29
30	/// An integer was too large for the QUIC varint range.
31	#[error(transparent)]
32	BoundsExceeded(#[from] coding::BoundsExceeded),
33
34	/// A duplicate ID was used
35	// The broadcast/track is a duplicate
36	#[error("duplicate")]
37	Duplicate,
38
39	/// Nobody is reading any more, so the producer stopped. Not a failure.
40	// Cancel is returned when there are no more readers.
41	#[error("cancelled")]
42	Cancel,
43
44	/// It took too long to open or transmit a stream.
45	#[error("timeout")]
46	Timeout,
47
48	/// The group is older than the latest group and dropped.
49	#[error("old")]
50	Old,
51
52	/// An application-chosen close code. Bounded to `u16` and offset past the library's
53	/// reserved range (`+ 64`) on the wire by [`Self::to_code`], so app codes never
54	/// collide with protocol ones.
55	///
56	/// The width asymmetry with [`Self::Remote`] is deliberate: `App` is a code *this*
57	/// side chooses to send, while `Remote` carries a raw code *received* off the wire
58	/// that didn't map to a known variant, which can be any `u32`.
59	#[error("app code={0}")]
60	App(u16),
61
62	/// The requested broadcast or track does not exist at the peer.
63	#[error("not found")]
64	NotFound,
65
66	/// A broadcast was requested that is neither announced nor served by a dynamic
67	/// router, so there is no route to it.
68	#[error("unroutable")]
69	Unroutable,
70
71	/// A frame's payload length disagreed with its declared size.
72	#[error("wrong frame size")]
73	WrongSize,
74
75	/// The peer broke a protocol rule; the session is unusable.
76	#[error("protocol violation")]
77	ProtocolViolation,
78
79	/// The peer's token does not grant the requested path or operation.
80	#[error("unauthorized")]
81	Unauthorized,
82
83	/// A valid message arrived in a state where it is not allowed.
84	#[error("unexpected message")]
85	UnexpectedMessage,
86
87	/// The peer asked for a feature this endpoint does not implement.
88	#[error("unsupported")]
89	Unsupported,
90
91	/// A message could not be serialized for the negotiated version.
92	#[error(transparent)]
93	Encode(#[from] coding::EncodeError),
94
95	/// A message carried more parameters than this endpoint accepts.
96	#[error("too many parameters")]
97	TooManyParameters,
98
99	/// The peer acted against the [`Role`](crate::Role) it advertised at SETUP.
100	#[error("invalid role")]
101	InvalidRole,
102
103	/// The peer offered an ALPN this endpoint doesn't recognize, so no version could be
104	/// negotiated. A connect-time error.
105	#[error("unknown ALPN: {0}")]
106	UnknownAlpn(String),
107
108	/// The producer was dropped without finishing, so the content is incomplete.
109	#[error("dropped")]
110	Dropped,
111
112	/// The handle was already closed by this side.
113	#[error("closed")]
114	Closed,
115
116	/// The reader fell behind the group's byte budget: the frame it wanted was dropped
117	/// to keep the group under its size limit. Named from the consumer's side (nothing is
118	/// "full"); distinct from [`Self::Evicted`], which drops a whole group under the
119	/// pool's memory pressure.
120	#[error("lagged")]
121	Lagged,
122
123	/// A frame declared a payload size larger than the receiver accepts.
124	#[error("frame too large")]
125	FrameTooLarge,
126
127	/// A frame's timestamp doesn't match its track's negotiated timescale: it's
128	/// missing on a timed track, present on an untimed track, or carries a
129	/// different scale than the track advertised.
130	#[error("frame timestamp doesn't match track timescale")]
131	TimestampMismatch,
132
133	/// The group was evicted from the cache under memory pressure (see
134	/// [`cache::Pool`](crate::cache::Pool)). Unlike [`Self::Old`], the group was
135	/// still within the publisher's window; it can be re-fetched.
136	#[error("evicted")]
137	Evicted,
138
139	/// A remote error received via a stream/session reset code.
140	#[error("remote error: code={0}")]
141	Remote(u32),
142}
143
144impl Error {
145	/// An integer code that is sent over the wire.
146	pub fn to_code(&self) -> u32 {
147		match self {
148			Self::Cancel => 0,
149			Self::RequiredExtension => 1,
150			Self::Old => 2,
151			Self::Timeout => 3,
152			Self::Transport(_) => 4,
153			Self::Decode(_) => 5,
154			Self::Unauthorized => 6,
155			Self::Version => 9,
156			Self::UnexpectedStream => 10,
157			Self::BoundsExceeded(_) => 11,
158			Self::Duplicate => 12,
159			Self::NotFound => 13,
160			Self::WrongSize => 14,
161			Self::ProtocolViolation => 15,
162			Self::UnexpectedMessage => 16,
163			Self::Unsupported => 17,
164			Self::Encode(_) => 18,
165			Self::TooManyParameters => 19,
166			Self::InvalidRole => 20,
167			Self::UnknownAlpn(_) => 21,
168			Self::Dropped => 24,
169			Self::Closed => 25,
170			Self::Lagged => 26,
171			Self::FrameTooLarge => 27,
172			// 28 is reserved (was per-frame decompression, removed in draft-05).
173			Self::TimestampMismatch => 29,
174			Self::Unroutable => 30,
175			Self::Evicted => 31,
176			Self::App(app) => *app as u32 + 64,
177			Self::Remote(code) => *code,
178		}
179	}
180
181	/// Convert a transport error into an [Error], decoding stream reset codes.
182	pub fn from_transport(err: impl web_transport_trait::Error) -> Self {
183		if let Some(code) = err.stream_error() {
184			return Self::Remote(code);
185		}
186
187		Self::Transport(err.to_string())
188	}
189}
190
191impl web_transport_trait::Error for Error {
192	fn session_error(&self) -> Option<(u32, String)> {
193		None
194	}
195}
196
197/// A [`Result`](std::result::Result) with this crate's [`Error`].
198pub type Result<T> = std::result::Result<T, Error>;
199
200#[cfg(test)]
201mod tests {
202	use super::*;
203
204	// The wire codes are a stable contract with every other implementation, so a variant
205	// rename (e.g. CacheFull -> Lagged) must not shift them. Pin the load-bearing ones.
206	#[test]
207	fn to_code_is_stable() {
208		assert_eq!(Error::Cancel.to_code(), 0);
209		assert_eq!(Error::Version.to_code(), 9);
210		assert_eq!(Error::UnknownAlpn(String::new()).to_code(), 21);
211		assert_eq!(Error::Lagged.to_code(), 26);
212		assert_eq!(Error::Evicted.to_code(), 31);
213		// App codes sit past the reserved library range; Remote is the raw received code.
214		assert_eq!(Error::App(0).to_code(), 64);
215		assert_eq!(Error::App(404).to_code(), 468);
216		assert_eq!(Error::Remote(468).to_code(), 468);
217	}
218}