moqtap_client/transport/mod.rs
1//! Transport abstraction for QUIC and WebTransport.
2//!
3//! Uses enum dispatch (not trait objects) since the transport set is closed.
4//! WebTransport support is behind the `webtransport` feature flag. Nothing
5//! here is per-draft; every draft's connection is carried over the same two.
6
7pub mod quic;
8#[cfg(feature = "webtransport")]
9pub mod webtransport;
10
11pub use quic::{dial_quic, DialError, QuicDialOptions};
12
13use std::future::Future;
14
15use bytes::Bytes;
16
17/// Errors from the transport layer.
18#[derive(Debug, thiserror::Error)]
19pub enum TransportError {
20 /// Connection-level error (e.g., peer closed, timeout).
21 #[error("connection error: {0}")]
22 Connection(String),
23 /// Error writing to a stream.
24 #[error("write error: {0}")]
25 Write(String),
26 /// Error reading from a stream.
27 #[error("read error: {0}")]
28 Read(String),
29 /// Stream was closed.
30 #[error("stream closed")]
31 StreamClosed,
32 /// Error sending a datagram.
33 #[error("send datagram error: {0}")]
34 SendDatagram(String),
35 /// Connection was lost.
36 #[error("connection lost")]
37 ConnectionLost,
38 /// Error during connection establishment.
39 #[error("connect error: {0}")]
40 Connect(String),
41 /// The peer abandoned transmission by resetting the stream. Carries
42 /// the peer's application error code so a forwarder can mirror it
43 /// verbatim with [`SendStream::reset`].
44 #[error("stream reset by peer: code {0}")]
45 StreamReset(u64),
46 /// The peer is no longer accepting data on this stream
47 /// (`STOP_SENDING`). Carries the peer's application error code so a
48 /// forwarder can mirror it verbatim with [`RecvStream::stop`].
49 #[error("stream stopped by peer: code {0}")]
50 Stopped(u64),
51}
52
53/// A transport-agnostic connection (QUIC or WebTransport).
54pub enum Transport {
55 /// Raw QUIC via quinn.
56 Quic(quic::QuicTransport),
57 /// WebTransport via h3 + h3-quinn.
58 #[cfg(feature = "webtransport")]
59 WebTransport(webtransport::WebTransportTransport),
60}
61
62impl Transport {
63 /// Open a bidirectional stream.
64 pub async fn open_bi(&self) -> Result<(SendStream, RecvStream), TransportError> {
65 match self {
66 Transport::Quic(t) => t.open_bi().await,
67 #[cfg(feature = "webtransport")]
68 Transport::WebTransport(t) => t.open_bi().await,
69 }
70 }
71
72 /// Accept an incoming bidirectional stream.
73 pub async fn accept_bi(&self) -> Result<(SendStream, RecvStream), TransportError> {
74 match self {
75 Transport::Quic(t) => t.accept_bi().await,
76 #[cfg(feature = "webtransport")]
77 Transport::WebTransport(t) => t.accept_bi().await,
78 }
79 }
80
81 /// Open a unidirectional send stream.
82 pub async fn open_uni(&self) -> Result<SendStream, TransportError> {
83 match self {
84 Transport::Quic(t) => t.open_uni().await,
85 #[cfg(feature = "webtransport")]
86 Transport::WebTransport(t) => t.open_uni().await,
87 }
88 }
89
90 /// Accept an incoming unidirectional stream.
91 pub async fn accept_uni(&self) -> Result<RecvStream, TransportError> {
92 match self {
93 Transport::Quic(t) => t.accept_uni().await,
94 #[cfg(feature = "webtransport")]
95 Transport::WebTransport(t) => t.accept_uni().await,
96 }
97 }
98
99 /// Send a datagram.
100 pub fn send_datagram(&self, data: Bytes) -> Result<(), TransportError> {
101 match self {
102 Transport::Quic(t) => t.send_datagram(data),
103 #[cfg(feature = "webtransport")]
104 Transport::WebTransport(t) => t.send_datagram(data),
105 }
106 }
107
108 /// Receive a datagram.
109 pub async fn recv_datagram(&self) -> Result<Bytes, TransportError> {
110 match self {
111 Transport::Quic(t) => t.recv_datagram().await,
112 #[cfg(feature = "webtransport")]
113 Transport::WebTransport(t) => t.recv_datagram().await,
114 }
115 }
116
117 /// Close the connection.
118 pub fn close(&self, code: u32, reason: &[u8]) {
119 match self {
120 Transport::Quic(t) => t.close(code, reason),
121 #[cfg(feature = "webtransport")]
122 Transport::WebTransport(t) => t.close(code, reason),
123 }
124 }
125}
126
127/// A transport-agnostic send stream.
128pub enum SendStream {
129 /// Raw QUIC send stream.
130 Quic(quinn::SendStream),
131 /// WebTransport send stream.
132 #[cfg(feature = "webtransport")]
133 WebTransport(webtransport::WtSendStream),
134}
135
136impl SendStream {
137 /// Get the QUIC stream ID (transport-level identifier).
138 pub fn stream_id(&self) -> u64 {
139 match self {
140 SendStream::Quic(s) => s.id().index(),
141 #[cfg(feature = "webtransport")]
142 SendStream::WebTransport(_) => 0, // WebTransport doesn't expose stream IDs
143 }
144 }
145
146 /// Write all bytes to the stream.
147 ///
148 /// Fails with [`TransportError::Stopped`] carrying the peer's
149 /// application error code if the peer sent `STOP_SENDING`.
150 pub async fn write_all(&mut self, buf: &[u8]) -> Result<(), TransportError> {
151 match self {
152 SendStream::Quic(s) => s.write_all(buf).await.map_err(TransportError::from),
153 #[cfg(feature = "webtransport")]
154 SendStream::WebTransport(s) => s.write_all(buf).await,
155 }
156 }
157
158 /// Finish the stream (send FIN).
159 pub fn finish(&mut self) -> Result<(), TransportError> {
160 match self {
161 SendStream::Quic(s) => {
162 s.finish().map_err(|_| TransportError::StreamClosed)?;
163 Ok(())
164 }
165 #[cfg(feature = "webtransport")]
166 SendStream::WebTransport(s) => s.finish(),
167 }
168 }
169
170 /// Reset the stream, telling the peer transmission was abandoned and
171 /// handing it `code` as the `RESET_STREAM` application error code.
172 ///
173 /// This is the only way to abandon a send stream truthfully: simply
174 /// dropping a `SendStream` sends a FIN instead, which tells the peer
175 /// the stream ended *cleanly*. A forwarder that saw the far side
176 /// reset must call this with the code it received, so a truncated
177 /// stream is never laundered into a complete one.
178 ///
179 /// # Errors
180 /// - [`TransportError::StreamClosed`] if the stream was already finished or
181 /// reset.
182 /// - [`TransportError::Write`] if `code` is outside the QUIC varint range
183 /// (`0..2^62`). Nothing is sent in that case and the stream stays usable.
184 pub fn reset(&mut self, code: u64) -> Result<(), TransportError> {
185 match self {
186 SendStream::Quic(s) => {
187 let code = varint_code(code)?;
188 s.reset(code).map_err(|_| TransportError::StreamClosed)
189 }
190 #[cfg(feature = "webtransport")]
191 SendStream::WebTransport(s) => s.reset(code),
192 }
193 }
194
195 /// Set the stream's send priority.
196 ///
197 /// Streams with a higher priority have their locally buffered data
198 /// transmitted first. Every stream starts at priority 0.
199 ///
200 /// # Errors
201 /// [`TransportError::StreamClosed`] once the stream's send state has been
202 /// discarded — but only on the QUIC arm, and quinn keeps that state around
203 /// for a while after a `finish` or `reset`, so this is not a reliable *is
204 /// the stream still live?* probe. The WebTransport arm never reports it at
205 /// all: `wtransport` discards the underlying error and always succeeds. Do
206 /// not treat `Ok(())` as proof the priority took effect.
207 pub fn set_priority(&self, priority: i32) -> Result<(), TransportError> {
208 match self {
209 SendStream::Quic(s) => {
210 s.set_priority(priority).map_err(|_| TransportError::StreamClosed)
211 }
212 #[cfg(feature = "webtransport")]
213 SendStream::WebTransport(s) => s.set_priority(priority),
214 }
215 }
216
217 /// Resolve when this send half stops being useful.
218 ///
219 /// [`write_all`](Self::write_all) only reports `STOP_SENDING` when
220 /// there is something to write, so a forwarder that has gone idle —
221 /// the normal state of a stream waiting on its source — never learns
222 /// that the peer walked away. This is the watcher for that case: it
223 /// borrows nothing, so it can sit in a `select!` beside the read
224 /// branch for the stream's whole life.
225 /// The four outcomes, all measured against quinn 0.11.9:
226 ///
227 /// - The peer sent `STOP_SENDING` → [`TransportError::Stopped`] carrying
228 /// the peer's application error code, the same typed value a failed
229 /// [`write_all`](Self::write_all) produces, so a forwarder can mirror it
230 /// verbatim with no new match arm.
231 /// - The stream was finished and the peer acked every byte → `Ok(())`.
232 /// quinn cannot tell that apart from *the send state was discarded*, so
233 /// `Ok(())` means *this stream is over*, never *the peer is happy*. It
234 /// cannot fire on a live stream.
235 /// - The connection was lost → [`TransportError::Connection`].
236 /// - **The local side reset the stream → this future never resolves.**
237 /// quinn keeps no stopped-notification for a stream it has locally reset,
238 /// so a watcher held across a [`reset`](Self::reset) stays pending until
239 /// the connection ends and holds a `tokio::sync::Notify` alive for that
240 /// long. Retire the future *before* resetting.
241 ///
242 /// The returned future is `'static`: it holds a handle on the
243 /// connection, not on `self`, so it may outlive this `SendStream`
244 /// and be spawned or stored on its own.
245 ///
246 /// # WebTransport arm
247 ///
248 /// Reaches the same quinn future through
249 /// `webtransport::WtSendStream::quic_stream` — spelled as code and not
250 /// as an intra-doc link because the `webtransport` module is behind its
251 /// own feature, so a link to it is broken in the default build and
252 /// `just doc-check` runs `cargo doc --workspace --no-deps` without it.
253 /// This bypasses `wtransport`'s own `stopped`, which collapses stopped /
254 /// closed / disconnected into one error. One asymmetry the QUIC arm
255 /// does not have: [`finish`](Self::finish) moves the inner
256 /// `wtransport` stream out, so calling this afterwards yields a
257 /// future that resolves immediately to
258 /// [`TransportError::StreamClosed`] — the "finished and acked" case
259 /// is unobservable there. Not yet exercised against a live
260 /// WebTransport session.
261 pub fn stopped(&self) -> impl Future<Output = Result<(), TransportError>> + Send + 'static {
262 // Resolve the arm eagerly so the returned future borrows nothing
263 // and both arms hand back the *same* quinn future type.
264 let watched: Result<_, TransportError> = match self {
265 SendStream::Quic(s) => Ok(s.stopped()),
266 #[cfg(feature = "webtransport")]
267 SendStream::WebTransport(s) => s.quic_stream().map(|q| q.stopped()),
268 };
269 async move { stopped_outcome(watched?.await) }
270 }
271}
272
273/// Map quinn's `stopped()` result onto [`TransportError`].
274///
275/// `Ok(None)` is quinn's *the send state is gone*, which it reports both for a
276/// finished-and-acked stream and for one whose state it discarded; neither is
277/// an error, so both become `Ok(())`.
278fn stopped_outcome(
279 outcome: Result<Option<quinn::VarInt>, quinn::StoppedError>,
280) -> Result<(), TransportError> {
281 match outcome {
282 Ok(Some(code)) => Err(TransportError::Stopped(code.into_inner())),
283 Ok(None) => Ok(()),
284 Err(e) => Err(TransportError::Connection(e.to_string())),
285 }
286}
287
288/// Convert an application error code to a quinn `VarInt`.
289///
290/// QUIC application error codes are varints, so values above
291/// 2^62 - 1 cannot be represented on the wire.
292fn varint_code(code: u64) -> Result<quinn::VarInt, TransportError> {
293 quinn::VarInt::from_u64(code)
294 .map_err(|_| TransportError::Write(format!("error code {code} exceeds the varint range")))
295}
296
297/// A transport-agnostic receive stream.
298pub enum RecvStream {
299 /// Raw QUIC receive stream.
300 Quic(quinn::RecvStream),
301 /// WebTransport receive stream.
302 #[cfg(feature = "webtransport")]
303 WebTransport(webtransport::WtRecvStream),
304}
305
306impl RecvStream {
307 /// Get the QUIC stream ID (transport-level identifier).
308 pub fn stream_id(&self) -> u64 {
309 match self {
310 RecvStream::Quic(s) => s.id().index(),
311 #[cfg(feature = "webtransport")]
312 RecvStream::WebTransport(_) => 0,
313 }
314 }
315
316 /// Read data into the buffer. Returns `Ok(Some(n))` with bytes read,
317 /// `Ok(None)` on stream end, or `Err` on failure.
318 ///
319 /// Fails with [`TransportError::StreamReset`] carrying the peer's
320 /// application error code if the peer reset the stream, which is how
321 /// callers distinguish an abandoned stream from a clean FIN
322 /// (`Ok(None)`).
323 pub async fn read(&mut self, buf: &mut [u8]) -> Result<Option<usize>, TransportError> {
324 match self {
325 RecvStream::Quic(s) => s.read(buf).await.map_err(TransportError::from),
326 #[cfg(feature = "webtransport")]
327 RecvStream::WebTransport(s) => s.read(buf).await,
328 }
329 }
330
331 /// Wait for the peer to reset this stream — **without reading a byte**.
332 ///
333 /// [`read`](Self::read) is the only other way to learn that a peer sent
334 /// `RESET_STREAM`, and it is unusable by a reader that has stopped
335 /// consuming on purpose: a forwarder applying backpressure holds its
336 /// source unread, so the reset surfaces on a call it is deliberately
337 /// not making, and the abandonment goes unobserved for as long as the
338 /// backpressure lasts. This observes the same event on its own.
339 ///
340 /// **It consumes nothing.** No bytes leave the receive buffer, so no
341 /// `MAX_STREAM_DATA` credit is granted and the peer stays flow-control
342 /// blocked exactly as it was. That is the whole point: it is safe to
343 /// poll *while* backpressure is being applied, which
344 /// [`read`](Self::read) is not.
345 ///
346 /// Cancel-safe: it registers interest and consumes no state, so
347 /// dropping the future loses nothing.
348 ///
349 /// # Returns
350 /// - `Ok(Some(code))` — the peer reset the stream with this application
351 /// error code. The same code [`TransportError::StreamReset`] would have
352 /// carried out of [`read`](Self::read).
353 /// - `Ok(None)` — **no reset is observable on this stream, now or ever**,
354 /// and the caller must stop asking: this resolves immediately every time,
355 /// so a caller that re-polls it in a loop spins. Either the transport
356 /// freed the stream's state (it was finished and fully read, or stopped)
357 /// or, on the WebTransport arm, `wtransport` exposes no reset-only
358 /// observable at all and this answers `Ok(None)` unconditionally.
359 /// - `Err` — a connection-level failure.
360 pub async fn received_reset(&mut self) -> Result<Option<u64>, TransportError> {
361 match self {
362 RecvStream::Quic(s) => match s.received_reset().await {
363 Ok(code) => Ok(code.map(|c| c.into_inner())),
364 Err(e) => Err(TransportError::Connection(e.to_string())),
365 },
366 // `wtransport::RecvStream` has no reset-only observable, so a
367 // WebTransport forwarder keeps the pre-existing behaviour: a
368 // peer reset is seen on the next `read` and not before.
369 #[cfg(feature = "webtransport")]
370 RecvStream::WebTransport(_) => Ok(None),
371 }
372 }
373
374 /// Stop accepting data on the stream, discarding anything unread and
375 /// telling the peer to stop transmitting with `code` as the
376 /// `STOP_SENDING` application error code.
377 ///
378 /// Dropping a `RecvStream` also stops it, but with a hard-coded code
379 /// of 0 — so a forwarder mirroring a peer's `STOP_SENDING` must call
380 /// this explicitly to keep the original code intact.
381 ///
382 /// After a successful call the stream is no longer readable, and the
383 /// two arms say so differently: the QUIC arm's [`read`](Self::read)
384 /// returns [`TransportError::Read`], the WebTransport arm's returns
385 /// [`TransportError::StreamClosed`] (the inner stream is consumed,
386 /// because `wtransport::RecvStream::stop` takes `self` by value).
387 /// Stop reading once you have stopped a stream rather than matching
388 /// on which error comes back.
389 ///
390 /// # Errors
391 /// - [`TransportError::StreamClosed`] if the stream was already stopped,
392 /// finished or reset.
393 /// - [`TransportError::Write`] if `code` is outside the QUIC varint range
394 /// (`0..2^62`) — `Write` because what failed is the `STOP_SENDING` frame
395 /// this endpoint would have sent. Nothing is sent in that case and the
396 /// stream stays readable.
397 pub fn stop(&mut self, code: u64) -> Result<(), TransportError> {
398 match self {
399 RecvStream::Quic(s) => {
400 let code = varint_code(code)?;
401 s.stop(code).map_err(|_| TransportError::StreamClosed)
402 }
403 #[cfg(feature = "webtransport")]
404 RecvStream::WebTransport(s) => s.stop(code),
405 }
406 }
407}