Skip to main content

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}