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}