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 requested path or operation is not granted, either by the peer's token
80	/// or by the scope of the handle it was requested through.
81	#[error("unauthorized")]
82	Unauthorized,
83
84	/// A valid message arrived in a state where it is not allowed.
85	#[error("unexpected message")]
86	UnexpectedMessage,
87
88	/// The peer asked for a feature this endpoint does not implement.
89	#[error("unsupported")]
90	Unsupported,
91
92	/// A message could not be serialized for the negotiated version.
93	#[error(transparent)]
94	Encode(#[from] coding::EncodeError),
95
96	/// A message carried more parameters than this endpoint accepts.
97	#[error("too many parameters")]
98	TooManyParameters,
99
100	/// The peer acted against the [`Role`](crate::Role) it advertised at SETUP.
101	#[error("invalid role")]
102	InvalidRole,
103
104	/// The peer offered an ALPN this endpoint doesn't recognize, so no version could be
105	/// negotiated. A connect-time error.
106	#[error("unknown ALPN: {0}")]
107	UnknownAlpn(String),
108
109	/// The producer was dropped without finishing, so the content is incomplete.
110	#[error("dropped")]
111	Dropped,
112
113	/// The handle was already closed by this side.
114	#[error("closed")]
115	Closed,
116
117	/// The reader fell behind the group's byte budget: the frame it wanted was dropped
118	/// to keep the group under its size limit. Named from the consumer's side (nothing is
119	/// "full"); distinct from [`Self::Evicted`], which drops a whole group under the
120	/// pool's memory pressure.
121	#[error("lagged")]
122	Lagged,
123
124	/// A frame declared a payload size larger than the receiver accepts.
125	#[error("frame too large")]
126	FrameTooLarge,
127
128	/// A frame's timestamp doesn't match its track's negotiated timescale: it's
129	/// missing on a timed track, present on an untimed track, or carries a
130	/// different scale than the track advertised.
131	#[error("frame timestamp doesn't match track timescale")]
132	TimestampMismatch,
133
134	/// The group was evicted by its own track to pay eviction debt under memory
135	/// pressure (see [`cache::Pool`](crate::cache::Pool)). Unlike [`Self::Old`],
136	/// the group was still within the publisher's window; it can be re-fetched.
137	#[error("evicted")]
138	Evicted,
139
140	/// A remote error received via a stream/session reset code.
141	#[error("remote error: code={0}")]
142	Remote(u32),
143	/// A whole-frame write was refused because a frame is already open on the group.
144	///
145	/// A [`crate::group::Producer`] streaming a frame with `create_frame` blocks the
146	/// whole-frame writes on every clone of that producer until it finishes, since
147	/// appending around it would reorder the group.
148	#[error("frame already open")]
149	FrameOpen,
150}
151
152impl Error {
153	/// An integer code that is sent over the wire.
154	pub fn to_code(&self) -> u32 {
155		match self {
156			Self::Cancel => 0,
157			Self::RequiredExtension => 1,
158			Self::Old => 2,
159			Self::Timeout => 3,
160			Self::Transport(_) => 4,
161			Self::Decode(_) => 5,
162			Self::Unauthorized => 6,
163			Self::Version => 9,
164			Self::UnexpectedStream => 10,
165			Self::BoundsExceeded(_) => 11,
166			Self::Duplicate => 12,
167			Self::NotFound => 13,
168			Self::WrongSize => 14,
169			Self::ProtocolViolation => 15,
170			Self::UnexpectedMessage => 16,
171			Self::Unsupported => 17,
172			Self::Encode(_) => 18,
173			Self::TooManyParameters => 19,
174			Self::InvalidRole => 20,
175			Self::UnknownAlpn(_) => 21,
176			Self::Dropped => 24,
177			Self::Closed => 25,
178			Self::Lagged => 26,
179			Self::FrameTooLarge => 27,
180			// 28 is reserved (was per-frame decompression, removed in draft-05).
181			Self::TimestampMismatch => 29,
182			Self::Unroutable => 30,
183			Self::Evicted => 31,
184			// 22 was unused in the 0-31 library range.
185			Self::FrameOpen => 22,
186			Self::App(app) => *app as u32 + 64,
187			Self::Remote(code) => *code,
188		}
189	}
190
191	/// Convert a transport error into an [Error], decoding stream reset codes.
192	pub fn from_transport(err: impl web_transport_trait::Error) -> Self {
193		match err.stream_error() {
194			// Code 0 is what [`Self::Cancel`] encodes to, and what a plain stream
195			// drop sends: the peer is done with the stream, not failing. Decoding it
196			// back keeps a routine unsubscribe out of the error paths.
197			Some(0) => return Self::Cancel,
198			Some(code) => return Self::Remote(code),
199			None => {}
200		}
201
202		Self::Transport(err.to_string())
203	}
204}
205
206impl web_transport_trait::Error for Error {
207	fn session_error(&self) -> Option<(u32, String)> {
208		None
209	}
210}
211
212/// A [`Result`](std::result::Result) with this crate's [`Error`].
213pub type Result<T> = std::result::Result<T, Error>;
214
215#[cfg(test)]
216mod tests {
217	use super::*;
218
219	// The wire codes are a stable contract with every other implementation, so a variant
220	// rename (e.g. CacheFull -> Lagged) must not shift them. Pin the load-bearing ones.
221	#[test]
222	fn to_code_is_stable() {
223		assert_eq!(Error::Cancel.to_code(), 0);
224		assert_eq!(Error::Version.to_code(), 9);
225		assert_eq!(Error::UnknownAlpn(String::new()).to_code(), 21);
226		assert_eq!(Error::Lagged.to_code(), 26);
227		assert_eq!(Error::Evicted.to_code(), 31);
228		// App codes sit past the reserved library range; Remote is the raw received code.
229		assert_eq!(Error::App(0).to_code(), 64);
230		assert_eq!(Error::App(404).to_code(), 468);
231		assert_eq!(Error::Remote(468).to_code(), 468);
232	}
233}