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}