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::{
12 dial_quic, dial_quic_to, show_cipher_suite, CertificateHook, CertificateLog, DialError,
13 DialPhase, QuicDialOptions, QuicTarget, TLS13_CIPHER_SUITES,
14};
15#[cfg(feature = "webtransport")]
16pub use webtransport::{dial_webtransport, dial_webtransport_to};
17
18use std::future::Future;
19
20use bytes::Bytes;
21
22/// A handshake that failed, with the codes the failure carried still intact.
23///
24/// The reason it is a struct and not another `String` variant: a peer's refusal
25/// arrives as a *number*, and flattening it into prose is lossy in a way that
26/// only shows up downstream. A TLS alert reaches QUIC as `0x0100 | alert`, so
27/// `no_application_protocol` (120) is `0x178`, and a caller that wants to
28/// distinguish "this relay does not speak our protocol" from "this relay's
29/// certificate expired" (alert 45, `0x12D`) was left parsing error messages for
30/// digits.
31///
32/// Every field is optional except [`reason`](Self::reason), because not every
33/// failure has a code — a timeout and a DNS miss are real outcomes with nothing
34/// numeric in them.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct HandshakeFailure {
37 /// The QUIC error code, when the failure carried one.
38 ///
39 /// Meaningless without [`code_space`](Self::code_space) — the same integer
40 /// says different things in the two spaces.
41 pub code: Option<u64>,
42 /// Which space [`code`](Self::code) is a number in.
43 pub code_space: Option<CodeSpace>,
44 /// The TLS alert, when the code was a *transport* code in QUIC's crypto
45 /// range.
46 ///
47 /// Derived and not independently sourced: QUIC has no separate field for
48 /// it, and `0x0100..=0x01ff` *is* how TLS alerts are carried.
49 pub tls_alert: Option<u8>,
50 /// What the stack said, for the detail no code carries — which certificate
51 /// field mismatched, when it expired, which name was expected.
52 ///
53 /// Alert 42 (`bad_certificate`) is rustls's catch-all, so telling an expired
54 /// certificate from a name mismatch still means reading this.
55 pub reason: String,
56}
57
58/// Which number space a QUIC error code belongs to.
59///
60/// Kept because the same integer means different things in each, and a report
61/// that loses the space is one that cannot be read. Transport code 271 is
62/// `0x10F` — inside the range TLS alerts are carried in. Application code 271
63/// is a peer's own close code and has nothing to do with TLS. Reading an alert
64/// out of the second produces a finding that never happened, which is worse
65/// than producing none.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum CodeSpace {
68 /// QUIC's own transport errors, where TLS alerts arrive as `0x0100 | alert`.
69 Transport,
70 /// The application's close codes — over MoQT, a draft's own error codes.
71 Application,
72}
73
74impl HandshakeFailure {
75 /// A failure carrying a transport-space code, with any TLS alert recovered.
76 ///
77 /// Prefer this over a struct literal: it is what keeps `tls_alert`
78 /// consistent with `code`, and a literal can set the two independently.
79 pub fn transport(code: u64, reason: String) -> Self {
80 Self {
81 code: Some(code),
82 code_space: Some(CodeSpace::Transport),
83 tls_alert: Self::alert_of(code),
84 reason,
85 }
86 }
87
88 /// A failure carrying an application-space close code.
89 ///
90 /// Never carries a TLS alert: application codes are a separate space, and
91 /// the ones that happen to land in `0x0100..=0x01ff` are the trap this
92 /// constructor exists to close.
93 pub fn application(code: u64, reason: String) -> Self {
94 Self { code: Some(code), code_space: Some(CodeSpace::Application), tls_alert: None, reason }
95 }
96
97 /// A failure with nothing numeric in it — a timeout, a DNS miss, a reset.
98 pub fn bare(reason: String) -> Self {
99 Self { code: None, code_space: None, tls_alert: None, reason }
100 }
101
102 /// Recover the alert from a **transport-space** code, if it carries one.
103 ///
104 /// `0x0100 | alert` is the mapping TLS-over-QUIC defines, so the range is
105 /// exactly one byte wide and the low byte is the alert. Applying this to an
106 /// application-space code is a bug — see [`CodeSpace`].
107 pub fn alert_of(code: u64) -> Option<u8> {
108 (0x0100..=0x01ff).contains(&code).then_some((code & 0xff) as u8)
109 }
110}
111
112impl std::fmt::Display for HandshakeFailure {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 f.write_str(&self.reason)
115 }
116}
117
118/// How the QUIC connection under a session was closed, as quinn recorded it.
119///
120/// The same over QUIC and WebTransport: both read the close off the
121/// `quinn::Connection`, so a transport-space code that `wtransport` keeps
122/// private is still read here. A `CLOSE_WEBTRANSPORT_SESSION` capsule is not a
123/// QUIC close; it reaches callers as [`TransportError::SessionClosed`].
124///
125/// There is none for a connection that is still open, nor for a close with no
126/// code: a timeout, a stateless reset, this endpoint's own application close.
127#[non_exhaustive]
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct TransportClose {
130 /// The close code.
131 pub code: u64,
132 /// Which space [`code`](Self::code) is a number in.
133 pub space: CodeSpace,
134 /// `true` when the peer sent the close, `false` when this endpoint's QUIC
135 /// stack detected a transport error and closed with it.
136 pub by_peer: bool,
137 /// The reason phrase, as sent.
138 pub reason: Vec<u8>,
139}
140
141impl TransportClose {
142 /// The close a quinn connection error records, if it carries a code.
143 pub(crate) fn of(e: &quinn::ConnectionError) -> Option<Self> {
144 match e {
145 quinn::ConnectionError::ConnectionClosed(close) => Some(Self {
146 code: u64::from(close.error_code),
147 space: CodeSpace::Transport,
148 by_peer: true,
149 reason: close.reason.to_vec(),
150 }),
151 quinn::ConnectionError::ApplicationClosed(close) => Some(Self {
152 code: close.error_code.into_inner(),
153 space: CodeSpace::Application,
154 by_peer: true,
155 reason: close.reason.to_vec(),
156 }),
157 quinn::ConnectionError::TransportError(error) => Some(Self {
158 code: u64::from(error.code),
159 space: CodeSpace::Transport,
160 by_peer: false,
161 reason: error.reason.clone().into_bytes(),
162 }),
163 _ => None,
164 }
165 }
166
167 /// The close `conn` has recorded so far.
168 pub(crate) fn read(conn: &quinn::Connection) -> Option<Self> {
169 conn.close_reason().as_ref().and_then(Self::of)
170 }
171}
172
173/// Errors from the transport layer.
174///
175/// `#[non_exhaustive]` so that a new failure mode is an additive change. The
176/// crate matches on these internally, where the attribute does not apply;
177/// downstream code that matches needs a `_` arm, which is the trade being made
178/// deliberately.
179#[derive(Debug, thiserror::Error)]
180#[non_exhaustive]
181pub enum TransportError {
182 /// Connection-level error (e.g., peer closed, timeout).
183 #[error("connection error: {0}")]
184 Connection(String),
185 /// Error writing to a stream.
186 #[error("write error: {0}")]
187 Write(String),
188 /// Error reading from a stream.
189 #[error("read error: {0}")]
190 Read(String),
191 /// Stream was closed.
192 #[error("stream closed")]
193 StreamClosed,
194 /// Error sending a datagram.
195 #[error("send datagram error: {0}")]
196 SendDatagram(String),
197 /// Connection was lost.
198 #[error("connection lost")]
199 ConnectionLost,
200 /// Error during connection establishment.
201 #[error("connect error: {0}")]
202 Connect(String),
203 /// A handshake reached the peer and did not complete, with its codes kept.
204 ///
205 /// Distinct from [`Connect`](Self::Connect), which is the same class of
206 /// event before there was anything numeric to keep — building the endpoint,
207 /// binding the socket, resolving a name. Both render the same way, so this
208 /// is additive to anything reading the message.
209 #[error("connect error: {0}")]
210 Handshake(HandshakeFailure),
211 /// The peer abandoned transmission by resetting the stream. Carries
212 /// the peer's application error code so a forwarder can mirror it
213 /// verbatim with [`SendStream::reset`].
214 #[error("stream reset by peer: code {0}")]
215 StreamReset(u64),
216 /// The peer is no longer accepting data on this stream
217 /// (`STOP_SENDING`). Carries the peer's application error code so a
218 /// forwarder can mirror it verbatim with [`RecvStream::stop`].
219 #[error("stream stopped by peer: code {0}")]
220 Stopped(u64),
221 /// The peer closed the whole session, with the code it named.
222 ///
223 /// Distinct from [`StreamReset`](Self::StreamReset), which ends one stream
224 /// and leaves the session alive. Over MoQT this code is a draft's own
225 /// session error — `VERSION_NEGOTIATION_FAILED`, `PROTOCOL_VIOLATION` — and
226 /// it is the sharpest thing a refusal says.
227 ///
228 /// It exists because quinn renders `ReadError::ConnectionLost` as the bare
229 /// words "connection lost" and drops the cause entirely, so without this
230 /// variant a relay that ends a session with a code *and* a reason phrase
231 /// reaches callers as neither. The code is on the value and also in the
232 /// message, spelled the way every other code this crate reports is spelled.
233 #[error("{reason} (code {code})")]
234 SessionClosed {
235 /// The peer's application close code.
236 code: u64,
237 /// quinn's own rendering of the close, which is the peer's reason
238 /// phrase where it sent one and the code where it did not.
239 reason: String,
240 },
241}
242
243/// A transport-agnostic connection (QUIC or WebTransport).
244pub enum Transport {
245 /// Raw QUIC via quinn.
246 Quic(quic::QuicTransport),
247 /// WebTransport via h3 + h3-quinn.
248 #[cfg(feature = "webtransport")]
249 WebTransport(webtransport::WebTransportTransport),
250}
251
252impl Transport {
253 /// Open a bidirectional stream.
254 pub async fn open_bi(&self) -> Result<(SendStream, RecvStream), TransportError> {
255 match self {
256 Transport::Quic(t) => t.open_bi().await,
257 #[cfg(feature = "webtransport")]
258 Transport::WebTransport(t) => t.open_bi().await,
259 }
260 }
261
262 /// Accept an incoming bidirectional stream.
263 pub async fn accept_bi(&self) -> Result<(SendStream, RecvStream), TransportError> {
264 match self {
265 Transport::Quic(t) => t.accept_bi().await,
266 #[cfg(feature = "webtransport")]
267 Transport::WebTransport(t) => t.accept_bi().await,
268 }
269 }
270
271 /// Open a unidirectional send stream.
272 pub async fn open_uni(&self) -> Result<SendStream, TransportError> {
273 match self {
274 Transport::Quic(t) => t.open_uni().await,
275 #[cfg(feature = "webtransport")]
276 Transport::WebTransport(t) => t.open_uni().await,
277 }
278 }
279
280 /// Accept an incoming unidirectional stream.
281 pub async fn accept_uni(&self) -> Result<RecvStream, TransportError> {
282 match self {
283 Transport::Quic(t) => t.accept_uni().await,
284 #[cfg(feature = "webtransport")]
285 Transport::WebTransport(t) => t.accept_uni().await,
286 }
287 }
288
289 /// Send a datagram.
290 pub fn send_datagram(&self, data: Bytes) -> Result<(), TransportError> {
291 match self {
292 Transport::Quic(t) => t.send_datagram(data),
293 #[cfg(feature = "webtransport")]
294 Transport::WebTransport(t) => t.send_datagram(data),
295 }
296 }
297
298 /// Receive a datagram.
299 pub async fn recv_datagram(&self) -> Result<Bytes, TransportError> {
300 match self {
301 Transport::Quic(t) => t.recv_datagram().await,
302 #[cfg(feature = "webtransport")]
303 Transport::WebTransport(t) => t.recv_datagram().await,
304 }
305 }
306
307 /// Close the connection.
308 pub fn close(&self, code: u32, reason: &[u8]) {
309 match self {
310 Transport::Quic(t) => t.close(code, reason),
311 #[cfg(feature = "webtransport")]
312 Transport::WebTransport(t) => t.close(code, reason),
313 }
314 }
315
316 /// A future resolving when the session ends, carrying the peer's close.
317 ///
318 /// Take it before handing the transport to a handshake: a peer refusing a
319 /// setup often closes the session after the handshake has already seen
320 /// end-of-stream.
321 ///
322 /// `Some` on every transport this crate dials. Over WebTransport only an
323 /// application or session close carries a code; a QUIC transport close
324 /// arrives as text, because `wtransport` keeps that code private.
325 /// [`transport_closed`](Self::transport_closed) reads it typed.
326 pub fn closed(&self) -> Option<impl Future<Output = TransportError> + Send + 'static> {
327 let closed: std::pin::Pin<Box<dyn Future<Output = TransportError> + Send>> = match self {
328 Transport::Quic(t) => Box::pin(t.closed()),
329 #[cfg(feature = "webtransport")]
330 Transport::WebTransport(t) => Box::pin(t.closed()),
331 };
332 Some(closed)
333 }
334
335 /// How the QUIC connection was closed, once it has been. See
336 /// [`TransportClose`].
337 pub fn transport_close(&self) -> Option<TransportClose> {
338 TransportClose::read(&self.quinn_connection())
339 }
340
341 /// A future resolving when the QUIC connection closes, with its
342 /// [`TransportClose`].
343 ///
344 /// The typed counterpart to [`closed`](Self::closed), and taken for the
345 /// same reason: before the transport is handed to a handshake. Holding the
346 /// future keeps the connection alive.
347 pub fn transport_closed(
348 &self,
349 ) -> impl Future<Output = Option<TransportClose>> + Send + 'static {
350 let conn = self.quinn_connection();
351 async move { TransportClose::of(&conn.closed().await) }
352 }
353
354 /// A handle on the quinn connection under either transport.
355 pub(crate) fn quinn_connection(&self) -> quinn::Connection {
356 match self {
357 Transport::Quic(t) => t.quinn_connection().clone(),
358 #[cfg(feature = "webtransport")]
359 Transport::WebTransport(t) => t.quinn_connection().clone(),
360 }
361 }
362
363 /// The certificate chain the peer presented, DER-encoded, leaf first.
364 ///
365 /// Empty when there is none to report — the handshake has not completed,
366 /// the peer sent no chain — rather than an error, because none of those
367 /// are failures of this connection.
368 ///
369 /// **This is the whole of the certificate API, and that is deliberate.**
370 /// The bytes are handed over exactly as the peer sent them and nothing
371 /// here parses them, dates them, or decides whether they are trustworthy.
372 /// A certificate verdict is a judgement about what the caller is trying to
373 /// establish: a probe grading a public relay wants to know whether the
374 /// chain reaches a public root, an operator running against their own CA
375 /// wants precisely the opposite answer to count as healthy, and a library
376 /// that picked one would be silently wrong for the other. Whoever knows
377 /// the question owns the answer; this owns the evidence.
378 ///
379 /// One consequence worth naming, because it is the finding this exists to
380 /// make visible: the *length* of what comes back is itself a measurement.
381 /// A relay that serves a leaf and no intermediate hands back a chain of
382 /// one, which a client validating against a fixed root set rejects while a
383 /// browser holding a cached intermediate connects to it happily. That is a
384 /// misconfiguration and not a mystery — but only to a caller that can see
385 /// how many certificates arrived, which is why the chain is returned whole
386 /// rather than as a leaf.
387 ///
388 /// Read off the live connection each time — nothing is cached — so a
389 /// caller that intends to keep the chain must take it while it still holds
390 /// the `Transport`. Closing does not by itself erase the answer (quinn
391 /// keeps the crypto session for the handle's lifetime), but dropping the
392 /// handle does, and a caller that closes by value has done both.
393 pub fn peer_certificates(&self) -> Vec<Vec<u8>> {
394 match self {
395 Transport::Quic(t) => t.peer_certificates(),
396 #[cfg(feature = "webtransport")]
397 Transport::WebTransport(t) => t.peer_certificates(),
398 }
399 }
400
401 /// The application protocol the server selected in the CONNECT response.
402 ///
403 /// `None` over QUIC, where the equivalent answer is the ALPN and
404 /// [`dial_quic_to`] already returns it. Over WebTransport this is
405 /// `WT-Protocol`, which from draft-15 on is where MOQT settles its version
406 /// on that transport. See
407 /// [`WebTransportTransport::wt_protocol`](webtransport::WebTransportTransport::wt_protocol)
408 /// for how the value is read, and why an unquoted one is still read.
409 #[cfg(feature = "wt-protocol")]
410 pub fn wt_protocol(&self) -> Option<String> {
411 match self {
412 Transport::Quic(_) => None,
413 Transport::WebTransport(t) => t.wt_protocol(),
414 }
415 }
416}
417
418/// A transport-agnostic send stream.
419pub enum SendStream {
420 /// Raw QUIC send stream.
421 Quic(quinn::SendStream),
422 /// WebTransport send stream.
423 #[cfg(feature = "webtransport")]
424 WebTransport(webtransport::WtSendStream),
425}
426
427impl SendStream {
428 /// Get the QUIC stream ID (transport-level identifier).
429 pub fn stream_id(&self) -> u64 {
430 match self {
431 SendStream::Quic(s) => u64::from(s.id()),
432 #[cfg(feature = "webtransport")]
433 SendStream::WebTransport(s) => s.stream_id(),
434 }
435 }
436
437 /// Write all bytes to the stream.
438 ///
439 /// Fails with [`TransportError::Stopped`] carrying the peer's
440 /// application error code if the peer sent `STOP_SENDING`.
441 pub async fn write_all(&mut self, buf: &[u8]) -> Result<(), TransportError> {
442 match self {
443 SendStream::Quic(s) => s.write_all(buf).await.map_err(TransportError::from),
444 #[cfg(feature = "webtransport")]
445 SendStream::WebTransport(s) => s.write_all(buf).await,
446 }
447 }
448
449 /// Finish the stream (send FIN).
450 pub fn finish(&mut self) -> Result<(), TransportError> {
451 match self {
452 SendStream::Quic(s) => {
453 s.finish().map_err(|_| TransportError::StreamClosed)?;
454 Ok(())
455 }
456 #[cfg(feature = "webtransport")]
457 SendStream::WebTransport(s) => s.finish(),
458 }
459 }
460
461 /// Reset the stream, telling the peer transmission was abandoned and
462 /// handing it `code` as the `RESET_STREAM` application error code.
463 ///
464 /// This is the only way to abandon a send stream truthfully: simply
465 /// dropping a `SendStream` sends a FIN instead, which tells the peer
466 /// the stream ended *cleanly*. A forwarder that saw the far side
467 /// reset must call this with the code it received, so a truncated
468 /// stream is never laundered into a complete one.
469 ///
470 /// # Errors
471 /// - [`TransportError::StreamClosed`] if the stream was already finished or
472 /// reset.
473 /// - [`TransportError::Write`] if `code` is outside the QUIC varint range
474 /// (`0..2^62`). Nothing is sent in that case and the stream stays usable.
475 pub fn reset(&mut self, code: u64) -> Result<(), TransportError> {
476 match self {
477 SendStream::Quic(s) => {
478 let code = varint_code(code)?;
479 s.reset(code).map_err(|_| TransportError::StreamClosed)
480 }
481 #[cfg(feature = "webtransport")]
482 SendStream::WebTransport(s) => s.reset(code),
483 }
484 }
485
486 /// Set the stream's send priority.
487 ///
488 /// Streams with a higher priority have their locally buffered data
489 /// transmitted first. Every stream starts at priority 0.
490 ///
491 /// # Errors
492 /// [`TransportError::StreamClosed`] once the stream's send state has been
493 /// discarded — but only on the QUIC arm, and quinn keeps that state around
494 /// for a while after a `finish` or `reset`, so this is not a reliable *is
495 /// the stream still live?* probe. The WebTransport arm never reports it at
496 /// all: `wtransport` discards the underlying error and always succeeds. Do
497 /// not treat `Ok(())` as proof the priority took effect.
498 pub fn set_priority(&self, priority: i32) -> Result<(), TransportError> {
499 match self {
500 SendStream::Quic(s) => {
501 s.set_priority(priority).map_err(|_| TransportError::StreamClosed)
502 }
503 #[cfg(feature = "webtransport")]
504 SendStream::WebTransport(s) => s.set_priority(priority),
505 }
506 }
507
508 /// Resolve when this send half stops being useful.
509 ///
510 /// [`write_all`](Self::write_all) only reports `STOP_SENDING` when
511 /// there is something to write, so a forwarder that has gone idle —
512 /// the normal state of a stream waiting on its source — never learns
513 /// that the peer walked away. This is the watcher for that case: it
514 /// borrows nothing, so it can sit in a `select!` beside the read
515 /// branch for the stream's whole life.
516 /// The four outcomes, on quinn 0.11.9:
517 ///
518 /// - The peer sent `STOP_SENDING` → [`TransportError::Stopped`] carrying
519 /// the peer's application error code, the same typed value a failed
520 /// [`write_all`](Self::write_all) produces, so a forwarder can mirror it
521 /// verbatim with no new match arm.
522 /// - The stream was finished and the peer acked every byte → `Ok(())`.
523 /// quinn cannot tell that apart from *the send state was discarded*, so
524 /// `Ok(())` means *this stream is over*, never *the peer is happy*. It
525 /// cannot fire on a live stream.
526 /// - The connection was lost → [`TransportError::Connection`].
527 /// - **The local side reset the stream → this future never resolves.**
528 /// quinn keeps no stopped-notification for a stream it has locally reset,
529 /// so a watcher held across a [`reset`](Self::reset) stays pending until
530 /// the connection ends and holds a `tokio::sync::Notify` alive for that
531 /// long. Retire the future *before* resetting.
532 ///
533 /// The returned future is `'static`: it holds a handle on the
534 /// connection, not on `self`, so it may outlive this `SendStream`
535 /// and be spawned or stored on its own.
536 ///
537 /// # WebTransport arm
538 ///
539 /// Reaches the same quinn future through
540 /// `webtransport::WtSendStream::quic_stream` — spelled as code and not
541 /// as an intra-doc link because the `webtransport` module is behind its
542 /// own feature, so a link to it is broken in the default build and
543 /// `just doc-check` runs `cargo doc --workspace --no-deps` without it.
544 /// This bypasses `wtransport`'s own `stopped`, which collapses stopped /
545 /// closed / disconnected into one error. One asymmetry the QUIC arm
546 /// does not have: [`finish`](Self::finish) moves the inner
547 /// `wtransport` stream out, so calling this afterwards yields a
548 /// future that resolves immediately to
549 /// [`TransportError::StreamClosed`] — the "finished and acked" case
550 /// is unobservable there. Not yet exercised against a live
551 /// WebTransport session.
552 pub fn stopped(&self) -> impl Future<Output = Result<(), TransportError>> + Send + 'static {
553 // Resolve the arm eagerly so the returned future borrows nothing
554 // and both arms hand back the *same* quinn future type.
555 let watched: Result<_, TransportError> = match self {
556 SendStream::Quic(s) => Ok(s.stopped()),
557 #[cfg(feature = "webtransport")]
558 SendStream::WebTransport(s) => s.quic_stream().map(|q| q.stopped()),
559 };
560 async move { stopped_outcome(watched?.await) }
561 }
562}
563
564/// Map quinn's `stopped()` result onto [`TransportError`].
565///
566/// `Ok(None)` is quinn's *the send state is gone*, which it reports both for a
567/// finished-and-acked stream and for one whose state it discarded; neither is
568/// an error, so both become `Ok(())`.
569fn stopped_outcome(
570 outcome: Result<Option<quinn::VarInt>, quinn::StoppedError>,
571) -> Result<(), TransportError> {
572 match outcome {
573 Ok(Some(code)) => Err(TransportError::Stopped(code.into_inner())),
574 Ok(None) => Ok(()),
575 Err(e) => Err(TransportError::Connection(e.to_string())),
576 }
577}
578
579/// Convert an application error code to a quinn `VarInt`.
580///
581/// QUIC application error codes are varints, so values above
582/// 2^62 - 1 cannot be represented on the wire.
583fn varint_code(code: u64) -> Result<quinn::VarInt, TransportError> {
584 quinn::VarInt::from_u64(code)
585 .map_err(|_| TransportError::Write(format!("error code {code} exceeds the varint range")))
586}
587
588/// A transport-agnostic receive stream.
589pub enum RecvStream {
590 /// Raw QUIC receive stream.
591 Quic(quinn::RecvStream),
592 /// WebTransport receive stream.
593 #[cfg(feature = "webtransport")]
594 WebTransport(webtransport::WtRecvStream),
595}
596
597impl RecvStream {
598 /// Get the QUIC stream ID (transport-level identifier).
599 pub fn stream_id(&self) -> u64 {
600 match self {
601 RecvStream::Quic(s) => u64::from(s.id()),
602 #[cfg(feature = "webtransport")]
603 RecvStream::WebTransport(s) => s.stream_id(),
604 }
605 }
606
607 /// Read data into the buffer. Returns `Ok(Some(n))` with bytes read,
608 /// `Ok(None)` on stream end, or `Err` on failure.
609 ///
610 /// Fails with [`TransportError::StreamReset`] carrying the peer's
611 /// application error code if the peer reset the stream, which is how
612 /// callers distinguish an abandoned stream from a clean FIN
613 /// (`Ok(None)`).
614 pub async fn read(&mut self, buf: &mut [u8]) -> Result<Option<usize>, TransportError> {
615 match self {
616 RecvStream::Quic(s) => s.read(buf).await.map_err(TransportError::from),
617 #[cfg(feature = "webtransport")]
618 RecvStream::WebTransport(s) => s.read(buf).await,
619 }
620 }
621
622 /// Wait for the peer to reset this stream — **without reading a byte**.
623 ///
624 /// [`read`](Self::read) is the only other way to learn that a peer sent
625 /// `RESET_STREAM`, and it is unusable by a reader that has stopped
626 /// consuming on purpose: a forwarder applying backpressure holds its
627 /// source unread, so the reset surfaces on a call it is deliberately
628 /// not making, and the abandonment goes unobserved for as long as the
629 /// backpressure lasts. This observes the same event on its own.
630 ///
631 /// **It consumes nothing.** No bytes leave the receive buffer, so no
632 /// `MAX_STREAM_DATA` credit is granted and the peer stays flow-control
633 /// blocked exactly as it was. That is the whole point: it is safe to
634 /// poll *while* backpressure is being applied, which
635 /// [`read`](Self::read) is not.
636 ///
637 /// Cancel-safe: it registers interest and consumes no state, so
638 /// dropping the future loses nothing.
639 ///
640 /// # Returns
641 /// - `Ok(Some(code))` — the peer reset the stream with this application
642 /// error code. The same code [`TransportError::StreamReset`] would have
643 /// carried out of [`read`](Self::read).
644 /// - `Ok(None)` — **no reset is observable on this stream, now or ever**,
645 /// and the caller must stop asking: this resolves immediately every time,
646 /// so a caller that re-polls it in a loop spins. Either the transport
647 /// freed the stream's state (it was finished and fully read, or stopped)
648 /// or, on the WebTransport arm, `wtransport` exposes no reset-only
649 /// observable at all and this answers `Ok(None)` unconditionally.
650 /// - `Err` — a connection-level failure.
651 pub async fn received_reset(&mut self) -> Result<Option<u64>, TransportError> {
652 match self {
653 RecvStream::Quic(s) => match s.received_reset().await {
654 Ok(code) => Ok(code.map(|c| c.into_inner())),
655 Err(e) => Err(TransportError::Connection(e.to_string())),
656 },
657 // `wtransport::RecvStream` has no reset-only observable, so a
658 // WebTransport forwarder keeps the pre-existing behaviour: a
659 // peer reset is seen on the next `read` and not before.
660 #[cfg(feature = "webtransport")]
661 RecvStream::WebTransport(_) => Ok(None),
662 }
663 }
664
665 /// Stop accepting data on the stream, discarding anything unread and
666 /// telling the peer to stop transmitting with `code` as the
667 /// `STOP_SENDING` application error code.
668 ///
669 /// Dropping a `RecvStream` also stops it, but with a hard-coded code
670 /// of 0 — so a forwarder mirroring a peer's `STOP_SENDING` must call
671 /// this explicitly to keep the original code intact.
672 ///
673 /// After a successful call the stream is no longer readable, and the
674 /// two arms say so differently: the QUIC arm's [`read`](Self::read)
675 /// returns [`TransportError::Read`], the WebTransport arm's returns
676 /// [`TransportError::StreamClosed`] (the inner stream is consumed,
677 /// because `wtransport::RecvStream::stop` takes `self` by value).
678 /// Stop reading once you have stopped a stream rather than matching
679 /// on which error comes back.
680 ///
681 /// # Errors
682 /// - [`TransportError::StreamClosed`] if the stream was already stopped,
683 /// finished or reset.
684 /// - [`TransportError::Write`] if `code` is outside the QUIC varint range
685 /// (`0..2^62`) — `Write` because what failed is the `STOP_SENDING` frame
686 /// this endpoint would have sent. Nothing is sent in that case and the
687 /// stream stays readable.
688 pub fn stop(&mut self, code: u64) -> Result<(), TransportError> {
689 match self {
690 RecvStream::Quic(s) => {
691 let code = varint_code(code)?;
692 s.stop(code).map_err(|_| TransportError::StreamClosed)
693 }
694 #[cfg(feature = "webtransport")]
695 RecvStream::WebTransport(s) => s.stop(code),
696 }
697 }
698}