Skip to main content

moqtap_client/draft17/
connection.rs

1use std::collections::VecDeque;
2use std::sync::Mutex;
3
4use bytes::{Buf, Bytes, BytesMut};
5
6use crate::draft17::endpoint::{Endpoint, EndpointError};
7use crate::draft17::event::{ClientEvent, Direction, StreamKind};
8use crate::draft17::observer::ConnectionObserver;
9use crate::draft17::session::request_id::Role;
10use crate::draft17::session::setup;
11use crate::malformed_tracks::MalformedTrackCondition;
12use crate::track_locations::{ObjectLocation, ObjectRole, TrackObjects};
13use crate::transport::{RecvStream, SendStream, Transport, TransportError};
14use moqtap_codec::dispatch::{
15    AnyControlMessage, AnyDatagramHeader, AnyFetchHeader, AnySubgroupHeader,
16};
17use moqtap_codec::draft17::data_stream::{
18    FetchHeader, FetchObject, FetchObjectHeader, FetchObjectReader, SubgroupObject,
19    SubgroupObjectReader,
20};
21use moqtap_codec::draft17::error_codes::DataStreamResetErrorCode;
22use moqtap_codec::draft17::message::{
23    ControlMessage, FetchOk, MessageType, PublishOk, RequestError, RequestOk, SubscribeOk,
24};
25use moqtap_codec::error::CodecError;
26use moqtap_codec::kvp::KeyValuePair;
27use moqtap_codec::types::*;
28use moqtap_codec::varint::VarInt;
29use moqtap_codec::version::DraftVersion;
30
31/// The ALPN identifier draft-17 uses on raw QUIC, `moqt-17`.
32///
33/// Drafts 07 to 14 share one ALPN, `moq-00`, and a peer that offers it has
34/// said nothing about which of the eight it speaks. Draft-15 ended that:
35/// from there each draft has an ALPN of its own, so the version is settled
36/// by the TLS handshake before a byte of MoQT is written.
37///
38/// This is [`DraftVersion::Draft17`]'s own
39/// [`quic_alpn`](DraftVersion::quic_alpn), which is what
40/// [`ClientConfig::alpn`] offers; the test below holds the two together.
41pub const MOQT_ALPN: &[u8] = b"moqt-17";
42
43/// The unidirectional stream type that marks one direction of the control
44/// plane, and the SETUP message type. On draft-17 they are one number,
45/// 0x2F00: Section 3.4 (Unidirectional Stream Types) lists it as the type of
46/// a SETUP stream, and Section 9.4 gives it as the SETUP message's own type
47/// field.
48///
49/// Because they are the same number a control stream carries no separate
50/// stream header. The varint a reader uses to recognise the stream is the
51/// first field of the SETUP message it then decodes, and a writer that
52/// encodes a SETUP onto a fresh unidirectional stream has already written the
53/// stream type by writing the message.
54///
55/// Encoded with MoQT's variable-length integer, whose width is the number of
56/// leading 1 bits in the first byte, 0x2F00 is the two bytes `AF 00` — four
57/// under RFC 9000's encoding, which this draft does not use. Read it through
58/// [`DraftVersion::decode_varint`] rather than assuming a width.
59pub const CONTROL_STREAM_TYPE: u64 = 0x2F00;
60
61/// The application error code a request stream is reset with when the
62/// requester abandons it: `CANCELLED`, 0x1.
63///
64/// Draft-17 removed UNSUBSCRIBE and FETCH_CANCEL. Cancelling a request is
65/// resetting the bidirectional stream it was made on, and `CANCELLED` is the
66/// code draft-17 assigns for "the subscriber or publisher cancelled the
67/// Request" — see [`DataStreamResetErrorCode::Cancelled`]. Taken from the
68/// codec's own registry rather than written as a literal so a renumbering in
69/// a later draft cannot be missed here.
70///
71/// This is what [`RequestStream::cancel`] uses when no code is chosen for it,
72/// and what `RequestStream`'s [`Drop`] sends.
73pub const REQUEST_CANCELLED: u64 = DataStreamResetErrorCode::Cancelled as u64;
74
75/// The application error code a request stream **the peer opened** is reset
76/// with when this endpoint abandons it: `INTERNAL_ERROR`, 0x0.
77///
78/// Dropping an inbound request is not the act [`REQUEST_CANCELLED`] describes.
79/// Draft-17 Section 3.3.1 grants a responder a cancel — "Receivers cancel
80/// requests if they are unable to or choose not to respond" — but a handle
81/// that fell out of scope chose nothing; it failed to serve, which is what
82/// [`DataStreamResetErrorCode::InternalError`], "an implementation specific
83/// error", names. The two codes are on the wire, so a peer counting refusals
84/// can tell a deliberate rejection from a dropped request only if they differ.
85///
86/// It is also what a responder that already answered is reset with when it is
87/// dropped without finishing. A FIN there would claim the request completed,
88/// and for a subscription it has not: PUBLISH_DONE is still owed.
89pub const REQUEST_UNANSWERED: u64 = DataStreamResetErrorCode::InternalError as u64;
90
91/// Errors from the connection layer.
92#[derive(Debug, thiserror::Error)]
93pub enum ConnectionError {
94    /// Endpoint state machine error.
95    #[error("endpoint error: {0}")]
96    Endpoint(#[from] EndpointError),
97    /// Wire codec error.
98    #[error("codec error: {0}")]
99    Codec(#[from] CodecError),
100    /// Transport-level error.
101    #[error("transport error: {0}")]
102    Transport(#[from] TransportError),
103    /// Variable-length integer decoding error.
104    #[error("varint error: {0}")]
105    VarInt(#[from] moqtap_codec::varint::VarIntError),
106    /// Control stream was not opened.
107    #[error("control stream not open")]
108    NoControlStream,
109    /// Stream ended before a complete message was read.
110    #[error("unexpected end of stream")]
111    UnexpectedEnd,
112    /// Stream was finished by the peer.
113    #[error("stream finished")]
114    StreamFinished,
115    /// Invalid server address string.
116    #[error("invalid server address: {0}")]
117    InvalidAddress(String),
118    /// TLS configuration error.
119    #[error("TLS config error: {0}")]
120    TlsConfig(String),
121    /// Data stream used out of order (e.g. object before header).
122    #[error("data stream state error: {0}")]
123    DataStreamState(&'static str),
124    /// An Object arrived carrying properties on a status that is not Normal.
125    ///
126    /// Draft-17 Section 10.2.1.2: "Any Object with status Normal can have
127    /// properties (Section 2.5). If an endpoint receives properties on an
128    /// Object with status that is not Normal, it MUST close the session with a
129    /// PROTOCOL_VIOLATION."
130    ///
131    /// The codec decodes such an Object rather than refusing it — the frame is
132    /// well formed, and a tool that reports non-conforming traffic has to be
133    /// able to read it. Being an endpoint rather than an observer is what turns
134    /// it into an error, so it is raised here, on the receive path, and not in
135    /// the decoder.
136    #[error(
137        "object {object_id} carries {properties_len} bytes of properties on status {status:?}, which is not Normal"
138    )]
139    PropertiesOnNonNormalStatus {
140        /// The Object ID the properties arrived on.
141        object_id: u64,
142        /// Length in bytes of the properties block.
143        properties_len: usize,
144        /// The Object's status, resolved through the encoding's elision rule.
145        ///
146        /// Spelled out in full because the glob import of `moqtap_codec::types`
147        /// brings a different `ObjectStatus` into this module.
148        status: moqtap_codec::draft17::types::ObjectStatus,
149    },
150    /// A message that begins a request stream was handed to
151    /// [`Connection::send_control`]. Nothing was written.
152    #[error(
153        "{0:?} begins a request stream of its own and must not be written on the control stream"
154    )]
155    RequestOnControlStream(MessageType),
156    /// A message draft-17 places on a request stream was handed to
157    /// [`Connection::send_control`]. Nothing was written.
158    ///
159    /// Distinct from [`ConnectionError::RequestOnControlStream`], which is about
160    /// a message that would *begin* a request stream of its own. This one is
161    /// about a message that belongs on a request stream already open, and so has
162    /// no meaning without one around it.
163    #[error("{0:?} belongs on a request stream, not on the control stream")]
164    RequestStreamMessageOnControlStream(MessageType),
165    /// A datagram carrying an Object Status arrived with bytes after its
166    /// header.
167    ///
168    /// Draft-17 Section 10.2.1.1: "Any object with a status code other than
169    /// zero MUST have an empty payload." Section 10.3.1 says the same thing
170    /// about the framing: a datagram with the STATUS bit set "is present and
171    /// there is no Object Payload."
172    ///
173    /// The codec cannot see this. `DatagramHeader::decode` stops at the end of
174    /// the header and never owns the datagram's tail, so the only layer that
175    /// holds both the status and the bytes after it is this one. Without the
176    /// check the application is handed, say, an End-of-Group object carrying
177    /// four bytes of payload — a combination the draft forbids outright.
178    ///
179    /// Recoverable: the drafts state the rule as a property of a conforming
180    /// object, not as one of the "MUST close the session" cases, so the datagram
181    /// is refused and the session left running.
182    #[error(
183        "datagram for object {object_id} carries {payload_len} bytes after a header \
184         whose status is {status:?}, which permits no payload"
185    )]
186    PayloadOnStatusDatagram {
187        /// Object ID from the datagram header.
188        object_id: u64,
189        /// How many bytes followed the header.
190        payload_len: usize,
191        /// The status the header declared.
192        ///
193        /// Spelled out in full because the glob import of `moqtap_codec::types`
194        /// brings a different `ObjectStatus` into this module.
195        status: Option<moqtap_codec::draft17::types::ObjectStatus>,
196    },
197    /// A bidirectional stream the peer opened began with a message type that
198    /// does not open a request stream.
199    ///
200    /// Draft-17 Section 3.3: "Bidirectional streams MUST NOT begin with any
201    /// other message type unless negotiated. If they do, the peer MUST close
202    /// the Session with a PROTOCOL_VIOLATION." The session has already been
203    /// closed on the wire by the time this is returned, and the offending
204    /// stream reset.
205    #[error(
206        "a bidirectional stream the peer opened began with {0:?}, which does not begin a request stream; the session was closed"
207    )]
208    NonRequestOnRequestStream(MessageType),
209    /// A `respond_*` helper was handed a request stream this endpoint opened.
210    /// Nothing was written and no state moved.
211    #[error(
212        "this endpoint opened request {0}; only the endpoint a request stream was opened toward may answer it"
213    )]
214    RespondedToOwnRequest(u64),
215}
216
217impl From<crate::transport::DialError> for ConnectionError {
218    /// Preserves the variants this error had when the dial was inlined here,
219    /// so a caller matching on `InvalidAddress` or `TlsConfig` sees no change.
220    fn from(e: crate::transport::DialError) -> Self {
221        match e {
222            crate::transport::DialError::InvalidAddress(s) => ConnectionError::InvalidAddress(s),
223            crate::transport::DialError::TlsConfig(s) => ConnectionError::TlsConfig(s),
224            crate::transport::DialError::Transport(e) => ConnectionError::Transport(e),
225        }
226    }
227}
228
229/// Transport type for the connection.
230#[derive(Debug, Clone)]
231pub enum TransportType {
232    /// Raw QUIC via quinn. The `addr` field should be `host:port`.
233    Quic,
234    /// WebTransport via wtransport. The `url` field is the WebTransport URL.
235    WebTransport {
236        /// The WebTransport endpoint URL (e.g., `https://host:port/path`).
237        url: String,
238    },
239}
240
241/// Configuration for a MoQT client connection.
242///
243/// Both `draft` and `transport` are required -- there is no `Default` impl.
244pub struct ClientConfig {
245    /// The MoQT draft version to use (primary, determines codec/framing).
246    pub draft: DraftVersion,
247    /// The transport type (QUIC or WebTransport).
248    pub transport: TransportType,
249    /// Whether to skip TLS certificate verification (for testing).
250    pub skip_cert_verification: bool,
251    /// Custom CA certificates to trust (DER-encoded).
252    pub ca_certs: Vec<Vec<u8>>,
253    /// Setup parameters to include in CLIENT_SETUP (e.g., auth tokens).
254    pub setup_parameters: Vec<KeyValuePair>,
255}
256
257impl ClientConfig {
258    /// Returns the ALPN protocol identifiers for the transport.
259    pub fn alpn(&self) -> Vec<Vec<u8>> {
260        match &self.transport {
261            TransportType::Quic => vec![self.draft.quic_alpn().to_vec()],
262            TransportType::WebTransport { .. } => vec![b"h3".to_vec()],
263        }
264    }
265}
266
267/// A framed writer for a send stream. Handles MoQT length-prefixed framing.
268pub struct FramedSendStream {
269    inner: SendStream,
270    draft: DraftVersion,
271    /// Stateful subgroup object writer.
272    subgroup_io: Option<SubgroupObjectReader>,
273}
274
275impl FramedSendStream {
276    /// Create a new framed send stream for the given draft version.
277    pub fn new(inner: SendStream, draft: DraftVersion) -> Self {
278        Self { inner, draft, subgroup_io: None }
279    }
280
281    /// Get the transport-level stream ID.
282    pub fn stream_id(&self) -> u64 {
283        self.inner.stream_id()
284    }
285
286    /// Write a control message to the stream with type+length framing.
287    /// Returns the raw bytes that were written (for event capture).
288    pub async fn write_control(
289        &mut self,
290        msg: &AnyControlMessage,
291    ) -> Result<Vec<u8>, ConnectionError> {
292        let mut buf = Vec::new();
293        msg.encode(&mut buf)?;
294        self.inner.write_all(&buf).await?;
295        Ok(buf)
296    }
297
298    /// Write a subgroup stream header. Also initializes the internal
299    /// delta-encoding state used by
300    /// [`FramedSendStream::write_subgroup_object`].
301    ///
302    /// The header is refused, and nothing is written, if its fields disagree
303    /// with its own stream type. That check has to happen here rather than at
304    /// the first object: the type is what every object after it is framed
305    /// against, so a header that went out saying the wrong thing cannot be
306    /// taken back.
307    pub async fn write_subgroup_header(
308        &mut self,
309        header: &AnySubgroupHeader,
310    ) -> Result<(), ConnectionError> {
311        let mut buf = Vec::new();
312        header.encode_stream_checked(&mut buf)?;
313        self.inner.write_all(&buf).await?;
314        // Clippy would rather see these two arms as an `if let`, and rustc rejects
315        // that in a single-draft build, where the pattern is irrefutable. Only a
316        // `match` satisfies both.
317        #[allow(clippy::single_match)]
318        match header {
319            AnySubgroupHeader::Draft17(ref d17) => {
320                self.subgroup_io = Some(SubgroupObjectReader::new(d17));
321            }
322            // Only this draft's header seeds the object reader. With draft 17 the only enabled
323            // draft `AnySubgroupHeader` has a single variant, the arm above is exhaustive and this
324            // one unreachable. Compiled in every configuration with the lint allowed, rather than
325            // gated on a `cfg` naming the other thirteen drafts: that list had to be edited in
326            // every draft module whenever a draft was added, and a copy that omitted one left this
327            // match non-exhaustive.
328            #[allow(unreachable_patterns)]
329            _ => {}
330        }
331        Ok(())
332    }
333
334    /// Write a fetch response header.
335    pub async fn write_fetch_header(
336        &mut self,
337        header: &AnyFetchHeader,
338    ) -> Result<(), ConnectionError> {
339        let mut buf = Vec::new();
340        header.encode_stream(&mut buf);
341        self.inner.write_all(&buf).await?;
342        Ok(())
343    }
344
345    /// Append a draft-17 subgroup object to the stream using the
346    /// stateful writer seeded from
347    /// [`FramedSendStream::write_subgroup_header`].
348    pub async fn write_subgroup_object(
349        &mut self,
350        object: &SubgroupObject,
351    ) -> Result<(), ConnectionError> {
352        let writer = self
353            .subgroup_io
354            .as_mut()
355            .ok_or(ConnectionError::DataStreamState("subgroup header not written yet"))?;
356        let mut buf = Vec::new();
357        writer.write_object(object, &mut buf)?;
358        self.inner.write_all(&buf).await?;
359        Ok(())
360    }
361
362    /// Append a fetch object to the stream.
363    ///
364    /// The fetch stream had a header writer and no object writer, so a caller
365    /// could open one and put nothing on it through this type. The subgroup
366    /// stream has had both since the writer was introduced.
367    ///
368    /// The declared length comes from the payload rather than from the caller's
369    /// field: a header that disagrees with the bytes beside it desynchronises
370    /// every object after it on the stream, and nothing downstream can recover.
371    ///
372    /// # Errors
373    ///
374    /// [`ConnectionError::Codec`] if the header's fields disagree with the
375    /// Serialization Flags that announce them, which the encoder refuses rather
376    /// than writing a frame its own reader cannot take apart.
377    pub async fn write_fetch_object(
378        &mut self,
379        header: &FetchObjectHeader,
380        payload: &[u8],
381    ) -> Result<(), ConnectionError> {
382        let mut header = header.clone();
383        header.payload_length = VarInt::from_usize(payload.len());
384        let mut buf = Vec::new();
385        header.encode(&mut buf)?;
386        buf.extend_from_slice(payload);
387        self.inner.write_all(&buf).await?;
388        Ok(())
389    }
390
391    /// Finish the stream (send FIN).
392    pub async fn finish(&mut self) -> Result<(), ConnectionError> {
393        self.inner.finish()?;
394        Ok(())
395    }
396
397    /// Reset the stream with `code`, telling the peer transmission was
398    /// abandoned rather than completed.
399    ///
400    /// Dropping a send stream sends a FIN, which claims the stream ended
401    /// cleanly; this is the only way to say the opposite. See
402    /// [`SendStream::reset`].
403    pub fn reset(&mut self, code: u64) -> Result<(), ConnectionError> {
404        self.inner.reset(code)?;
405        Ok(())
406    }
407
408    /// Returns the draft version this stream is framed for.
409    pub fn draft(&self) -> DraftVersion {
410        self.draft
411    }
412}
413
414/// What an Object Status makes of an object here.
415///
416/// Two answers where drafts 08 through 13 have three, and the missing one is
417/// the point: the end-of-track status settles where the track ended and is
418/// judged against nothing, because the rule about where one may be placed is
419/// not in this draft. `a_track_may_end_where_it_has_already_been.rs` asserts
420/// that acceptance.
421///
422/// Every other status is a statement about objects rather than one of them.
423fn object_role(status: Option<u64>) -> ObjectRole {
424    match status {
425        None | Some(0x0) => ObjectRole::Produced,
426        Some(0x4) => ObjectRole::EndsTrack(None),
427        _ => ObjectRole::Neither,
428    }
429}
430
431/// A framed reader for a recv stream. Handles MoQT varint-length decoding.
432pub struct FramedRecvStream {
433    inner: RecvStream,
434    buf: BytesMut,
435    draft: DraftVersion,
436    /// Stateful subgroup object reader.
437    subgroup_io: Option<SubgroupObjectReader>,
438    /// Stateful fetch object reader, seeded by
439    /// [`FramedRecvStream::read_fetch_header`]. Draft-17 needs nothing the
440    /// fetch header does not carry, so unlike draft-18 there is no separate
441    /// call to start it.
442    fetch_io: Option<FetchObjectReader>,
443    /// The record this stream's objects are measured against, and the Group ID
444    /// its header named.
445    ///
446    /// One group for the whole stream: a subgroup header names it once and no
447    /// object header repeats it. `None` on a stream that was never given one -
448    /// a stream for an alias no live binding names, and every stream built
449    /// outside [`Connection::accept_subgroup_stream`].
450    tracking: Option<(TrackObjects, u64)>,
451}
452
453impl FramedRecvStream {
454    /// Create a new framed receive stream for the given draft version.
455    pub fn new(inner: RecvStream, draft: DraftVersion) -> Self {
456        Self {
457            inner,
458            buf: BytesMut::with_capacity(4096),
459            draft,
460            subgroup_io: None,
461            fetch_io: None,
462            tracking: None,
463        }
464    }
465
466    /// Get the transport-level stream ID.
467    pub fn stream_id(&self) -> u64 {
468        self.inner.stream_id()
469    }
470
471    /// Measure this stream's objects against `objects`, all of them in `group`.
472    fn measure_objects_against(&mut self, objects: TrackObjects, group: u64) {
473        self.tracking = Some((objects, group));
474    }
475
476    /// Judge one object this stream carried against where its track ended.
477    fn note_subgroup_object(
478        &self,
479        object: u64,
480        status: Option<u64>,
481    ) -> Result<(), ConnectionError> {
482        let Some((objects, group)) = &self.tracking else { return Ok(()) };
483        let at = ObjectLocation { group: *group, object };
484        objects.note_past_final(at, object_role(status)).map_err(|end| {
485            ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject {
486                alias: objects.alias(),
487                group: at.group,
488                object: at.object,
489                final_group: end.group,
490                final_object: end.object,
491            })
492        })
493    }
494
495    /// Read more data from the stream into the internal buffer.
496    async fn fill(&mut self) -> Result<bool, ConnectionError> {
497        let mut tmp = [0u8; 4096];
498        match self.inner.read(&mut tmp).await {
499            Ok(Some(n)) => {
500                self.buf.extend_from_slice(&tmp[..n]);
501                Ok(true)
502            }
503            Ok(None) => Ok(false),
504            Err(e) => Err(ConnectionError::Transport(e)),
505        }
506    }
507
508    /// Ensure at least `n` bytes are available in the buffer.
509    async fn ensure(&mut self, n: usize) -> Result<(), ConnectionError> {
510        while self.buf.len() < n {
511            if !self.fill().await? {
512                return Err(ConnectionError::UnexpectedEnd);
513            }
514        }
515        Ok(())
516    }
517
518    /// Read this stream's leading variable-length integer **without
519    /// consuming it**, and return its value.
520    ///
521    /// Every unidirectional MoQT stream on draft-17 opens with a varint
522    /// naming what it is (Section 3.4): 0x05 for FETCH_HEADER, 0x10-0x1D for
523    /// SUBGROUP_HEADER, and [`CONTROL_STREAM_TYPE`] for SETUP. Telling the
524    /// peer's control stream apart from a data stream means reading that
525    /// varint, and taking it off the transport would destroy it: the control
526    /// stream's type varint *is* the SETUP message's type field, so a stream
527    /// whose type had been stripped would no longer decode as a SETUP.
528    ///
529    /// Nothing is stripped. The bytes land in this reader's own buffer, and
530    /// every other method here — [`read_control`](Self::read_control),
531    /// [`read_subgroup_header`](Self::read_subgroup_header),
532    /// [`read_fetch_header`](Self::read_fetch_header) — decodes out of that
533    /// buffer and advances it only on a successful decode. A stream this was
534    /// called on is indistinguishable from one it was not, which is what
535    /// makes it safe to peek a stream and then hand it to whichever reader
536    /// the type turned out to call for.
537    ///
538    /// It reads, so it can block: a peer that opens a stream and then writes
539    /// nothing leaves this pending until a byte arrives or the stream ends.
540    ///
541    /// # Errors
542    ///
543    /// - [`ConnectionError::UnexpectedEnd`] if the stream ends before a whole
544    ///   varint has arrived.
545    /// - [`ConnectionError::Transport`] if the peer reset the stream.
546    /// - [`ConnectionError::VarInt`] if the bytes are not a valid varint.
547    ///
548    /// Whatever did arrive stays in the buffer in every case.
549    pub async fn peek_stream_type(&mut self) -> Result<u64, ConnectionError> {
550        self.ensure(1).await?;
551        let type_len = self.draft.varint_len(self.buf[0]);
552        self.ensure(type_len).await?;
553        let mut cursor = &self.buf[..type_len];
554        Ok(self.draft.decode_varint(&mut cursor)?.into_inner())
555    }
556
557    /// Read a control message from the stream.
558    ///
559    /// When `capture_raw` is true, the returned tuple includes a clone of the
560    /// framed wire bytes (for observer emission). When false, the second
561    /// element is `None` and the payload clone is skipped.
562    pub async fn read_control(
563        &mut self,
564        capture_raw: bool,
565    ) -> Result<(AnyControlMessage, Option<Vec<u8>>), ConnectionError> {
566        // Read type ID varint
567        self.ensure(1).await?;
568        let type_len = self.draft.varint_len(self.buf[0]);
569        self.ensure(type_len).await?;
570
571        let mut cursor = &self.buf[..type_len];
572        let _type_id = self.draft.decode_varint(&mut cursor)?;
573
574        // Draft-17: 16-bit BE payload length
575        let (payload_len, len_field_size) = if self.draft.uses_fixed_length_framing() {
576            self.ensure(type_len + 2).await?;
577            let hi = self.buf[type_len] as usize;
578            let lo = self.buf[type_len + 1] as usize;
579            ((hi << 8) | lo, 2)
580        } else {
581            self.ensure(type_len + 1).await?;
582            let payload_len_start = type_len;
583            let payload_len_varint_len = self.draft.varint_len(self.buf[payload_len_start]);
584            self.ensure(type_len + payload_len_varint_len).await?;
585            let mut cursor = &self.buf[payload_len_start..type_len + payload_len_varint_len];
586            let payload_len = self.draft.decode_varint(&mut cursor)?.into_inner() as usize;
587            (payload_len, payload_len_varint_len)
588        };
589
590        // Read full payload
591        let total = type_len + len_field_size + payload_len;
592        self.ensure(total).await?;
593
594        // Capture raw bytes only if requested (observer attached).
595        let raw = capture_raw.then(|| self.buf[..total].to_vec());
596
597        // Now decode the whole message
598        let mut frame = &self.buf[..total];
599        let msg = AnyControlMessage::decode(self.draft, &mut frame)?;
600        self.buf.advance(total);
601        Ok((msg, raw))
602    }
603
604    /// Read a subgroup stream header. Also initializes the internal
605    /// delta-decoding state.
606    pub async fn read_subgroup_header(&mut self) -> Result<AnySubgroupHeader, ConnectionError> {
607        self.ensure(1).await?;
608        loop {
609            let mut cursor = &self.buf[..];
610            match AnySubgroupHeader::decode(self.draft, &mut cursor) {
611                Ok(header) => {
612                    let consumed = self.buf.len() - cursor.remaining();
613                    self.buf.advance(consumed);
614                    // Clippy would rather see these two arms as an `if let`, and rustc rejects
615                    // that in a single-draft build, where the pattern is irrefutable. Only a
616                    // `match` satisfies both.
617                    #[allow(clippy::single_match)]
618                    match header {
619                        AnySubgroupHeader::Draft17(ref d17) => {
620                            self.subgroup_io = Some(SubgroupObjectReader::new(d17));
621                        }
622                        // Only this draft's header seeds the object reader. With draft 17 the only
623                        // enabled draft `AnySubgroupHeader` has a single variant, the arm above is
624                        // exhaustive and this one unreachable. Compiled in every configuration with
625                        // the lint allowed, rather than gated on a `cfg` naming the other thirteen
626                        // drafts: that list had to be edited in every draft module whenever a draft
627                        // was added, and a copy that omitted one left this match non-exhaustive.
628                        #[allow(unreachable_patterns)]
629                        _ => {}
630                    }
631                    return Ok(header);
632                }
633                Err(CodecError::UnexpectedEnd) => {
634                    if !self.fill().await? {
635                        return Err(ConnectionError::UnexpectedEnd);
636                    }
637                }
638                Err(e) => return Err(ConnectionError::Codec(e)),
639            }
640        }
641    }
642
643    /// Read a fetch response header.
644    pub async fn read_fetch_header(&mut self) -> Result<AnyFetchHeader, ConnectionError> {
645        self.ensure(1).await?;
646        loop {
647            let mut cursor = &self.buf[..];
648            match AnyFetchHeader::decode(self.draft, &mut cursor) {
649                Ok(header) => {
650                    let consumed = self.buf.len() - cursor.remaining();
651                    self.buf.advance(consumed);
652                    // Seeding here is what makes `read_fetch_object` usable: a
653                    // draft-17 fetch object may leave out fields and take the
654                    // prior Object's, so the objects of one stream have to be
655                    // read through one reader and the header is where that
656                    // reader begins.
657                    self.fetch_io = Some(FetchObjectReader::new());
658                    return Ok(header);
659                }
660                Err(CodecError::UnexpectedEnd) => {
661                    if !self.fill().await? {
662                        return Err(ConnectionError::UnexpectedEnd);
663                    }
664                }
665                Err(e) => return Err(ConnectionError::Codec(e)),
666            }
667        }
668    }
669
670    /// Read the next draft-17 subgroup object from this stream using
671    /// the stateful reader seeded by
672    /// [`FramedRecvStream::read_subgroup_header`].
673    ///
674    /// Errors with [`ConnectionError::PropertiesOnNonNormalStatus`] on an
675    /// Object that carries properties on a status other than Normal, which
676    /// draft-17 Section 10.2.1.2 answers with a session close. The Object is
677    /// consumed from the stream before the check, so the reader stays in step
678    /// with the wire and a caller that reports the violation and reads on sees
679    /// the following Object rather than a re-parse of this one.
680    pub async fn read_subgroup_object(&mut self) -> Result<SubgroupObject, ConnectionError> {
681        if self.subgroup_io.is_none() {
682            return Err(ConnectionError::DataStreamState("subgroup header not read yet"));
683        }
684        loop {
685            let reader = self.subgroup_io.as_mut().unwrap();
686            let mut probe = reader.clone();
687            let mut cursor = &self.buf[..];
688            match probe.read_object(&mut cursor) {
689                Ok(obj) => {
690                    let consumed = self.buf.len() - cursor.remaining();
691                    self.buf.advance(consumed);
692                    *reader = probe;
693                    if !obj.properties_permitted() {
694                        return Err(ConnectionError::PropertiesOnNonNormalStatus {
695                            object_id: obj.object_id.into_inner(),
696                            properties_len: obj.extension_headers.len(),
697                            status: obj.status(),
698                        });
699                    }
700                    self.note_subgroup_object(
701                        obj.object_id.into_inner(),
702                        obj.object_status.map(|s| s as u64),
703                    )?;
704                    return Ok(obj);
705                }
706                Err(CodecError::UnexpectedEnd) => {
707                    if !self.fill().await? {
708                        return Err(ConnectionError::UnexpectedEnd);
709                    }
710                }
711                Err(e) => return Err(ConnectionError::Codec(e)),
712            }
713        }
714    }
715
716    /// Read the next draft-17 fetch header from this stream.
717    ///
718    /// The typed twin of [`read_fetch_header`](Self::read_fetch_header), and it
719    /// has to do the same two things that one does.
720    ///
721    /// It seeds the object reader, because the two consume the same bytes: a
722    /// version that left `fetch_io` unset would put the stream in a state no
723    /// caller can leave, with the next
724    /// [`read_fetch_object`](Self::read_fetch_object) returning its
725    /// `fetch header not read yet` refusal about a header this method has just
726    /// read, and the bytes it would need already spent.
727    ///
728    /// It fills before decoding, and treats a varint that ran out of buffer as
729    /// a short read rather than a malformed header. `FetchHeader::decode`
730    /// reports that as `CodecError::VarInt(VarIntError::UnexpectedEnd)` where
731    /// [`AnyFetchHeader`] reports a bare `CodecError::UnexpectedEnd`, so a loop
732    /// matching only the latter never reaches its own `fill` — and since the
733    /// buffer starts empty, that is every first call on a fresh stream.
734    pub async fn read_fetch_stream_header(&mut self) -> Result<FetchHeader, ConnectionError> {
735        self.ensure(1).await?;
736        loop {
737            let mut cursor = &self.buf[..];
738            match FetchHeader::decode(&mut cursor) {
739                Ok(hdr) => {
740                    let consumed = self.buf.len() - cursor.remaining();
741                    self.buf.advance(consumed);
742                    self.fetch_io = Some(FetchObjectReader::new());
743                    return Ok(hdr);
744                }
745                Err(CodecError::UnexpectedEnd)
746                | Err(CodecError::VarInt(moqtap_codec::varint::VarIntError::UnexpectedEnd)) => {
747                    if !self.fill().await? {
748                        return Err(ConnectionError::UnexpectedEnd);
749                    }
750                }
751                Err(e) => return Err(ConnectionError::Codec(e)),
752            }
753        }
754    }
755
756    /// Stop accepting data on this stream with `code` as the `STOP_SENDING`
757    /// application error code, discarding anything unread.
758    ///
759    /// Dropping a receive stream also stops it, but with a hard-coded 0. See
760    /// [`RecvStream::stop`].
761    pub fn stop(&mut self, code: u64) -> Result<(), ConnectionError> {
762        self.inner.stop(code)?;
763        Ok(())
764    }
765
766    /// Wait for the peer to reset this stream, consuming nothing.
767    ///
768    /// See [`RecvStream::received_reset`] for what `Ok(None)` means and why a
769    /// caller must not re-poll after it.
770    pub async fn received_reset(&mut self) -> Result<Option<u64>, ConnectionError> {
771        Ok(self.inner.received_reset().await?)
772    }
773
774    /// Read the next draft-17 fetch object and its payload.
775    ///
776    /// The mirror of [`FramedSendStream::write_fetch_object`]. What comes back
777    /// is a [`FetchObject`] rather than a header: draft-17's reader resolves the
778    /// fields the Serialization Flags left off the wire, so the Group ID,
779    /// Subgroup ID, Object ID and Priority it carries are the frame's own
780    /// values, not the flags that say where to find them. `object.header` is
781    /// the frame exactly as it arrived, for a caller forwarding the bytes on.
782    ///
783    /// The payload comes back with it for the reason the codec leaves it on the
784    /// wire: `payload_length` says how many bytes follow, and a reader that
785    /// takes the wrong number of them desynchronises every later object on the
786    /// stream. Doing it here is the only place that count and the buffer are
787    /// both in hand.
788    ///
789    /// # Errors
790    ///
791    /// [`ConnectionError::DataStreamState`] when
792    /// [`FramedRecvStream::read_fetch_header`] has not been read first, since
793    /// that is what seeds the reader; [`ConnectionError::UnexpectedEnd`] when
794    /// the stream ends inside the header or inside the payload it declared; and
795    /// [`ConnectionError::Codec`] on a Serialization Flags value the draft does
796    /// not define, and on every rule Section 10.4.4.1 answers with a session
797    /// close — a first Object that inherits a field no Object before it
798    /// established, or an inherited Object ID or Subgroup ID one past the end of
799    /// the space.
800    pub async fn read_fetch_object(&mut self) -> Result<(FetchObject, Vec<u8>), ConnectionError> {
801        if self.fetch_io.is_none() {
802            return Err(ConnectionError::DataStreamState("fetch header not read yet"));
803        }
804        let object = loop {
805            let reader = self.fetch_io.as_mut().unwrap();
806            // Advanced on a probe and committed only once the whole header was
807            // there to read: a reader advanced by a short read would resolve
808            // the next Object against a half-read one.
809            let mut probe = reader.clone();
810            let mut cursor = &self.buf[..];
811            match probe.read_object_header(&mut cursor) {
812                Ok(object) => {
813                    let consumed = self.buf.len() - cursor.remaining();
814                    self.buf.advance(consumed);
815                    *reader = probe;
816                    break object;
817                }
818                Err(CodecError::UnexpectedEnd) => {
819                    if !self.fill().await? {
820                        return Err(ConnectionError::UnexpectedEnd);
821                    }
822                }
823                Err(e) => return Err(ConnectionError::Codec(e)),
824            }
825        };
826        let payload = self.read_object_payload(&object.header.payload_length).await?;
827        Ok((object, payload))
828    }
829
830    /// Take the `length` payload bytes that follow a fetch object's header.
831    ///
832    /// Separate from the header read because the header is decoded from a probe
833    /// cursor that may have to be retried after a fill, and the payload is a
834    /// flat byte count that never is.
835    async fn read_object_payload(&mut self, length: &VarInt) -> Result<Vec<u8>, ConnectionError> {
836        let length = length.into_inner() as usize;
837        self.ensure(length).await?;
838        let payload = self.buf[..length].to_vec();
839        self.buf.advance(length);
840        Ok(payload)
841    }
842
843    /// Returns the draft version this stream is framed for.
844    pub fn draft(&self) -> DraftVersion {
845        self.draft
846    }
847}
848
849/// Which of the six message types draft-17 Section 3.3 lets a bidirectional
850/// stream begin with opened a request stream.
851///
852/// Draft-17 Section 3.3: "A request stream begins with one of these six
853/// message types: TRACK_STATUS, SUBSCRIBE, PUBLISH, FETCH, PUBLISH_NAMESPACE,
854/// and SUBSCRIBE_NAMESPACE. Bidirectional streams MUST NOT begin with any
855/// other message type unless negotiated."
856///
857/// The set is per draft and is not stable across drafts: draft-18 added
858/// SUBSCRIBE_TRACKS and renumbered SUBSCRIBE_NAMESPACE from 0x11 to 0x50. A
859/// six-variant enum here is what makes it impossible to name draft-18's
860/// seventh kind in draft-17 code.
861#[derive(Debug, Clone, Copy, PartialEq, Eq)]
862pub enum RequestKind {
863    /// TRACK_STATUS, 0x0D.
864    TrackStatus,
865    /// SUBSCRIBE, 0x03.
866    Subscribe,
867    /// PUBLISH, 0x1D.
868    Publish,
869    /// FETCH, 0x16 — standalone or joining.
870    Fetch,
871    /// PUBLISH_NAMESPACE, 0x06.
872    PublishNamespace,
873    /// SUBSCRIBE_NAMESPACE, 0x11 on this draft.
874    SubscribeNamespace,
875}
876
877impl RequestKind {
878    /// The message type a request stream of this kind begins with.
879    pub const fn message_type(self) -> MessageType {
880        match self {
881            RequestKind::TrackStatus => MessageType::TrackStatus,
882            RequestKind::Subscribe => MessageType::Subscribe,
883            RequestKind::Publish => MessageType::Publish,
884            RequestKind::Fetch => MessageType::Fetch,
885            RequestKind::PublishNamespace => MessageType::PublishNamespace,
886            RequestKind::SubscribeNamespace => MessageType::SubscribeNamespace,
887        }
888    }
889
890    /// The kind of request stream `ty` opens, or `None` when it opens none.
891    ///
892    /// The inverse of [`message_type`](Self::message_type) and the classifier
893    /// the accept path runs on the first message of a bidirectional stream the
894    /// peer opened. `None` is the PROTOCOL_VIOLATION case of draft-17
895    /// Section 3.3.
896    ///
897    /// Like [`starts_a_request_stream`] the match is exhaustive over
898    /// [`MessageType`] with **no wildcard arm**, so a message type added in a
899    /// later draft stops this compiling until someone classifies it; the unit
900    /// test below holds the two functions to the same answer for every
901    /// assigned type, so neither can drift from the other.
902    pub const fn from_message_type(ty: MessageType) -> Option<RequestKind> {
903        match ty {
904            MessageType::TrackStatus => Some(RequestKind::TrackStatus),
905            MessageType::Subscribe => Some(RequestKind::Subscribe),
906            MessageType::Publish => Some(RequestKind::Publish),
907            MessageType::Fetch => Some(RequestKind::Fetch),
908            MessageType::PublishNamespace => Some(RequestKind::PublishNamespace),
909            MessageType::SubscribeNamespace => Some(RequestKind::SubscribeNamespace),
910            MessageType::Setup
911            | MessageType::GoAway
912            | MessageType::Namespace
913            | MessageType::NamespaceDone
914            | MessageType::PublishBlocked
915            | MessageType::RequestUpdate
916            | MessageType::SubscribeOk
917            | MessageType::RequestOk
918            | MessageType::RequestError
919            | MessageType::FetchOk
920            | MessageType::PublishDone
921            | MessageType::PublishOk => None,
922        }
923    }
924}
925
926/// Which side opened the bidirectional stream a request travels on.
927///
928/// Draft-17 Section 3.3 gives every request a bidirectional stream, and either
929/// endpoint may open one. The two directions are not symmetric — one side owes
930/// a response and the other is waiting for it — so a [`RequestStream`] carries
931/// this to say which side of that it is on.
932#[derive(Debug, Clone, Copy, PartialEq, Eq)]
933pub enum RequestOrigin {
934    /// This endpoint opened the stream and wrote the request on it. What comes
935    /// back is a response, and dropping the handle cancels the request.
936    Local,
937    /// The peer opened the stream; this endpoint owes it a response. What
938    /// comes back is a follow-up to the peer's request, never a response, and
939    /// dropping the handle abandons a request that was asked of us.
940    Peer,
941}
942/// Whether draft-17 places this message on a request stream that is already
943/// open.
944///
945/// Four of them. REQUEST_UPDATE modifies the request its stream carries
946/// (Section 9.10). NAMESPACE and NAMESPACE_DONE report and withdraw the
947/// namespaces a SUBSCRIBE_NAMESPACE asked for, on that request's own response
948/// stream (Sections 9.18 to 9.20), and PUBLISH_BLOCKED names a track in that
949/// namespace the publisher cannot offer (Section 9.21) — draft-17 has no
950/// SUBSCRIBE_TRACKS, so all three share one stream.
951///
952/// `Endpoint::receive_message` already closes the session over all four when
953/// they arrive on the control stream. Without this the client would write on the
954/// control stream exactly what its own peer half refuses to read there.
955fn belongs_on_a_request_stream(ty: MessageType) -> bool {
956    matches!(
957        ty,
958        MessageType::RequestUpdate
959            | MessageType::Namespace
960            | MessageType::NamespaceDone
961            | MessageType::PublishBlocked
962    )
963}
964
965/// Whether `ty` is one of the six message types draft-17 Section 3.3 lets a
966/// bidirectional stream begin with.
967///
968/// The match is exhaustive over [`MessageType`] and deliberately has **no
969/// wildcard arm**. That is the drift guard: `MessageType` is not
970/// `#[non_exhaustive]`, so the day a draft gains a message type this stops
971/// compiling until someone says here whether the new type opens a request
972/// stream. A wildcard would silently answer "no" for it.
973///
974/// Classification is over the typed `MessageType`, never over a raw `u64`,
975/// because the number alone does not say which registry it came from: on
976/// draft-17, 0x10 is GOAWAY as a control message type and also a
977/// SUBGROUP_HEADER as a unidirectional stream type.
978pub const fn starts_a_request_stream(ty: MessageType) -> bool {
979    match ty {
980        // The six that begin a request stream.
981        MessageType::TrackStatus
982        | MessageType::Subscribe
983        | MessageType::Publish
984        | MessageType::Fetch
985        | MessageType::PublishNamespace
986        | MessageType::SubscribeNamespace => true,
987        // These travel on a request stream too — NAMESPACE, NAMESPACE_DONE and
988        // PUBLISH_BLOCKED on the response stream of the SUBSCRIBE_NAMESPACE
989        // that asked for them (Sections 9.18 to 9.21), REQUEST_UPDATE on the
990        // stream of the request it modifies (Section 9.10) — but none of them
991        // may *begin* one, which is the only question asked here. SETUP is the
992        // control stream's own type varint and belongs to no bidirectional
993        // stream at all; GOAWAY drains the whole session.
994        MessageType::Setup
995        | MessageType::GoAway
996        | MessageType::Namespace
997        | MessageType::NamespaceDone
998        | MessageType::PublishBlocked
999        | MessageType::RequestUpdate => false,
1000        // Responses. They cannot begin a stream: they arrive on the request
1001        // stream their request opened, which is why they carry no request id
1002        // of their own on this draft.
1003        MessageType::SubscribeOk
1004        | MessageType::RequestOk
1005        | MessageType::RequestError
1006        | MessageType::FetchOk
1007        | MessageType::PublishDone
1008        | MessageType::PublishOk => false,
1009    }
1010}
1011
1012/// One request and its answer, on a bidirectional stream of their own.
1013///
1014/// Draft-17 Section 3.3 moved requests off the control plane: each request is
1015/// the first message on a bidirectional stream it opens, and the response
1016/// comes back on that same stream. Responses carry no request id on this
1017/// draft — **the stream is the correlation**, which is why this handle exists
1018/// and why a bare request id is no longer enough to find an answer.
1019///
1020/// # Reading and writing go through the connection
1021///
1022/// This handle owns both halves of the stream but not the session, so the
1023/// endpoint state machine and the observer stay where they were. Read a
1024/// response with [`Connection::recv_on_request_stream`], write a follow-up with
1025/// [`Connection::send_on_request_stream`], and cancel with
1026/// [`Connection::cancel_request_stream`].
1027///
1028/// [`cancel`](Self::cancel) and [`peer_cancelled`](Self::peer_cancelled) are on
1029/// the handle because they touch the stream and nothing else, and [`Drop`]
1030/// needs the first of them. Neither moves the endpoint's record of the request,
1031/// which is why the connection carries a pair of its own.
1032///
1033/// # Dropping this cancels the request
1034///
1035/// A dropped handle resets the send half and sends `STOP_SENDING` on the
1036/// receive half, both with [`REQUEST_CANCELLED`], unless the stream was
1037/// already cancelled or finished. Letting the default drop stand would send a
1038/// FIN instead, telling the peer the request ended *cleanly* when it was
1039/// abandoned.
1040///
1041/// The consequence is sharp and worth stating: a live subscription's request
1042/// stream must be **held for the subscription's life**, because PUBLISH_DONE
1043/// arrives on it. Keeping only [`request_id`](Self::request_id) and letting
1044/// the handle fall out of scope cancels the subscription.
1045///
1046/// What a drop cannot do is say so at the endpoint. [`Drop`] holds the stream
1047/// and not the session, so the request stays where it was in the endpoint's
1048/// record while the stream it travelled on is gone. Call
1049/// [`Connection::cancel_request_stream`] wherever that record matters.
1050///
1051/// # Which side opened it changes what this handle does
1052///
1053/// [`origin`](Self::origin) says whether this endpoint opened the stream or
1054/// accepted it, and three behaviours turn on it: reads dispatch as responses
1055/// or as follow-ups to the peer's request, the `respond_*` helpers refuse a
1056/// stream this endpoint opened, and [`Drop`] resets with
1057/// [`REQUEST_UNANSWERED`] rather than [`REQUEST_CANCELLED`]. Everything else —
1058/// [`cancel`](Self::cancel), [`peer_cancelled`](Self::peer_cancelled),
1059/// [`finish`](Self::finish),
1060/// [`Connection::send_on_request_stream`] — is the same in both directions.
1061/// Draft-17 Section 3.3.1 is explicit that a cancel is available to both:
1062/// "Senders cancel requests if the response is no longer of interest;
1063/// Receivers cancel requests if they are unable to or choose not to respond."
1064///
1065/// All fields are private so the shape can grow without breaking callers.
1066#[must_use = "dropping a request stream cancels the request; hold it until the response arrives"]
1067pub struct RequestStream {
1068    send: FramedSendStream,
1069    recv: FramedRecvStream,
1070    request_id: VarInt,
1071    kind: RequestKind,
1072    draft: DraftVersion,
1073    stream_id: u64,
1074    cancelled: bool,
1075    finished: bool,
1076    origin: RequestOrigin,
1077    /// Whether a `respond_*` helper has written a response on this stream.
1078    /// True on a [`RequestOrigin::Peer`] stream from the response written on
1079    /// it, and on a [`RequestOrigin::Local`] one from the answer to an update
1080    /// the peer sent, which is the only response this endpoint writes on a
1081    /// stream of its own.
1082    responded: bool,
1083}
1084
1085impl RequestStream {
1086    /// The request id the endpoint allocated for this request.
1087    ///
1088    /// Useful for logging and for endpoint calls that still take one. It is
1089    /// not enough to find the response: draft-17 responses carry no request
1090    /// id, so only this stream identifies them.
1091    pub fn request_id(&self) -> VarInt {
1092        self.request_id
1093    }
1094
1095    /// Which of the six request types opened this stream.
1096    pub fn kind(&self) -> RequestKind {
1097        self.kind
1098    }
1099
1100    /// The transport-level stream identifier, the same one
1101    /// [`ClientEvent::StreamOpened`] reports for data streams.
1102    pub fn stream_id(&self) -> u64 {
1103        self.stream_id
1104    }
1105
1106    /// The draft version this stream is framed for.
1107    pub fn draft(&self) -> DraftVersion {
1108        self.draft
1109    }
1110
1111    /// Which side opened this stream.
1112    ///
1113    /// [`RequestOrigin::Peer`] means this endpoint owes a response and the
1114    /// `respond_*` helpers apply; [`RequestOrigin::Local`] means it is waiting
1115    /// for one.
1116    pub fn origin(&self) -> RequestOrigin {
1117        self.origin
1118    }
1119
1120    /// Whether a response has been written on this stream by one of the
1121    /// `respond_*` helpers.
1122    ///
1123    /// On a [`RequestOrigin::Local`] stream this says an update the peer sent
1124    /// was answered here, not that the request itself was: that one is
1125    /// answered by the peer.
1126    pub fn responded(&self) -> bool {
1127        self.responded
1128    }
1129
1130    /// Whether [`cancel`](Self::cancel) has already run on this handle.
1131    ///
1132    /// Says nothing about the peer: a peer reset is learned from
1133    /// [`peer_cancelled`](Self::peer_cancelled) or from the next read.
1134    pub fn is_cancelled(&self) -> bool {
1135        self.cancelled
1136    }
1137
1138    /// Cancel the request by resetting the stream, handing the peer `code`.
1139    ///
1140    /// Draft-17 removed UNSUBSCRIBE and FETCH_CANCEL: resetting the request
1141    /// stream is how a request is withdrawn. Both halves are shut — a QUIC
1142    /// bidirectional stream has two independent halves, so resetting only the
1143    /// send half would leave the peer free to keep writing a response nobody
1144    /// will read. The send half is reset with `code` and the receive half is
1145    /// stopped with the same value.
1146    ///
1147    /// [`REQUEST_CANCELLED`] is the ordinary choice. The parameter is a plain
1148    /// `u64` rather than a draft enum because draft-17's registry for these
1149    /// values is titled "Data Stream Reset Error Codes" and a request stream
1150    /// is not a data stream; a caller who wants a typed value has
1151    /// [`DataStreamResetErrorCode::as_u64`].
1152    ///
1153    /// **This is the stream and nothing else.** The endpoint's record of the
1154    /// request does not move, so a response already in flight is still accepted
1155    /// after this returns. [`Connection::cancel_request_stream`] does both and
1156    /// is what a caller holding a connection should reach for; this stays
1157    /// because [`Drop`] has no connection to reach.
1158    ///
1159    /// Idempotent, and it retires the [`Drop`] behaviour: a cancelled handle
1160    /// does nothing further when it goes out of scope. Errors from a stream
1161    /// that was already reset or stopped are swallowed for the same reason —
1162    /// the request is cancelled either way.
1163    ///
1164    /// # Errors
1165    ///
1166    /// [`ConnectionError::Transport`] carrying [`TransportError::Write`] if
1167    /// `code` is outside the QUIC varint range (`0..2^62`). Nothing is sent
1168    /// in that case, and the handle is *not* marked cancelled, so a caller
1169    /// can retry with a representable code.
1170    pub fn cancel(&mut self, code: u64) -> Result<(), ConnectionError> {
1171        if self.cancelled {
1172            return Ok(());
1173        }
1174        // Reject an unrepresentable code before either half is touched, so a
1175        // failed call leaves the stream exactly as it was.
1176        if code > MAX_QUIC_VARINT {
1177            return Err(ConnectionError::Transport(TransportError::Write(format!(
1178                "error code {code} exceeds the varint range"
1179            ))));
1180        }
1181        self.cancelled = true;
1182        // Already-finished or already-reset halves report StreamClosed; the
1183        // request ends up cancelled regardless, so neither is worth raising.
1184        let _ = self.send.reset(code);
1185        let _ = self.recv.stop(code);
1186        Ok(())
1187    }
1188
1189    /// Wait for the peer to cancel this request, consuming nothing.
1190    ///
1191    /// A caller applying backpressure is deliberately not calling
1192    /// [`Connection::recv_on_request_stream`], which is the only other place a
1193    /// peer reset surfaces — so without this the abandonment goes unobserved
1194    /// for as long as the backpressure lasts. This grants no flow-control
1195    /// credit and is cancel-safe.
1196    ///
1197    /// Returns `Ok(Some(code))` with the peer's application error code, or
1198    /// `Ok(None)` meaning **no reset is observable, now or ever — stop
1199    /// asking**. A caller that re-polls after `Ok(None)` spins.
1200    ///
1201    /// Like [`cancel`](Self::cancel), this records nothing at the endpoint.
1202    /// [`Connection::peer_cancelled_on_request_stream`] is the same wait with
1203    /// the record attached.
1204    ///
1205    /// On WebTransport this always answers `Ok(None)`: `wtransport` exposes no
1206    /// reset-only observable, so a WebTransport caller learns of a peer cancel
1207    /// on its next read and not before.
1208    pub async fn peer_cancelled(&mut self) -> Result<Option<u64>, ConnectionError> {
1209        self.recv.received_reset().await
1210    }
1211
1212    /// Finish the send half cleanly, leaving the receive half open.
1213    ///
1214    /// Whether a requester may FIN before its response arrives is not settled
1215    /// by anything this implementation can check, so no request helper calls
1216    /// this and the default is to leave the send half open for the request's
1217    /// life. It is offered for a caller that knows its peer.
1218    ///
1219    /// A finished handle, like a cancelled one, does nothing further on
1220    /// [`Drop`].
1221    pub async fn finish(&mut self) -> Result<(), ConnectionError> {
1222        if self.finished || self.cancelled {
1223            return Ok(());
1224        }
1225        self.finished = true;
1226        self.send.finish().await
1227    }
1228}
1229
1230impl Drop for RequestStream {
1231    /// Reset the request unless it was already cancelled or finished.
1232    ///
1233    /// See the type-level note: the default drop would FIN the send half,
1234    /// which claims a clean end for a request the caller walked away from.
1235    ///
1236    /// The code says which walking away it was. A stream this endpoint opened
1237    /// is cancelled — [`REQUEST_CANCELLED`] — which is the requester act
1238    /// draft-17 Section 3.3.1 describes. A stream the peer opened is reset
1239    /// with [`REQUEST_UNANSWERED`] whether or not a response was already
1240    /// written: before one, the request was never served; after one, the
1241    /// obligations that follow it are still outstanding.
1242    fn drop(&mut self) {
1243        if self.cancelled || self.finished {
1244            return;
1245        }
1246        let code = match self.origin {
1247            RequestOrigin::Local => REQUEST_CANCELLED,
1248            RequestOrigin::Peer => REQUEST_UNANSWERED,
1249        };
1250        let _ = self.send.reset(code);
1251        let _ = self.recv.stop(code);
1252    }
1253}
1254
1255/// Holds a peer-opened stream pair while its first message is being read, and
1256/// puts it back on the connection's queue if that read is abandoned.
1257///
1258/// [`Connection::accept_request_stream`] awaits a whole control message, and a
1259/// caller may drop that future — a `select!` against a shutdown signal is the
1260/// ordinary reason. Without this the stream, and every byte already read off
1261/// it into the reader's buffer, would go with the future: the peer would see a
1262/// request stream reset for no reason it could act on.
1263///
1264/// [`Drop`] is the only place this can run, because a cancelled future is
1265/// never polled again. Every path that finishes — success or error — takes the
1266/// pair out first, so a pair still present when this drops was cancelled.
1267struct PendingInbound<'a> {
1268    pair: Option<(FramedSendStream, FramedRecvStream)>,
1269    queue: &'a Mutex<VecDeque<(FramedSendStream, FramedRecvStream)>>,
1270}
1271
1272impl Drop for PendingInbound<'_> {
1273    fn drop(&mut self) {
1274        if let Some(pair) = self.pair.take() {
1275            // Front, not back: this stream arrived before anything still
1276            // queued behind it, and a partially read message must not be
1277            // handed out after a stream that arrived later.
1278            self.queue.lock().unwrap_or_else(|p| p.into_inner()).push_front(pair);
1279        }
1280    }
1281}
1282
1283/// The largest value a QUIC application error code can carry, `2^62 - 1`.
1284///
1285/// Checked by [`RequestStream::cancel`] before either half of the stream is
1286/// touched, so an unrepresentable code cannot half-cancel a request.
1287const MAX_QUIC_VARINT: u64 = (1u64 << 62) - 1;
1288
1289/// A live MoQT connection over QUIC or WebTransport, combining the endpoint
1290/// state machine with actual network I/O.
1291pub struct Connection {
1292    transport: Transport,
1293    endpoint: Endpoint,
1294    draft: DraftVersion,
1295    control_send: Option<FramedSendStream>,
1296    control_recv: Option<FramedRecvStream>,
1297    observer: Option<Box<dyn ConnectionObserver>>,
1298    /// Setup events buffered during `connect()` and replayed when an
1299    /// observer attaches via `set_observer` — without this, an observer
1300    /// attached after `connect` returns would never see the handshake.
1301    pending_events: Vec<ClientEvent>,
1302    /// Unidirectional streams accepted while `connect` was looking for the
1303    /// peer's control stream, in arrival order.
1304    ///
1305    /// Data streams are allowed to arrive before the control streams on this
1306    /// draft, so the search cannot assume the first unidirectional stream is
1307    /// the control one — and dropping the ones that are not would silently
1308    /// lose objects the peer already sent.
1309    /// [`accept_subgroup_stream`](Connection::accept_subgroup_stream) empties
1310    /// this before it accepts anything new.
1311    ///
1312    /// Behind a mutex because that method takes `&self`. The lock is only
1313    /// ever held for a `pop_front`, never across an await.
1314    deferred_uni: Mutex<VecDeque<FramedRecvStream>>,
1315    /// Bidirectional streams the peer opened that
1316    /// [`accept_request_stream`](Connection::accept_request_stream) took off
1317    /// the transport but did not finish reading a first message from, because
1318    /// its future was dropped. In arrival order.
1319    ///
1320    /// Without this a caller could not put `accept_request_stream` in a
1321    /// `select!` at all: losing the race would lose a stream the peer had
1322    /// already opened and, with it, whatever of the request had arrived.
1323    /// [`accept_request_stream`](Connection::accept_request_stream) empties
1324    /// this before it accepts anything new.
1325    ///
1326    /// Behind a mutex for the same reason `deferred_uni` is: the lock is only
1327    /// ever held for a push or a pop, never across an await.
1328    pending_inbound: Mutex<VecDeque<(FramedSendStream, FramedRecvStream)>>,
1329}
1330
1331impl Connection {
1332    /// Connect to a MoQT server as a client.
1333    ///
1334    /// Establishes a QUIC or WebTransport connection (based on
1335    /// `config.transport`), brings up the control plane, performs the SETUP
1336    /// handshake, and returns a ready-to-use connection.
1337    ///
1338    /// # The control plane is a pair of unidirectional streams
1339    ///
1340    /// Draft-17 Section 3.3: "MOQT uses a pair of unidirectional streams for
1341    /// creating the session and exchanging control messages. Each peer opens
1342    /// one control stream beginning with a SETUP message." So each direction
1343    /// is a separate stream opened by the peer that writes on it. This opens
1344    /// one with `open_uni` and writes SETUP on it, then finds the peer's by
1345    /// accepting unidirectional streams until one leads with
1346    /// [`CONTROL_STREAM_TYPE`].
1347    ///
1348    /// Nothing is written ahead of the SETUP: 0x2F00 is both the SETUP
1349    /// message type and the unidirectional stream type for a control stream,
1350    /// so the message's own first field is the stream header. See
1351    /// [`CONTROL_STREAM_TYPE`].
1352    ///
1353    /// A bidirectional stream is *not* the control stream here — the same
1354    /// section makes it a request stream, one that begins with TRACK_STATUS,
1355    /// SUBSCRIBE, PUBLISH, FETCH, PUBLISH_NAMESPACE or SUBSCRIBE_NAMESPACE:
1356    /// "Bidirectional streams MUST NOT begin with any other message type
1357    /// unless negotiated. If they do, the peer MUST close the Session with a
1358    /// PROTOCOL_VIOLATION." A SETUP written on a bidirectional stream is
1359    /// exactly that case, so a peer that enforces the topology answers it by
1360    /// closing the session.
1361    ///
1362    /// # Unidirectional streams that arrive before the peer's control stream
1363    ///
1364    /// They are kept, not dropped. Section 3.3 expects them: "Unidirectional
1365    /// streams containing Objects or bidirectional stream(s) beginning with a
1366    /// request message could arrive prior to the control streams, in which
1367    /// case the data SHOULD be buffered until both control streams arrive and
1368    /// setup is complete." Each such stream is set aside and handed to
1369    /// [`accept_subgroup_stream`](Self::accept_subgroup_stream) in arrival
1370    /// order, ahead of any newly accepted stream. Only the leading type
1371    /// varint is read from them here; the rest stays on the transport, unread
1372    /// and still flow-controlled, so nothing is buffered in this process
1373    /// beyond those few bytes.
1374    ///
1375    /// One limit worth knowing: the search waits for each stream's type
1376    /// varint in turn, so a peer that opens a unidirectional stream and then
1377    /// writes nothing on it stalls the handshake behind that stream.
1378    pub async fn connect(addr: &str, config: ClientConfig) -> Result<Self, ConnectionError> {
1379        // PATH is for native QUIC only, and the transport is known here and
1380        // nowhere further in. Refusing before dialling means a session that
1381        // the server would close on sight is never opened.
1382        setup::validate_client_path_transport(
1383            &config.setup_parameters,
1384            matches!(config.transport, TransportType::WebTransport { .. }),
1385        )
1386        .map_err(EndpointError::from)?;
1387
1388        let transport = match &config.transport {
1389            TransportType::Quic => Self::connect_quic(addr, &config).await?,
1390            TransportType::WebTransport { url } => {
1391                let url = url.clone();
1392                Self::connect_webtransport(&url, &config).await?
1393            }
1394        };
1395
1396        Self::adopt(transport, config).await
1397    }
1398
1399    /// Run the MoQT setup handshake over a transport somebody else established.
1400    ///
1401    /// For choosing the draft from what the server selected: dial once through
1402    /// [`crate::transport::dial_quic`] offering every ALPN, then bring the
1403    /// connection to the module its answer names. [`Self::connect`] cannot do
1404    /// this — it derives its single ALPN from the draft it was given.
1405    ///
1406    /// `config.draft` must match this module. The transport is adopted as
1407    /// given; nothing here re-checks the ALPN it was negotiated with.
1408    pub async fn adopt(
1409        transport: Transport,
1410        config: ClientConfig,
1411    ) -> Result<Self, ConnectionError> {
1412        let draft = config.draft;
1413        // PATH is for native QUIC only, and the transport is known here and
1414        // nowhere further in. Refusing before dialling means a session that
1415        // the server would close on sight is never opened.
1416        setup::validate_client_path_transport(
1417            &config.setup_parameters,
1418            matches!(config.transport, TransportType::WebTransport { .. }),
1419        )
1420        .map_err(EndpointError::from)?;
1421
1422        // Send half of the control plane: one unidirectional stream whose
1423        // first message is SETUP, which is also its stream header.
1424        let send = transport.open_uni().await?;
1425        let mut control_send = FramedSendStream::new(send, draft);
1426
1427        // Perform setup handshake (draft-17: no versions)
1428        let mut endpoint = Endpoint::new(Role::Client);
1429        endpoint.connect()?;
1430        let setup_msg = endpoint.send_setup(config.setup_parameters.clone())?;
1431        let any_setup = AnyControlMessage::Draft17(setup_msg);
1432        let raw_setup = control_send.write_control(&any_setup).await?;
1433
1434        // Receive half: the peer's control stream is whichever unidirectional
1435        // stream leads with CONTROL_STREAM_TYPE.
1436        let mut deferred_uni: VecDeque<FramedRecvStream> = VecDeque::new();
1437        let mut control_recv = loop {
1438            let recv = transport.accept_uni().await?;
1439            let mut framed = FramedRecvStream::new(recv, draft);
1440            match framed.peek_stream_type().await {
1441                Ok(CONTROL_STREAM_TYPE) => break framed,
1442                // Every other type is a data stream — and so is a stream that
1443                // ended or failed before its type arrived, not because it is
1444                // one but because there is nothing left to decide with. The
1445                // data path sees the same end one read later and reports it
1446                // the way it reports every other. Treating it as the control
1447                // stream would hand the session's control plane to a stream
1448                // that carried nothing.
1449                _ => deferred_uni.push_back(framed),
1450            }
1451        };
1452
1453        let (server_setup, raw_server_setup) = control_recv.read_control(true).await?;
1454        // Unified SETUP in draft-17: server responds with the same message type.
1455        match &server_setup {
1456            AnyControlMessage::Draft17(ControlMessage::Setup(ref s)) => {
1457                endpoint.receive_setup(s)?;
1458            }
1459            _ => {
1460                return Err(ConnectionError::Endpoint(EndpointError::NotActive));
1461            }
1462        }
1463
1464        let pending_events = vec![
1465            ClientEvent::ControlMessage {
1466                direction: Direction::Send,
1467                message: any_setup,
1468                stream_id: None,
1469                raw: Some(raw_setup),
1470            },
1471            ClientEvent::ControlMessage {
1472                direction: Direction::Receive,
1473                message: server_setup,
1474                stream_id: None,
1475                raw: raw_server_setup,
1476            },
1477            ClientEvent::SetupComplete { negotiated_version: 0xff000000 + 17 },
1478        ];
1479
1480        Ok(Self {
1481            transport,
1482            endpoint,
1483            draft,
1484            control_send: Some(control_send),
1485            control_recv: Some(control_recv),
1486            observer: None,
1487            pending_events,
1488            deferred_uni: Mutex::new(deferred_uni),
1489            pending_inbound: Mutex::new(VecDeque::new()),
1490        })
1491    }
1492
1493    /// Establish a raw QUIC connection.
1494    ///
1495    /// Offers this draft's ALPN alone; [`crate::transport::dial_quic`] holds the
1496    /// TLS and endpoint setup.
1497    async fn connect_quic(addr: &str, config: &ClientConfig) -> Result<Transport, ConnectionError> {
1498        let (transport, _negotiated) = crate::transport::dial_quic(
1499            addr,
1500            &crate::transport::QuicDialOptions {
1501                skip_cert_verification: config.skip_cert_verification,
1502                ca_certs: config.ca_certs.clone(),
1503                alpn: config.alpn(),
1504            },
1505        )
1506        .await?;
1507        Ok(transport)
1508    }
1509
1510    /// Establish a WebTransport connection.
1511    #[cfg(feature = "webtransport")]
1512    async fn connect_webtransport(
1513        url: &str,
1514        config: &ClientConfig,
1515    ) -> Result<Transport, ConnectionError> {
1516        use crate::transport::webtransport::WebTransportTransport;
1517
1518        let wt_config = if config.skip_cert_verification {
1519            wtransport::ClientConfig::builder()
1520                .with_bind_default()
1521                .with_no_cert_validation()
1522                .build()
1523        } else {
1524            wtransport::ClientConfig::builder().with_bind_default().with_native_certs().build()
1525        };
1526
1527        let endpoint = wtransport::Endpoint::client(wt_config)
1528            .map_err(|e| ConnectionError::Transport(TransportError::Connect(e.to_string())))?;
1529
1530        let connection = endpoint
1531            .connect(url)
1532            .await
1533            .map_err(|e| ConnectionError::Transport(TransportError::Connect(e.to_string())))?;
1534
1535        Ok(Transport::WebTransport(WebTransportTransport::new(connection)))
1536    }
1537
1538    /// Stub for when the webtransport feature is not enabled.
1539    #[cfg(not(feature = "webtransport"))]
1540    async fn connect_webtransport(
1541        _url: &str,
1542        _config: &ClientConfig,
1543    ) -> Result<Transport, ConnectionError> {
1544        Err(ConnectionError::Transport(TransportError::Connect(
1545            "webtransport feature not enabled".into(),
1546        )))
1547    }
1548
1549    // -- Observer ---------------------------------------------------
1550
1551    /// Attach an observer. Buffered handshake events from `connect()` are
1552    /// flushed in arrival order before this returns.
1553    pub fn set_observer(&mut self, observer: Box<dyn ConnectionObserver>) {
1554        self.observer = Some(observer);
1555        for event in self.pending_events.drain(..) {
1556            if let Some(ref obs) = self.observer {
1557                obs.on_event_owned(event);
1558            }
1559        }
1560    }
1561
1562    /// Remove the observer.
1563    pub fn clear_observer(&mut self) {
1564        self.observer = None;
1565    }
1566
1567    /// Emit an event to the observer, if one is attached.
1568    fn emit(&self, event: ClientEvent) {
1569        if let Some(ref obs) = self.observer {
1570            obs.on_event_owned(event);
1571        }
1572    }
1573
1574    // -- Control message I/O ----------------------------------------
1575
1576    /// Send a control message on the control stream.
1577    ///
1578    /// Wraps the draft-17 message in `AnyControlMessage::Draft17` for
1579    /// framing. This is the route for the messages that belong to the session
1580    /// rather than to one request: GOAWAY, NAMESPACE, NAMESPACE_DONE,
1581    /// PUBLISH_BLOCKED and REQUEST_UPDATE, the last of which carries its own
1582    /// request id and is handled on the control stream by the peer's endpoint.
1583    /// SETUP is written by [`connect`](Self::connect) and is the control
1584    /// stream's own type varint.
1585    ///
1586    /// # Requests are refused here
1587    ///
1588    /// Draft-17 Section 3.3 moved requests off the control plane, in a
1589    /// sentence drafts 18 and 19 keep word for word: "In addition to the
1590    /// control streams, this specification uses bidirectional streams to
1591    /// carry requests." The response comes back on that same bidirectional
1592    /// stream, and resetting it cancels the request (Section 3.3.1).
1593    ///
1594    /// Which types may open one is not shared, so this method is written
1595    /// against draft-17's six and no further: they are [`RequestKind`], and
1596    /// draft-18 makes them seven by adding SUBSCRIBE_TRACKS.
1597    ///
1598    /// Handing one of those six to this method returns
1599    /// [`ConnectionError::RequestOnControlStream`] and writes **nothing** —
1600    /// an enforcing peer sees no bytes at all, not a misplaced request. Use
1601    /// the typed helpers, which open a bidirectional stream each:
1602    /// [`subscribe`](Self::subscribe), [`fetch`](Self::fetch),
1603    /// [`joining_fetch`](Self::joining_fetch), [`publish`](Self::publish),
1604    /// [`track_status`](Self::track_status),
1605    /// [`publish_namespace`](Self::publish_namespace) and
1606    /// [`subscribe_namespace`](Self::subscribe_namespace).
1607    ///
1608    /// Response types are still permitted. A response written here will be
1609    /// refused by a conforming peer, whose endpoint answers a response on the
1610    /// control stream with an error — but this is currently the only route
1611    /// for them at all, since nothing yet accepts a request stream the peer
1612    /// opened, and refusing them would remove capability rather than fix a
1613    /// misdirected write. [`publish_done`](Self::publish_done), the one
1614    /// response with a helper, does not come through here: it takes the
1615    /// request stream its PUBLISH opened.
1616    pub async fn send_control(&mut self, msg: &ControlMessage) -> Result<(), ConnectionError> {
1617        let ty = msg.message_type();
1618        if starts_a_request_stream(ty) {
1619            return Err(ConnectionError::RequestOnControlStream(ty));
1620        }
1621        // What this endpoint refuses to receive on the control stream it must
1622        // not write there either, or the client emits frames its own peer half
1623        // would close the session over.
1624        if belongs_on_a_request_stream(ty) {
1625            return Err(ConnectionError::RequestStreamMessageOnControlStream(ty));
1626        }
1627        let any = AnyControlMessage::Draft17(msg.clone());
1628        let send = self.control_send.as_mut().ok_or(ConnectionError::NoControlStream)?;
1629        let raw = send.write_control(&any).await?;
1630        self.emit(ClientEvent::ControlMessage {
1631            direction: Direction::Send,
1632            message: any,
1633            stream_id: None,
1634            raw: Some(raw),
1635        });
1636        Ok(())
1637    }
1638
1639    /// Read the next control message from the control stream.
1640    ///
1641    /// Returns the `AnyControlMessage` and also extracts the draft-17
1642    /// `ControlMessage` for internal endpoint dispatch.
1643    pub async fn recv_control(&mut self) -> Result<ControlMessage, ConnectionError> {
1644        let recv = self.control_recv.as_mut().ok_or(ConnectionError::NoControlStream)?;
1645        let capture_raw = self.observer.is_some();
1646        let read = recv.read_control(capture_raw).await;
1647        let (any, raw) = match read {
1648            Ok(v) => v,
1649            Err(e) => return Err(self.close_for_codec(e)),
1650        };
1651        if capture_raw {
1652            self.emit(ClientEvent::ControlMessage {
1653                direction: Direction::Receive,
1654                message: any.clone(),
1655                stream_id: None,
1656                raw,
1657            });
1658        }
1659        // Unwrap to draft-17 for the endpoint
1660        match any {
1661            AnyControlMessage::Draft17(msg) => Ok(msg),
1662            // `AnyControlMessage` carries one variant per enabled draft feature. With draft 17 the
1663            // only one enabled the arm above is exhaustive and this rejection arm unreachable.
1664            // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
1665            // naming the other thirteen drafts: that list had to be edited in every draft module
1666            // whenever a draft was added, and a copy that omitted one left this match
1667            // non-exhaustive.
1668            #[allow(unreachable_patterns)]
1669            _ => Err(ConnectionError::Codec(CodecError::UnknownMessageType(0))),
1670        }
1671    }
1672
1673    /// Read and dispatch the next incoming control message through the
1674    /// endpoint state machine. Returns the decoded message for inspection.
1675    ///
1676    /// Responses never arrive here. Draft-17 responses carry no request id
1677    /// and belong on the request stream that asked for them, so the endpoint
1678    /// refuses a response that turns up on the control stream. Read them with
1679    /// [`recv_on_request_stream`](Self::recv_on_request_stream).
1680    pub async fn recv_and_dispatch(&mut self) -> Result<ControlMessage, ConnectionError> {
1681        let msg = self.recv_control().await?;
1682        self.endpoint.receive_message(msg.clone()).map_err(|e| self.close_if_session_fatal(e))?;
1683
1684        // Emit draining event if this was a GoAway
1685        if let ControlMessage::GoAway(ref ga) = msg {
1686            self.emit(ClientEvent::Draining { new_session_uri: ga.new_session_uri.clone() });
1687        }
1688
1689        Ok(msg)
1690    }
1691
1692    // -- Request streams --------------------------------------------
1693
1694    /// Open the bidirectional stream a request will be carried on.
1695    ///
1696    /// Opened *before* the endpoint allocates a request id, so a transport
1697    /// that refuses a new stream — the peer's `initial_max_streams_bidi` is
1698    /// exhausted, the connection is gone — costs nothing. The endpoint has no
1699    /// way to abandon a request it has already allocated, so every failure
1700    /// that can be moved ahead of the allocation is.
1701    ///
1702    /// Nothing is written here. A request stream carries no stream-type
1703    /// header: its first field is the leading message's own type field, which
1704    /// is what [`begin_request`](Self::begin_request) writes.
1705    async fn open_request_bi(
1706        &self,
1707    ) -> Result<(FramedSendStream, FramedRecvStream), ConnectionError> {
1708        let (send, recv) = self.transport.open_bi().await?;
1709        Ok((FramedSendStream::new(send, self.draft), FramedRecvStream::new(recv, self.draft)))
1710    }
1711
1712    /// Reset a request stream that was opened but whose request could not be
1713    /// built, and pass the endpoint's error through.
1714    ///
1715    /// Without this, an endpoint refusal — the session is draining, the
1716    /// request-id range is exhausted — would leave a bidirectional stream
1717    /// open that never carries a first message, and dropping it would FIN it,
1718    /// telling the peer an empty stream ended cleanly.
1719    fn or_abandon<T>(
1720        halves: &mut (FramedSendStream, FramedRecvStream),
1721        built: Result<T, EndpointError>,
1722    ) -> Result<T, ConnectionError> {
1723        match built {
1724            Ok(value) => Ok(value),
1725            Err(e) => {
1726                let _ = halves.0.reset(REQUEST_CANCELLED);
1727                let _ = halves.1.stop(REQUEST_CANCELLED);
1728                Err(ConnectionError::Endpoint(e))
1729            }
1730        }
1731    }
1732
1733    /// Write `msg` as the first message on an opened bidirectional stream and
1734    /// hand back the [`RequestStream`] that owns both halves.
1735    ///
1736    /// This is the one place a request reaches the wire. Every request helper
1737    /// funnels through it, so the ordering — open, allocate, write, emit — is
1738    /// stated once.
1739    ///
1740    /// A failed write resets both halves rather than leaving a half-written
1741    /// request stream behind. What it cannot undo is the endpoint's
1742    /// allocation: the request id and its state machine already exist, and
1743    /// there is no way to retract them, so a write that fails here leaves one
1744    /// pending request the endpoint will never see answered.
1745    async fn begin_request(
1746        &mut self,
1747        halves: (FramedSendStream, FramedRecvStream),
1748        kind: RequestKind,
1749        request_id: VarInt,
1750        msg: &ControlMessage,
1751    ) -> Result<RequestStream, ConnectionError> {
1752        debug_assert_eq!(
1753            msg.message_type(),
1754            kind.message_type(),
1755            "a request stream's first message must be the one its kind names"
1756        );
1757        let (mut send, mut recv) = halves;
1758        let stream_id = send.stream_id();
1759        self.emit(ClientEvent::StreamOpened {
1760            direction: Direction::Send,
1761            stream_kind: StreamKind::Request,
1762            stream_id,
1763        });
1764        let any = AnyControlMessage::Draft17(msg.clone());
1765        let raw = match send.write_control(&any).await {
1766            Ok(raw) => raw,
1767            Err(e) => {
1768                let _ = send.reset(REQUEST_CANCELLED);
1769                let _ = recv.stop(REQUEST_CANCELLED);
1770                return Err(e);
1771            }
1772        };
1773        self.emit(ClientEvent::ControlMessage {
1774            direction: Direction::Send,
1775            message: any,
1776            stream_id: Some(stream_id),
1777            raw: Some(raw),
1778        });
1779        Ok(RequestStream {
1780            send,
1781            recv,
1782            request_id,
1783            kind,
1784            draft: self.draft,
1785            stream_id,
1786            cancelled: false,
1787            finished: false,
1788            origin: RequestOrigin::Local,
1789            responded: false,
1790        })
1791    }
1792
1793    /// Read the next message off a request stream and dispatch it through the
1794    /// endpoint with that stream's own request id.
1795    ///
1796    /// On draft-17 a response carries no request id; the stream is the
1797    /// correlation, so the id comes from the handle and not from the wire.
1798    ///
1799    /// This blocks until a whole message has arrived. Backpressure is per
1800    /// request: a stream nobody reads stays unread, and the peer stays flow
1801    /// controlled on it alone. A peer that reset the stream surfaces as
1802    /// [`ConnectionError::Transport`] carrying
1803    /// [`TransportError::StreamReset`] with the peer's code; a caller that is
1804    /// deliberately not reading should watch
1805    /// [`RequestStream::peer_cancelled`] instead.
1806    ///
1807    /// # Errors
1808    ///
1809    /// [`ConnectionError::Endpoint`] if the message is not one of this
1810    /// draft's response types, or if it does not fit the request's state.
1811    /// The message has already been emitted to the observer by then — what
1812    /// arrived is reported whether or not the endpoint accepts it.
1813    pub async fn recv_on_request_stream(
1814        &mut self,
1815        stream: &mut RequestStream,
1816    ) -> Result<ControlMessage, ConnectionError> {
1817        let capture_raw = self.observer.is_some();
1818        let (any, raw) = match stream.recv.read_control(capture_raw).await {
1819            Ok(read) => read,
1820            Err(e) => {
1821                // A peer that reset this stream cancelled the request on it,
1822                // and this is where a caller reading normally learns of it. The
1823                // record is made and its verdict dropped: the read's own error
1824                // is what the caller has to act on, and returning a state error
1825                // in its place would hide a reset behind it.
1826                if matches!(e, ConnectionError::Transport(TransportError::StreamReset(_))) {
1827                    let _ = self.endpoint.cancel_request(stream.request_id);
1828                }
1829                return Err(e);
1830            }
1831        };
1832        if capture_raw {
1833            self.emit(ClientEvent::ControlMessage {
1834                direction: Direction::Receive,
1835                message: any.clone(),
1836                stream_id: Some(stream.stream_id()),
1837                raw,
1838            });
1839        }
1840        let msg = match any {
1841            AnyControlMessage::Draft17(msg) => Ok::<_, ConnectionError>(msg),
1842            // `AnyControlMessage` carries one variant per enabled draft feature. With draft 17 the
1843            // only one enabled the arm above is exhaustive and this rejection arm unreachable.
1844            // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
1845            // naming the other thirteen drafts: that list had to be edited in every draft module
1846            // whenever a draft was added, and a copy that omitted one left this match
1847            // non-exhaustive.
1848            #[allow(unreachable_patterns)]
1849            _ => Err(ConnectionError::Codec(CodecError::UnknownMessageType(0))),
1850        }?;
1851        // Which dispatcher this belongs to is decided by who opened the
1852        // stream, not by the message. On a stream this endpoint opened the
1853        // next message is the answer to our request; on one the peer opened it
1854        // cannot be, because we are the one who owes an answer. Feeding a
1855        // peer's REQUEST_UPDATE to the response dispatcher would look up a
1856        // request we never made.
1857        let dispatched = match stream.origin {
1858            RequestOrigin::Local => {
1859                self.endpoint.receive_response_on_stream(stream.request_id, msg.clone())
1860            }
1861            RequestOrigin::Peer => {
1862                self.endpoint.receive_on_peer_request_stream(stream.request_id, msg.clone())
1863            }
1864        };
1865        dispatched.map_err(|e| self.close_if_session_fatal(e))?;
1866        Ok(msg)
1867    }
1868
1869    /// Write a follow-up message on an already-open request stream.
1870    ///
1871    /// The request itself was written when the stream was opened; this is for
1872    /// what comes after it on the same stream, PUBLISH_DONE among them — see
1873    /// [`publish_done`](Self::publish_done), which uses this.
1874    ///
1875    /// It does not refuse any message type. Which messages may follow a
1876    /// request on its own stream is not something this implementation can
1877    /// settle, so the choice is left to the caller rather than guessed at.
1878    pub async fn send_on_request_stream(
1879        &mut self,
1880        stream: &mut RequestStream,
1881        msg: &ControlMessage,
1882    ) -> Result<(), ConnectionError> {
1883        let any = AnyControlMessage::Draft17(msg.clone());
1884        let raw = stream.send.write_control(&any).await?;
1885        self.emit(ClientEvent::ControlMessage {
1886            direction: Direction::Send,
1887            message: any,
1888            stream_id: Some(stream.stream_id()),
1889            raw: Some(raw),
1890        });
1891        Ok(())
1892    }
1893
1894    /// Cancel a request: record it at the endpoint, then terminate its stream.
1895    ///
1896    /// Section 3.3.1 puts the cancel at the stream — "Implementations SHOULD
1897    /// cancel requests by abruptly terminating any directions of a stream that
1898    /// are still open" — while the request's own state lives in the endpoint,
1899    /// so the two have to move together. This is the only place that moves
1900    /// both.
1901    ///
1902    /// The endpoint goes first and the stream is terminated only if it agrees,
1903    /// which is the order every request path here uses: a caller acts on a
1904    /// stream after the endpoint has accepted the step, never before. A refused
1905    /// cancel therefore leaves the stream exactly as it was, and
1906    /// [`RequestStream::cancel`] is still there for a caller that wants the
1907    /// stream reset regardless.
1908    ///
1909    /// Idempotent from both ends: a request that has already ended accepts the
1910    /// cancel and stays where it is, and a handle that has already been
1911    /// cancelled resets nothing a second time.
1912    ///
1913    /// # Errors
1914    ///
1915    /// [`ConnectionError::Endpoint`] if no request carries this stream's id or
1916    /// the request has not been written, and [`ConnectionError::Transport`] if
1917    /// `code` is outside the QUIC varint range — see
1918    /// [`RequestStream::cancel`], which is what sends it.
1919    pub fn cancel_request_stream(
1920        &mut self,
1921        stream: &mut RequestStream,
1922        code: u64,
1923    ) -> Result<(), ConnectionError> {
1924        let recorded = self.endpoint.cancel_request(stream.request_id);
1925        recorded.map_err(|e| self.close_if_session_fatal(e))?;
1926        stream.cancel(code)
1927    }
1928
1929    /// Wait for the peer to cancel this request, and record it if it does.
1930    ///
1931    /// [`RequestStream::peer_cancelled`] with the endpoint's record attached. A
1932    /// caller applying backpressure is deliberately not calling
1933    /// [`recv_on_request_stream`](Self::recv_on_request_stream), which is the
1934    /// other place a peer reset surfaces, so without this the request would end
1935    /// on the wire and stay open in the endpoint's record for as long as the
1936    /// backpressure lasts.
1937    ///
1938    /// Returns what the handle's own method returns; see it for the `Ok(None)`
1939    /// case and for what WebTransport can and cannot observe. Cancel-safe, and
1940    /// it grants no flow-control credit.
1941    pub async fn peer_cancelled_on_request_stream(
1942        &mut self,
1943        stream: &mut RequestStream,
1944    ) -> Result<Option<u64>, ConnectionError> {
1945        let code = stream.peer_cancelled().await?;
1946        if code.is_some() {
1947            // Discarded for the reason the read path discards it: the peer has
1948            // ended the request whatever the record said, and a state error
1949            // here would replace the answer the caller asked for.
1950            let _ = self.endpoint.cancel_request(stream.request_id);
1951        }
1952        Ok(code)
1953    }
1954
1955    // -- Accepting the peer's request streams -----------------------
1956
1957    /// Accept the next bidirectional stream the peer opened, read the request
1958    /// it begins with, and hand back that request and a handle to answer it
1959    /// on.
1960    ///
1961    /// This is the mirror of the request helpers. Where
1962    /// [`subscribe`](Self::subscribe) and its siblings open a stream and write
1963    /// a request, this takes one the peer opened and reads one. Draft-17
1964    /// Section 3.3 puts requests in both directions on bidirectional streams,
1965    /// so a client that only ever calls the helpers can never be published to
1966    /// or subscribed from.
1967    ///
1968    /// The returned [`RequestStream`] carries [`RequestOrigin::Peer`]. Answer
1969    /// it with [`respond_subscribe_ok`](Self::respond_subscribe_ok),
1970    /// [`respond_fetch_ok`](Self::respond_fetch_ok),
1971    /// [`respond_publish_ok`](Self::respond_publish_ok),
1972    /// [`respond_ok`](Self::respond_ok) or
1973    /// [`respond_error`](Self::respond_error), and **hold it for as long as
1974    /// the request lasts** — a subscription's PUBLISH_DONE is written on it,
1975    /// and dropping it resets the stream.
1976    ///
1977    /// # Two refusals, two codes
1978    ///
1979    /// Draft-17 Section 3.3, on a stream that begins with the wrong type:
1980    /// "Bidirectional streams MUST NOT begin with any other message type
1981    /// unless negotiated. If they do, the peer MUST close the Session with a
1982    /// PROTOCOL_VIOLATION." Section 9.1, on the Request ID: "If an endpoint
1983    /// receives a Request ID where the least significant bit is incorrect for
1984    /// the sender, or a duplicate Request ID, it MUST close the session with
1985    /// INVALID_REQUEST_ID." Both are closes of the session on the wire, with
1986    /// different codes, and both happen before this returns — the error handed
1987    /// back reports a session that is already gone, not one the caller must
1988    /// remember to close.
1989    ///
1990    /// # Cancelling this future loses nothing
1991    ///
1992    /// A stream taken off the transport but not yet read is put back on an
1993    /// internal queue, and the next call takes it before accepting anything
1994    /// new — including whatever bytes of the request had already arrived,
1995    /// which live in the stream's own reader. So this is safe to `select!`
1996    /// against a shutdown signal or a timer. See
1997    /// [`pending_inbound_count`](Self::pending_inbound_count).
1998    ///
1999    /// What it is **not** safe to do is run concurrently with another method
2000    /// on the same connection: this takes `&mut self` because registering the
2001    /// peer's request moves endpoint state, and no signature avoids that while
2002    /// the connection owns the endpoint. A caller blocked in
2003    /// [`recv_on_request_stream`](Self::recv_on_request_stream) waiting for
2004    /// its own response is not accepting, and the peer's request streams queue
2005    /// up in the transport behind it. One loop that never blocks indefinitely
2006    /// on a single read is the shape this supports.
2007    ///
2008    /// # Ordering
2009    ///
2010    /// The endpoint is told about the request last, after every step that can
2011    /// fail or be cancelled, and building the handle afterwards cannot fail.
2012    /// This is the inverse of the outbound path's reasoning — it opens the
2013    /// stream before allocating a Request ID for the same reason — and rests
2014    /// on the same fact: the endpoint has no way to
2015    /// abandon a request it has already registered. Registering earlier would
2016    /// let a cancelled accept leave a state machine keyed to a stream nobody
2017    /// holds, and the peer's next use of that Request ID would then be
2018    /// reported as a duplicate — a session close, over an id the peer used
2019    /// exactly once.
2020    ///
2021    /// # Errors
2022    ///
2023    /// - [`ConnectionError::NonRequestOnRequestStream`] — the session has been
2024    ///   closed with PROTOCOL_VIOLATION and the stream reset.
2025    /// - [`ConnectionError::Endpoint`] carrying `RequestId` or
2026    ///   `DuplicateRequestId` — the session has been closed with
2027    ///   INVALID_REQUEST_ID and the stream reset.
2028    /// - [`ConnectionError::Endpoint`] carrying `NotActive` or `Draining` —
2029    ///   the stream is reset, the session is left alone.
2030    /// - [`ConnectionError::Transport`] or [`ConnectionError::Codec`] — the
2031    ///   stream is reset, the session is left alone.
2032    pub async fn accept_request_stream(
2033        &mut self,
2034    ) -> Result<(ControlMessage, RequestStream), ConnectionError> {
2035        let pair = match self.take_pending_inbound() {
2036            Some(pair) => pair,
2037            None => {
2038                let (send, recv) = self.transport.accept_bi().await?;
2039                (FramedSendStream::new(send, self.draft), FramedRecvStream::new(recv, self.draft))
2040            }
2041        };
2042        let capture_raw = self.observer.is_some();
2043
2044        let (any, raw, mut send, mut recv) = {
2045            let mut pending = PendingInbound { pair: Some(pair), queue: &self.pending_inbound };
2046            let read = {
2047                let (_, recv) = pending.pair.as_mut().expect("set on construction");
2048                recv.read_control(capture_raw).await
2049            };
2050            // Taken out before anything can return, so the guard's Drop puts
2051            // the pair back for exactly one reason: this future was cancelled.
2052            let (mut send, mut recv) = pending.pair.take().expect("set on construction");
2053            match read {
2054                Ok((any, raw)) => (any, raw, send, recv),
2055                Err(e) => {
2056                    // A stream whose first message could not be read is not
2057                    // worth queueing: the next accept would fail on it the
2058                    // same way. Reset rather than FIN — nothing was served.
2059                    let _ = send.reset(REQUEST_UNANSWERED);
2060                    let _ = recv.stop(REQUEST_UNANSWERED);
2061                    return Err(e);
2062                }
2063            }
2064        };
2065
2066        // Reported once the request has actually arrived rather than when the
2067        // stream came off the transport, so a cancelled accept that is retried
2068        // does not report the same stream twice.
2069        let stream_id = send.stream_id();
2070        self.emit(ClientEvent::StreamOpened {
2071            direction: Direction::Receive,
2072            stream_kind: StreamKind::Request,
2073            stream_id,
2074        });
2075        if capture_raw {
2076            self.emit(ClientEvent::ControlMessage {
2077                direction: Direction::Receive,
2078                message: any.clone(),
2079                stream_id: Some(stream_id),
2080                raw,
2081            });
2082        }
2083
2084        let msg = match any {
2085            AnyControlMessage::Draft17(msg) => msg,
2086            // `AnyControlMessage` carries one variant per enabled draft feature. With draft 17 the
2087            // only one enabled the arm above is exhaustive and this rejection arm unreachable.
2088            // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
2089            // naming the other thirteen drafts: that list had to be edited in every draft module
2090            // whenever a draft was added, and a copy that omitted one left this match
2091            // non-exhaustive.
2092            #[allow(unreachable_patterns)]
2093            _ => {
2094                let _ = send.reset(REQUEST_UNANSWERED);
2095                let _ = recv.stop(REQUEST_UNANSWERED);
2096                return Err(ConnectionError::Codec(CodecError::UnknownMessageType(0)));
2097            }
2098        };
2099
2100        let ty = msg.message_type();
2101        let Some(kind) = RequestKind::from_message_type(ty) else {
2102            let err = self.endpoint.refuse_non_request(ty);
2103            self.close_for(&err);
2104            let _ = send.reset(REQUEST_UNANSWERED);
2105            let _ = recv.stop(REQUEST_UNANSWERED);
2106            return Err(ConnectionError::NonRequestOnRequestStream(ty));
2107        };
2108
2109        let request_id = match self.endpoint.receive_request_on_stream(&msg) {
2110            Ok(request_id) => request_id,
2111            Err(e) => {
2112                let _ = send.reset(REQUEST_UNANSWERED);
2113                let _ = recv.stop(REQUEST_UNANSWERED);
2114                return Err(self.close_if_session_fatal(e));
2115            }
2116        };
2117
2118        Ok((
2119            msg,
2120            RequestStream {
2121                send,
2122                recv,
2123                request_id,
2124                kind,
2125                draft: self.draft,
2126                stream_id,
2127                cancelled: false,
2128                finished: false,
2129                origin: RequestOrigin::Peer,
2130                responded: false,
2131            },
2132        ))
2133    }
2134
2135    /// Take the oldest stream pair a cancelled
2136    /// [`accept_request_stream`](Self::accept_request_stream) put back, if any.
2137    ///
2138    /// Synchronous on purpose, like
2139    /// [`take_deferred_uni`](Self::take_deferred_uni): the guard is dropped
2140    /// before the caller awaits, so the lock is never held across a suspension
2141    /// point.
2142    fn take_pending_inbound(&self) -> Option<(FramedSendStream, FramedRecvStream)> {
2143        self.pending_inbound.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).pop_front()
2144    }
2145
2146    /// How many peer-opened request streams a cancelled
2147    /// [`accept_request_stream`](Self::accept_request_stream) put back and a
2148    /// later call has not yet taken.
2149    ///
2150    /// Zero unless an accept future was dropped mid-read.
2151    pub fn pending_inbound_count(&self) -> usize {
2152        self.pending_inbound.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).len()
2153    }
2154
2155    // -- Answering the peer's requests ------------------------------
2156
2157    /// Write `msg` as the response to the request `stream` carries, driving
2158    /// the endpoint first and the wire second.
2159    ///
2160    /// The response goes on the request's own bidirectional stream and never
2161    /// on the control stream: draft-17 responses carry no Request ID, so the
2162    /// stream is the only thing that says what is being answered. Taking the
2163    /// id off the handle rather than from the caller makes that correlation
2164    /// unforgeable.
2165    ///
2166    /// `fin` is true only for REQUEST_ERROR. See
2167    /// [`respond_error`](Self::respond_error).
2168    async fn respond(
2169        &mut self,
2170        stream: &mut RequestStream,
2171        msg: ControlMessage,
2172        fin: bool,
2173    ) -> Result<(), ConnectionError> {
2174        // A request this endpoint made is answered by the peer, with one
2175        // exception the draft states outright: "A subscriber can also send
2176        // REQUEST_UPDATE to modify parameters of a subscription established
2177        // with PUBLISH", and the receiver of one "MUST respond with exactly one
2178        // REQUEST_OK or REQUEST_ERROR message indicating if the update was
2179        // successful". On a PUBLISH this endpoint sent, that receiver is this
2180        // endpoint, so the one response it may write on a stream of its own is
2181        // the answer to an update waiting there.
2182        let answers_an_update =
2183            matches!(msg, ControlMessage::RequestOk(_) | ControlMessage::RequestError(_))
2184                && self.endpoint.has_unanswered_update(stream.request_id);
2185        if stream.origin != RequestOrigin::Peer && !answers_an_update {
2186            return Err(ConnectionError::RespondedToOwnRequest(stream.request_id.into_inner()));
2187        }
2188        // The endpoint first, so a response that does not fit the request's
2189        // state is refused before any of it reaches the wire. What this cannot
2190        // undo is a write that fails afterwards, which leaves the state
2191        // machine one step ahead of the peer — the same asymmetry
2192        // `begin_request` carries on the outbound side.
2193        self.endpoint.send_response_on_stream(stream.request_id, &msg)?;
2194        self.send_on_request_stream(stream, &msg).await?;
2195        stream.responded = true;
2196        // `fin` says the message ends the exchange; owing a termination says
2197        // it does not, whatever the message looks like. A REQUEST_ERROR
2198        // answering an update is the case where the two disagree, and the
2199        // draft asks for a PUBLISH_DONE after it that a finished send half
2200        // could not carry.
2201        if fin && !self.endpoint.owes_update_failure(stream.request_id) {
2202            stream.finish().await?;
2203        }
2204        Ok(())
2205    }
2206
2207    /// Answer a peer's PUBLISH_NAMESPACE, SUBSCRIBE_NAMESPACE or TRACK_STATUS
2208    /// with REQUEST_OK.
2209    ///
2210    /// The send half is left open. A SUBSCRIBE_NAMESPACE responder still owes
2211    /// the peer the namespaces it accepted, so finishing here would end the
2212    /// request before it had been served; a TRACK_STATUS responder owes
2213    /// nothing further and may call [`RequestStream::finish`] straight after.
2214    ///
2215    /// # Errors
2216    ///
2217    /// [`ConnectionError::RespondedToOwnRequest`] if `stream` is one this
2218    /// endpoint opened, and [`ConnectionError::Endpoint`] if no request of a
2219    /// kind REQUEST_OK answers is pending on it. Nothing is written either
2220    /// way.
2221    pub async fn respond_ok(
2222        &mut self,
2223        stream: &mut RequestStream,
2224        response: RequestOk,
2225    ) -> Result<(), ConnectionError> {
2226        self.respond(stream, ControlMessage::RequestOk(response), false).await
2227    }
2228
2229    /// Answer a peer's SUBSCRIBE with SUBSCRIBE_OK.
2230    ///
2231    /// The send half is left open, and it must be: this endpoint is now the
2232    /// publisher of an established subscription and owes it a PUBLISH_DONE,
2233    /// which travels on this same stream —
2234    /// [`publish_done_on`](Self::publish_done_on).
2235    pub async fn respond_subscribe_ok(
2236        &mut self,
2237        stream: &mut RequestStream,
2238        response: SubscribeOk,
2239    ) -> Result<(), ConnectionError> {
2240        self.respond(stream, ControlMessage::SubscribeOk(response), false).await
2241    }
2242
2243    /// Answer a peer's FETCH with FETCH_OK.
2244    ///
2245    /// The send half is left open. The fetched objects travel on separate
2246    /// unidirectional streams, so a fetch responder may call
2247    /// [`RequestStream::finish`] as soon as this returns; it is not done here
2248    /// because nothing about FETCH_OK says the responder has no more to write.
2249    pub async fn respond_fetch_ok(
2250        &mut self,
2251        stream: &mut RequestStream,
2252        response: FetchOk,
2253    ) -> Result<(), ConnectionError> {
2254        self.respond(stream, ControlMessage::FetchOk(response), false).await
2255    }
2256
2257    /// Answer a peer's PUBLISH with PUBLISH_OK.
2258    ///
2259    /// Draft-17 has PUBLISH_OK as a message of its own, 0x1E; drafts 18 and 19
2260    /// folded it into REQUEST_OK. The send half is left open: accepting a
2261    /// PUBLISH establishes a subscription whose PUBLISH_DONE arrives on this
2262    /// stream.
2263    pub async fn respond_publish_ok(
2264        &mut self,
2265        stream: &mut RequestStream,
2266        response: PublishOk,
2267    ) -> Result<(), ConnectionError> {
2268        self.respond(stream, ControlMessage::PublishOk(response), false).await
2269    }
2270
2271    /// Reject a peer's request with REQUEST_ERROR, and finish the send half.
2272    ///
2273    /// The FIN is part of the act, not a convenience: draft-17 Section 3.3.1
2274    /// says "When an endpoint rejects a request without performing any
2275    /// application processing, it SHOULD send a REQUEST_ERROR and FIN the
2276    /// stream." It is also the one response that can be finished immediately,
2277    /// because a rejected request leaves nothing further to send — every
2278    /// success path owes the peer something more.
2279    ///
2280    /// A finished handle does nothing further on [`Drop`], so the rejected
2281    /// stream is not then reset.
2282    pub async fn respond_error(
2283        &mut self,
2284        stream: &mut RequestStream,
2285        response: RequestError,
2286    ) -> Result<(), ConnectionError> {
2287        self.respond(stream, ControlMessage::RequestError(response), true).await
2288    }
2289
2290    /// End a subscription this endpoint accepted, on the stream the peer's
2291    /// SUBSCRIBE opened.
2292    ///
2293    /// The mirror of [`publish_done`](Self::publish_done), which ends a
2294    /// publication this endpoint offered with PUBLISH. Both write PUBLISH_DONE
2295    /// on a request stream and take the Request ID off the handle; they differ
2296    /// in which state machine moves, and therefore in which one refuses.
2297    pub async fn publish_done_on(
2298        &mut self,
2299        stream: &mut RequestStream,
2300        status_code: VarInt,
2301        stream_count: VarInt,
2302        reason_phrase: Vec<u8>,
2303    ) -> Result<(), ConnectionError> {
2304        let msg = ControlMessage::PublishDone(moqtap_codec::draft17::message::PublishDone {
2305            status_code,
2306            stream_count,
2307            reason_phrase,
2308        });
2309        self.respond(stream, msg, false).await
2310    }
2311
2312    // -- Subscribe flow ---------------------------------------------
2313
2314    /// Send a SUBSCRIBE on a bidirectional stream of its own.
2315    ///
2316    /// The returned [`RequestStream`] is where SUBSCRIBE_OK, REQUEST_ERROR
2317    /// and later PUBLISH_DONE arrive — read them with
2318    /// [`recv_on_request_stream`](Self::recv_on_request_stream). **Hold it for
2319    /// the subscription's life**: dropping it resets the stream, which
2320    /// cancels the subscription.
2321    pub async fn subscribe(
2322        &mut self,
2323        track_namespace: TrackNamespace,
2324        track_name: Vec<u8>,
2325        parameters: Vec<KeyValuePair>,
2326    ) -> Result<RequestStream, ConnectionError> {
2327        let mut halves = self.open_request_bi().await?;
2328        let (req_id, msg) = Self::or_abandon(
2329            &mut halves,
2330            self.endpoint.subscribe(track_namespace, track_name, parameters),
2331        )?;
2332        self.begin_request(halves, RequestKind::Subscribe, req_id, &msg).await
2333    }
2334
2335    // Draft-17: UNSUBSCRIBE removed. Subscribers end a subscription by
2336    // resetting its request stream — `RequestStream::cancel` — or wait for
2337    // PublishDone.
2338
2339    // -- Fetch flow -------------------------------------------------
2340
2341    /// Send a standalone FETCH on a bidirectional stream of its own.
2342    ///
2343    /// FETCH_OK or REQUEST_ERROR comes back on the returned
2344    /// [`RequestStream`]; the fetched objects arrive on separate
2345    /// unidirectional data streams. Dropping the handle cancels the fetch.
2346    #[allow(clippy::too_many_arguments)]
2347    pub async fn fetch(
2348        &mut self,
2349        track_namespace: TrackNamespace,
2350        track_name: Vec<u8>,
2351        start_group: VarInt,
2352        start_object: VarInt,
2353        end_group: VarInt,
2354        end_object: VarInt,
2355        parameters: Vec<KeyValuePair>,
2356    ) -> Result<RequestStream, ConnectionError> {
2357        let mut halves = self.open_request_bi().await?;
2358        let (req_id, msg) = Self::or_abandon(
2359            &mut halves,
2360            self.endpoint.fetch(
2361                track_namespace,
2362                track_name,
2363                start_group,
2364                start_object,
2365                end_group,
2366                end_object,
2367                parameters,
2368            ),
2369        )?;
2370        self.begin_request(halves, RequestKind::Fetch, req_id, &msg).await
2371    }
2372
2373    /// Send a Relative Joining Fetch (Fetch Type 0x2) on a bidirectional
2374    /// stream of its own.
2375    ///
2376    /// A joining FETCH names an existing subscription's request id but is
2377    /// still a FETCH, so it opens its own request stream rather than sharing
2378    /// the subscription's.
2379    ///
2380    /// `joining_start` counts groups back from the subscription's largest
2381    /// group. To name the starting group outright, use
2382    /// [`absolute_joining_fetch`](Self::absolute_joining_fetch).
2383    pub async fn joining_fetch(
2384        &mut self,
2385        joining_request_id: VarInt,
2386        joining_start: VarInt,
2387        parameters: Vec<KeyValuePair>,
2388    ) -> Result<RequestStream, ConnectionError> {
2389        let mut halves = self.open_request_bi().await?;
2390        let (req_id, msg) = Self::or_abandon(
2391            &mut halves,
2392            self.endpoint.joining_fetch(joining_request_id, joining_start, parameters),
2393        )?;
2394        self.begin_request(halves, RequestKind::Fetch, req_id, &msg).await
2395    }
2396
2397    /// Send an Absolute Joining Fetch (Fetch Type 0x3) on a bidirectional
2398    /// stream of its own.
2399    ///
2400    /// Here `joining_start` is the group to begin at rather than an offset:
2401    /// draft-17 Section 9.14.2.1 has the publisher set the Start Location to
2402    /// {Joining Start, 0}.
2403    pub async fn absolute_joining_fetch(
2404        &mut self,
2405        joining_request_id: VarInt,
2406        joining_start: VarInt,
2407        parameters: Vec<KeyValuePair>,
2408    ) -> Result<RequestStream, ConnectionError> {
2409        let mut halves = self.open_request_bi().await?;
2410        let (req_id, msg) = Self::or_abandon(
2411            &mut halves,
2412            self.endpoint.absolute_joining_fetch(joining_request_id, joining_start, parameters),
2413        )?;
2414        self.begin_request(halves, RequestKind::Fetch, req_id, &msg).await
2415    }
2416
2417    // Draft-17: FETCH_CANCEL removed. Fetchers abort with
2418    // `RequestStream::cancel`, which resets the request stream.
2419
2420    // -- Namespace flows --------------------------------------------
2421
2422    /// Send a SUBSCRIBE_NAMESPACE on a bidirectional stream of its own.
2423    ///
2424    /// `subscribe_options` is draft-17's; draft-18 removed the field.
2425    pub async fn subscribe_namespace(
2426        &mut self,
2427        namespace_prefix: TrackNamespace,
2428        subscribe_options: VarInt,
2429        parameters: Vec<KeyValuePair>,
2430    ) -> Result<RequestStream, ConnectionError> {
2431        let mut halves = self.open_request_bi().await?;
2432        let (req_id, msg) = Self::or_abandon(
2433            &mut halves,
2434            self.endpoint.subscribe_namespace(namespace_prefix, subscribe_options, parameters),
2435        )?;
2436        self.begin_request(halves, RequestKind::SubscribeNamespace, req_id, &msg).await
2437    }
2438
2439    /// Send a PUBLISH_NAMESPACE on a bidirectional stream of its own.
2440    pub async fn publish_namespace(
2441        &mut self,
2442        track_namespace: TrackNamespace,
2443        parameters: Vec<KeyValuePair>,
2444    ) -> Result<RequestStream, ConnectionError> {
2445        let mut halves = self.open_request_bi().await?;
2446        let (req_id, msg) = Self::or_abandon(
2447            &mut halves,
2448            self.endpoint.publish_namespace(track_namespace, parameters),
2449        )?;
2450        self.begin_request(halves, RequestKind::PublishNamespace, req_id, &msg).await
2451    }
2452
2453    // -- Track Status flow ------------------------------------------
2454
2455    /// Send a TRACK_STATUS on a bidirectional stream of its own.
2456    pub async fn track_status(
2457        &mut self,
2458        track_namespace: TrackNamespace,
2459        track_name: Vec<u8>,
2460        parameters: Vec<KeyValuePair>,
2461    ) -> Result<RequestStream, ConnectionError> {
2462        let mut halves = self.open_request_bi().await?;
2463        let (req_id, msg) = Self::or_abandon(
2464            &mut halves,
2465            self.endpoint.track_status(track_namespace, track_name, parameters),
2466        )?;
2467        self.begin_request(halves, RequestKind::TrackStatus, req_id, &msg).await
2468    }
2469
2470    // -- Publish flow (publisher side) ------------------------------
2471
2472    /// Send a PUBLISH on a bidirectional stream of its own.
2473    ///
2474    /// PUBLISH_OK or REQUEST_ERROR comes back on the returned
2475    /// [`RequestStream`], and [`publish_done`](Self::publish_done) is written
2476    /// back on it when the publication ends — so the handle must be held for
2477    /// as long as the publication lasts.
2478    pub async fn publish(
2479        &mut self,
2480        track_namespace: TrackNamespace,
2481        track_name: Vec<u8>,
2482        track_alias: VarInt,
2483        parameters: Vec<KeyValuePair>,
2484        track_properties: Vec<KeyValuePair>,
2485    ) -> Result<RequestStream, ConnectionError> {
2486        let mut halves = self.open_request_bi().await?;
2487        let (req_id, msg) = Self::or_abandon(
2488            &mut halves,
2489            self.endpoint.publish(
2490                track_namespace,
2491                track_name,
2492                track_alias,
2493                parameters,
2494                track_properties,
2495            ),
2496        )?;
2497        self.begin_request(halves, RequestKind::Publish, req_id, &msg).await
2498    }
2499
2500    /// Send a PUBLISH_DONE on the request stream the PUBLISH opened.
2501    ///
2502    /// PUBLISH_DONE is a response and carries no request id on the wire, so
2503    /// the stream is the only thing that says which publication ended. The id
2504    /// the endpoint needs is taken off `stream`, which makes the correlation
2505    /// unforgeable — there is no way to name one request and write on
2506    /// another's stream.
2507    pub async fn publish_done(
2508        &mut self,
2509        stream: &mut RequestStream,
2510        status_code: VarInt,
2511        stream_count: VarInt,
2512        reason_phrase: Vec<u8>,
2513    ) -> Result<(), ConnectionError> {
2514        let request_id = stream.request_id();
2515        let msg = self.endpoint.send_publish_done(
2516            request_id,
2517            status_code,
2518            stream_count,
2519            reason_phrase,
2520        )?;
2521        self.send_on_request_stream(stream, &msg).await
2522    }
2523
2524    // -- Data streams -----------------------------------------------
2525
2526    /// Open a new unidirectional stream for sending subgroup data.
2527    pub async fn open_subgroup_stream(
2528        &self,
2529        header: &AnySubgroupHeader,
2530    ) -> Result<FramedSendStream, ConnectionError> {
2531        let send = self.transport.open_uni().await?;
2532        let mut framed = FramedSendStream::new(send, self.draft);
2533        let sid = framed.stream_id();
2534        framed.write_subgroup_header(header).await?;
2535        self.emit(ClientEvent::StreamOpened {
2536            direction: Direction::Send,
2537            stream_kind: StreamKind::Subgroup,
2538            stream_id: sid,
2539        });
2540        self.emit(ClientEvent::DataStreamHeader {
2541            stream_id: sid,
2542            direction: Direction::Send,
2543            header: header.clone(),
2544        });
2545        Ok(framed)
2546    }
2547
2548    /// Open a new unidirectional stream for sending a FETCH's objects.
2549    ///
2550    /// The objects answering a FETCH do not go on the request's own stream:
2551    /// they go on a unidirectional stream of their own, which opens with a
2552    /// FETCH_HEADER naming the request they belong to. This writes that header
2553    /// and hands back the stream, the same way
2554    /// [`open_subgroup_stream`](Self::open_subgroup_stream) does for a
2555    /// subgroup.
2556    ///
2557    /// The caller owns the stream that comes back. Nothing here remembers
2558    /// which request it belongs to, so an endpoint serving several fetches at
2559    /// once keeps its own map from Request ID to stream.
2560    pub async fn open_fetch_stream(
2561        &self,
2562        header: &AnyFetchHeader,
2563    ) -> Result<FramedSendStream, ConnectionError> {
2564        let send = self.transport.open_uni().await?;
2565        let mut framed = FramedSendStream::new(send, self.draft);
2566        let sid = framed.stream_id();
2567        framed.write_fetch_header(header).await?;
2568        self.emit(ClientEvent::StreamOpened {
2569            direction: Direction::Send,
2570            stream_kind: StreamKind::Fetch,
2571            stream_id: sid,
2572        });
2573        Ok(framed)
2574    }
2575
2576    /// Accept an incoming unidirectional data stream and read its subgroup
2577    /// header.
2578    ///
2579    /// Streams the peer opened before its control stream are returned first,
2580    /// in arrival order, before any new one is accepted from the transport:
2581    /// [`connect`](Self::connect) had to look at them to find the control
2582    /// stream and set the rest aside rather than drop them. They are
2583    /// otherwise ordinary — the type varint `connect` read is still on the
2584    /// front of each one.
2585    pub async fn accept_subgroup_stream(
2586        &self,
2587    ) -> Result<(AnySubgroupHeader, FramedRecvStream), ConnectionError> {
2588        let mut framed = match self.take_deferred_uni() {
2589            Some(framed) => framed,
2590            None => FramedRecvStream::new(self.transport.accept_uni().await?, self.draft),
2591        };
2592        let sid = framed.stream_id();
2593        let header = framed.read_subgroup_header().await?;
2594        self.emit(ClientEvent::StreamOpened {
2595            direction: Direction::Receive,
2596            stream_kind: StreamKind::Subgroup,
2597            stream_id: sid,
2598        });
2599        self.emit(ClientEvent::DataStreamHeader {
2600            stream_id: sid,
2601            direction: Direction::Receive,
2602            header: header.clone(),
2603        });
2604        // The track is resolved here and not inside the stream: it takes the
2605        // endpoint's alias table, which a stream handle has no way back to.
2606        // Handed over rather than offered, so measuring is not something a
2607        // caller has to remember to ask for.
2608        if let Some(objects) = self.endpoint.track_objects(header.track_alias()) {
2609            framed.measure_objects_against(objects, header.group_id());
2610        }
2611        Ok((header, framed))
2612    }
2613
2614    /// Accept the next unidirectional stream and read its fetch header.
2615    ///
2616    /// [`accept_subgroup_stream`](Self::accept_subgroup_stream)'s twin. The two
2617    /// are separate because the header decides how every object after it is
2618    /// framed, so a caller has to know which it is expecting before the first
2619    /// byte is read.
2620    ///
2621    /// Objects come off the returned stream with
2622    /// [`FramedRecvStream::read_fetch_object`].
2623    pub async fn accept_fetch_stream(
2624        &self,
2625    ) -> Result<(AnyFetchHeader, FramedRecvStream), ConnectionError> {
2626        let mut framed = match self.take_deferred_uni() {
2627            Some(framed) => framed,
2628            None => FramedRecvStream::new(self.transport.accept_uni().await?, self.draft),
2629        };
2630        let sid = framed.stream_id();
2631        let header = framed.read_fetch_header().await?;
2632        self.emit(ClientEvent::StreamOpened {
2633            direction: Direction::Receive,
2634            stream_kind: StreamKind::Fetch,
2635            stream_id: sid,
2636        });
2637        self.emit(ClientEvent::FetchStreamHeader {
2638            stream_id: sid,
2639            direction: Direction::Receive,
2640            header: header.clone(),
2641        });
2642        // A fetch header goes out as `FetchStreamHeader`; `DataStreamHeader`
2643        // carries an `AnySubgroupHeader` and cannot express one. What
2644        // `accept_subgroup_stream` does beyond this - the forwarding-preference
2645        // note, the object measurement - is about a subgroup and has no
2646        // counterpart on a fetch stream.
2647        Ok((header, framed))
2648    }
2649
2650    /// Take the oldest stream [`connect`](Self::connect) set aside, if any.
2651    ///
2652    /// Synchronous on purpose: the guard is dropped before the caller awaits,
2653    /// so the lock is never held across a suspension point. A poisoned lock
2654    /// is recovered rather than propagated — nothing here can leave the queue
2655    /// in a state a later reader could be misled by, since the only mutation
2656    /// is a `pop_front`.
2657    fn take_deferred_uni(&self) -> Option<FramedRecvStream> {
2658        self.deferred_uni.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).pop_front()
2659    }
2660
2661    /// How many unidirectional streams [`connect`](Self::connect) set aside
2662    /// and [`accept_subgroup_stream`](Self::accept_subgroup_stream) has not
2663    /// yet handed back.
2664    ///
2665    /// Zero for a peer that opened its control stream first, which is the
2666    /// ordinary case.
2667    pub fn deferred_stream_count(&self) -> usize {
2668        self.deferred_uni.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).len()
2669    }
2670
2671    /// Send an object via datagram.
2672    ///
2673    /// The header goes through `AnyDatagramHeader::encode`, which refuses a
2674    /// header whose Object Status the framing it names cannot carry. Such a
2675    /// header errors here and nothing is sent, rather than going out as an
2676    /// ordinary payload datagram with the status quietly dropped.
2677    pub fn send_datagram(
2678        &self,
2679        header: &AnyDatagramHeader,
2680        payload: &[u8],
2681    ) -> Result<(), ConnectionError> {
2682        let mut buf = Vec::new();
2683        header.encode(&mut buf)?;
2684        buf.extend_from_slice(payload);
2685        self.emit(ClientEvent::DatagramReceived {
2686            direction: Direction::Send,
2687            header: header.clone(),
2688            payload_len: payload.len(),
2689        });
2690        self.transport.send_datagram(bytes::Bytes::from(buf))?;
2691        Ok(())
2692    }
2693
2694    /// Receive a datagram and decode its header.
2695    pub async fn recv_datagram(&self) -> Result<(AnyDatagramHeader, Bytes), ConnectionError> {
2696        let data = self.transport.recv_datagram().await?;
2697        let mut cursor = &data[..];
2698        let header = AnyDatagramHeader::decode(self.draft, &mut cursor)?;
2699        let consumed = data.len() - cursor.len();
2700        let payload = data.slice(consumed..);
2701        self.emit(ClientEvent::DatagramReceived {
2702            direction: Direction::Receive,
2703            header: header.clone(),
2704            payload_len: payload.len(),
2705        });
2706        // Refutable only in a build with more than one draft enabled;
2707        // in a single-draft build `AnyDatagramHeader` has one variant.
2708        #[allow(irrefutable_let_patterns)]
2709        if let AnyDatagramHeader::Draft17(h) = &header {
2710            if !h.permits_payload() && !payload.is_empty() {
2711                return Err(ConnectionError::PayloadOnStatusDatagram {
2712                    object_id: h.object_id.into_inner(),
2713                    payload_len: payload.len(),
2714                    status: h.object_status,
2715                });
2716            }
2717        }
2718        // A datagram is a whole object, so the connection can measure it
2719        // without help from the caller. It cannot *answer* the condition,
2720        // though: the answer is a reset of a request stream the caller holds,
2721        // so both data paths report and neither withdraws - see
2722        // `Connection::requests_to_cancel`.
2723        let meta = header.meta();
2724        self.endpoint.note_received_object(
2725            meta.track_alias,
2726            ObjectLocation { group: meta.group_id, object: meta.object_id },
2727            object_role(meta.status),
2728        )?;
2729        Ok((header, payload))
2730    }
2731
2732    // -- Accessors --------------------------------------------------
2733
2734    /// Access the underlying endpoint state machine.
2735    pub fn endpoint(&self) -> &Endpoint {
2736        &self.endpoint
2737    }
2738
2739    /// Mutable access to the endpoint state machine.
2740    pub fn endpoint_mut(&mut self) -> &mut Endpoint {
2741        &mut self.endpoint
2742    }
2743
2744    /// Returns the draft version this connection is using.
2745    pub fn draft(&self) -> DraftVersion {
2746        self.draft
2747    }
2748
2749    /// Close the session on the wire when the endpoint says a violation is
2750    /// fatal to it.
2751    ///
2752    /// [`EndpointError::session_error_code`] answers `Some` for exactly the
2753    /// errors draft-17 tells the receiver to close the session over, and the
2754    /// endpoint has already moved its own state machine to Closed by the time
2755    /// this runs. Without this step that move was purely internal: the local
2756    /// endpoint refused to start anything new while the peer, which is the one
2757    /// that broke the rule, saw a session that was still open and went on
2758    /// sending. "MUST close the session with a PROTOCOL_VIOLATION" is a
2759    /// statement about the wire, so it takes a CONNECTION_CLOSE to satisfy it.
2760    ///
2761    /// The reason phrase is the error's own `Display` text, which names the
2762    /// message and the rule rather than repeating the numeric code the close
2763    /// already carries.
2764    ///
2765    /// Errors that answer `None` are recoverable and nothing is sent.
2766    fn close_for(&self, err: &EndpointError) {
2767        if let Some(code) = err.session_error_code() {
2768            // QUIC application error codes are 62-bit; every code in this
2769            // registry is far below `u32::MAX`, and saturating rather than
2770            // truncating means a future code that is not could never be
2771            // reported as a different, assigned one.
2772            let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
2773            self.close(wire_code, err.to_string().as_bytes());
2774        }
2775    }
2776
2777    /// [`close_for`](Self::close_for), then the error unchanged, for the
2778    /// common case where the endpoint's error is also what the caller returns.
2779    fn close_if_session_fatal(&self, err: EndpointError) -> ConnectionError {
2780        self.close_for(&err);
2781        ConnectionError::Endpoint(err)
2782    }
2783
2784    /// The code to close the session with when a control message could not be
2785    /// decoded because the peer broke a rule draft-17 answers with a close.
2786    ///
2787    /// Every variant listed here comes from a sentence in the draft that names
2788    /// the consequence: the reason phrase and GOAWAY URI maxima (Sections
2789    /// 1.4.4 and 9.5), the KVP value maximum and the delta-encoded
2790    /// type overflow (Section 1.4.3), the duplicate-parameter rule (Section
2791    /// 9.3), and the Track Namespace field, count and length rules
2792    /// (Section 2.4.1). Each of those reads "MUST close the session with a
2793    /// PROTOCOL_VIOLATION". The Required Request ID Delta bound of Section 9.2 is
2794    /// the one that names a different code, INVALID_REQUIRED_REQUEST_ID.
2795    ///
2796    /// One more rule reaches this table without naming a code: "An endpoint
2797    /// that receives an unknown message type MUST close the session", stated in
2798    /// those words by all thirteen drafts. Protocol Violation is what carries
2799    /// it, as it does on every draft below this one.
2800    ///
2801    /// [`CodecError::ObjectIdOverflow`] is absent on purpose, and this is the
2802    /// one draft where that is so. Section 10.4.2 gives the same Object ID
2803    /// delta arithmetic drafts 18 and 19 give, and the codec reports the wrap
2804    /// on all three, but only those two go on to say "the endpoint MUST close
2805    /// the session with a PROTOCOL_VIOLATION". Draft-17 states no consequence,
2806    /// so the wrap stops here at a refused frame rather than a closed session.
2807    ///
2808    /// `None` for everything else, including [`CodecError::InvalidField`]. That
2809    /// variant is shared by a dozen unrelated malformations, only some of which
2810    /// the draft answers with a close, so treating it as fatal would close
2811    /// sessions the draft does not ask to be closed. Splitting it is the way to
2812    /// bring the rest of those rules under this function; widening the match is
2813    /// not.
2814    fn codec_session_error_code(
2815        err: &CodecError,
2816    ) -> Option<moqtap_codec::draft17::error_codes::SessionErrorCode> {
2817        use moqtap_codec::draft17::error_codes::SessionErrorCode;
2818        use moqtap_codec::kvp::KvpError;
2819        match err {
2820            // The declared Length disagreeing with the fields, which every
2821            // draft answers with a close. Drafts 07 through 10 name no code for
2822            // it, so it takes the one their other unnamed rules take.
2823            // A Filter Type outside the four this draft assigns, Section 5.1.2:
2824            // "An endpoint that receives a filter type other than the above MUST
2825            // close the session with PROTOCOL_VIOLATION."
2826            //
2827            // Drafts 07 through 14 carried the Filter Type as a field of
2828            // SUBSCRIBE. From draft-15 it is the first field inside the
2829            // length-prefixed filter parameter, where a codec that carries the
2830            // value as opaque bytes never reads it — the rule did not change and
2831            // the place it has to be enforced did.
2832            CodecError::InvalidFilterType(_) => Some(SessionErrorCode::ProtocolViolation),
2833            // A Fetch Type outside the three this draft assigns: "An endpoint
2834            // that receives a Fetch Type other than 0x1, 0x2 or 0x3 MUST close
2835            // the session with a PROTOCOL_VIOLATION." The value decides which
2836            // fields follow it — a Standalone fetch carries a track name and a
2837            // range where a joining fetch carries a Request ID and an offset —
2838            // so a reader that cannot name the type cannot find the end of the
2839            // message.
2840            CodecError::InvalidFetchType(_) => Some(SessionErrorCode::ProtocolViolation),
2841            CodecError::ControlMessageLengthMismatch { .. } => {
2842                Some(SessionErrorCode::ProtocolViolation)
2843            }
2844            CodecError::KeyDeltaOverflow(..)
2845            | CodecError::DuplicateParameter(_)
2846            | CodecError::TrackNameTooLong
2847            | CodecError::InvalidNamespaceTupleSize(_)
2848            | CodecError::ReasonPhraseTooLong
2849            | CodecError::GoAwayUriTooLong
2850            | CodecError::UnknownMessageType(_)
2851            | CodecError::Kvp(KvpError::ValueTooLong(_))
2852            | CodecError::EmptyNamespaceField => Some(SessionErrorCode::ProtocolViolation),
2853            CodecError::InvalidRequiredRequestIdDelta(..) => {
2854                Some(SessionErrorCode::InvalidRequiredRequestId)
2855            }
2856            // An unknown data-plane type. Drafts 17 and later split the sentence
2857            // in two: Section 3.4 for streams, Section 10 for datagrams, both
2858            // ending "MUST close the session" and neither naming a code, so both
2859            // take the one this draft's other unnamed rules take.
2860            // A Message Parameter whose value is outside the range its type
2861            // allows: FORWARD in Section 9.3.10 and GROUP_ORDER in Section 9.3.6.
2862            // Each states that a receiver "MUST close the session with
2863            // PROTOCOL_VIOLATION".
2864            CodecError::ParameterValueOutOfRange { .. } => {
2865                Some(SessionErrorCode::ProtocolViolation)
2866            }
2867            // A Track Extension or Track Property whose value is outside the
2868            // range its type allows: DEFAULT_PUBLISHER_GROUP_ORDER in Section 11.4
2869            // and DYNAMIC_GROUPS in Section 11.5.
2870            // Each states that a receiver "MUST close the session with
2871            // PROTOCOL_VIOLATION".
2872            //
2873            // A separate arm from the parameter rule above because the two
2874            // registries are separate: 0x22 is GROUP_ORDER as a parameter and
2875            // DEFAULT_PUBLISHER_GROUP_ORDER as a Track Property, and a log that
2876            // named only the number would not say which.
2877            CodecError::TrackPropertyValueOutOfRange { .. } => {
2878                Some(SessionErrorCode::ProtocolViolation)
2879            }
2880            CodecError::UnknownStreamType(_) | CodecError::UnknownDatagramType(_) => {
2881                Some(SessionErrorCode::ProtocolViolation)
2882            }
2883            // A Type inside a form this draft defines but on a list it names as
2884            // invalid: Section 10.4.2 for a subgroup header whose SUBGROUP_ID_MODE
2885            // is the reserved 0b11, Section 10.3.1 for a datagram asking to be both
2886            // an object status and an end-of-group marker. Unlike the rule above,
2887            // these two name their code outright.
2888            CodecError::InvalidTypeValue { .. } => Some(SessionErrorCode::ProtocolViolation),
2889            // A key-value pair whose value is not the serialization its own
2890            // Type defines, Section 1.4.3: "If a receiver understands a Type,
2891            // and the following Value or Length/Value does not match the
2892            // serialization defined by that Type, the receiver MUST close the
2893            // session with error code KEY_VALUE_FORMATTING_ERROR."
2894            //
2895            // Section 9.3.2 states the same answer for the one structure this
2896            // draft spells out: "If the Token structure cannot be decoded, the
2897            // receiver MUST close the Session with KEY_VALUE_FORMATTING_ERROR."
2898            //
2899            // The one rule in this table that names a code other than Protocol
2900            // Violation.
2901            CodecError::KeyValueFormatting { .. }
2902            // A filter parameter whose value is not a filter reaches the same
2903            // sentence. Drafts 15 and 16 answered it with PROTOCOL_VIOLATION
2904            // instead, on the strength of a sentence of the parameter's own that
2905            // this draft dropped; what remains is the general rule above, so the
2906            // code changed with it.
2907            | CodecError::SubscriptionFilterMalformed { .. } => {
2908                Some(SessionErrorCode::KeyValueFormattingError)
2909            }
2910            // A Message Parameter whose type this draft does not define, Section
2911            // 9.3: "All Message Parameters MUST be defined in the negotiated
2912            // version of MOQT or negotiated via Setup Options. An endpoint that
2913            // receives an unknown Message Parameter MUST close the session with
2914            // PROTOCOL_VIOLATION."
2915            //
2916            // One namespace only. This draft also says a receiver ignores an
2917            // unrecognised Setup Option, so an unknown type in a SETUP is carried and
2918            // the codec never raises this for one.
2919            CodecError::UnknownMessageParameter(_) => Some(SessionErrorCode::ProtocolViolation),
2920            // A Message Parameter in a message type its own definition does not
2921            // name, Section 9.3.1: "Each Message Parameter definition indicates
2922            // the message types in which it can appear. If it appears in some
2923            // other type of message, the receiving endpoint MUST close the
2924            // connection with a PROTOCOL_VIOLATION."
2925            //
2926            // Draft-16 and every draft before it end that same sentence "it MUST
2927            // be ignored", so this is a rule whose answer reverses rather than
2928            // one that arrives.
2929            CodecError::ParameterOutOfScope { .. } => Some(SessionErrorCode::ProtocolViolation),
2930            // Everything this draft does not answer, named rather than swept up
2931            // by a wildcard. The arm is exhaustive deliberately: a new
2932            // `CodecError` variant will not compile until it has been placed on
2933            // one side or the other, on this draft, which is the decision a `_`
2934            // arm makes silently and invisibly on all thirteen at once.
2935            //
2936            // Adding one variant to `CodecError` was tried, and produces
2937            // thirteen `E0004`s, one per draft, each naming the variant that has
2938            // nowhere to go. That is the whole mechanism.
2939            //
2940            // The nesting stops at `VarInt`, whose variants report how the bytes
2941            // ran out rather than a rule an endpoint states, so there is nothing
2942            // in it for a draft to answer. `Kvp` is spelled out because it does
2943            // carry one.
2944            // Neither field exists from draft-15 on. Forwarding became the
2945            // FORWARD parameter, which carries the same rule in a different
2946            // shape and is answered above under its own variant; Content Exists
2947            // became the presence or absence of a LARGEST_OBJECT parameter.
2948            CodecError::InvalidForward(_)
2949            | CodecError::InvalidContentExists(_)
2950            | CodecError::UnexpectedEnd
2951            | CodecError::MessageTooLong(_)
2952            | CodecError::VarInt(_)
2953            | CodecError::InvalidField
2954            | CodecError::InvalidRange(..)
2955            | CodecError::ParameterLengthMismatch(_)
2956            | CodecError::EndOfTrackObjectId(_)
2957            | CodecError::ParametersOutOfOrder(..)
2958            | CodecError::ObjectIdOverflow(..)
2959            | CodecError::ExtensionsOnNonExistentObject(_)
2960            // This draft introduced the End Group Delta and states nothing
2961            // about the sum leaving the number space. Drafts 18 and 19 add, at
2962            // draft-18 Section 5.1.2, "If the resulting Group ID would be
2963            // greater than 2^64 - 1, the endpoint MUST close the session with a
2964            // PROTOCOL_VIOLATION" and answer it; refusing here would close a
2965            // session over a sentence this draft does not have.
2966            | CodecError::FilterEndGroupOverflow { .. }
2967            // The object payload rule, Section 10.2.1.1: "Any object with a status
2968            // code other than zero MUST have an empty payload." A MUST on the
2969            // sender with no receiver action named anywhere — the "SHOULD be
2970            // treated as a protocol error" in the same paragraph belongs to the
2971            // sentence before it, which is about a status value this draft does
2972            // not assign — so an object carrying a payload it may not is refused
2973            // and the session stays open.
2974            //
2975            // That was already the answer. The bytes used to arrive as
2976            // `InvalidField`, which is on this side too; naming the rule changes
2977            // nothing a peer can observe and makes the decision legible.
2978            | CodecError::PayloadNotPermitted { .. }
2979            | CodecError::UnsupportedDraft(_)
2980            | CodecError::Kvp(
2981                KvpError::MissingLength | KvpError::UnexpectedEnd | KvpError::VarInt(_),
2982            ) => None,
2983        }
2984    }
2985
2986    /// Close the session on the wire when a decode failure is one draft-17
2987    /// answers with a close, and hand the error back unchanged.
2988    ///
2989    /// The codec's counterpart to
2990    /// [`close_if_session_fatal`](Self::close_if_session_fatal). Without it
2991    /// every bound the decoder enforces would stop at *this endpoint refused the
2992    /// frame* while the peer, which is the one that broke the rule, saw a
2993    /// session that was still open and went on sending. "MUST close the session
2994    /// with a PROTOCOL_VIOLATION" is a statement about the wire.
2995    fn close_for_codec(&self, err: ConnectionError) -> ConnectionError {
2996        if let ConnectionError::Codec(inner) = &err {
2997            if let Some(code) = Self::codec_session_error_code(inner) {
2998                // QUIC application error codes are 62-bit; every code in this
2999                // registry is far below `u32::MAX`, and saturating rather than
3000                // truncating means a future code that is not could never be
3001                // reported as a different, assigned one.
3002                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
3003                self.close(wire_code, inner.to_string().as_bytes());
3004            }
3005        }
3006        err
3007    }
3008
3009    /// Name every request whose stream the caller must reset, for a track a
3010    /// data path has just found malformed.
3011    ///
3012    /// Section 2.4.2 answers its whole list of conditions at once: "it MUST
3013    /// cancel any corresponding subscription or fetches for that Track from
3014    /// that publisher". On this draft cancelling a request is a transport
3015    /// operation rather than a message — Section 3.3.1: "Implementations SHOULD cancel requests
3016    /// by abruptly terminating any directions of a stream that are still open
3017    /// using RESET_STREAM / RESET_STREAM_AT or STOP_SENDING."
3018    ///
3019    /// A SHOULD on this draft and on draft-18; draft-19 drops the keyword and
3020    /// states it outright.
3021    ///
3022    /// # Why this returns ids instead of doing it
3023    ///
3024    /// Because the streams are the caller's. Every request on this draft lives
3025    /// at the front of a bidirectional stream of its own, and
3026    /// [`Connection::recv_on_request_stream`] hands that stream back as a
3027    /// [`RequestStream`]. There is no handle here to reset. So the connection
3028    /// does the half it can — note the track, and work out which requests
3029    /// receive it — and the caller passes each id to
3030    /// [`Connection::cancel_request_stream`], which resets the stream *and*
3031    /// moves the endpoint's record.
3032    ///
3033    /// **This is the one place in this crate where the two halves of an answer
3034    /// are split across the API boundary**, and it is the draft that splits
3035    /// them: drafts 12 through 16 answer with a control message, which the
3036    /// connection owns, so `withdraw_for_data_stream` there does the whole
3037    /// thing.
3038    ///
3039    /// # Both data paths come here
3040    ///
3041    /// Unlike the drafts that answer with a message, where a datagram is read
3042    /// through the connection and answers itself. Here neither path can, for
3043    /// the same reason, so there is one entry point rather than two. Pass it
3044    /// whatever error a read returned; anything that is not this condition
3045    /// gives back an empty list.
3046    ///
3047    /// Empty is not "the track was fine" — it is also what an alias no live
3048    /// binding names gives, and what a track this endpoint only publishes
3049    /// gives.
3050    pub fn requests_to_cancel(&self, err: &ConnectionError) -> Vec<VarInt> {
3051        let ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject { alias, .. }) = err
3052        else {
3053            return Vec::new();
3054        };
3055        self.endpoint
3056            .requests_for_malformed_track(*alias, MalformedTrackCondition::ObjectPastFinalObject)
3057    }
3058
3059    /// Close the session when a failure raised while reading a *data* stream is
3060    /// one draft-17 answers with a close. Reports whether it closed.
3061    ///
3062    /// [`recv_control`](Self::recv_control) does this for itself, because it
3063    /// owns both the stream and the connection. A data stream does not:
3064    /// [`accept_subgroup_stream`](Self::accept_subgroup_stream) hands the caller
3065    /// a [`FramedRecvStream`], which holds no connection and so cannot close
3066    /// one, and the reads that raise these failures happen there. The caller is
3067    /// the only party holding both halves, which is what this is for.
3068    ///
3069    /// Splitting it this way rather than closing inside the reader keeps a
3070    /// caller that is deliberately permissive — a tool reproducing a capture,
3071    /// say — able to read a violating stream and report it without tearing the
3072    /// session down. The rule is stated at endpoints, and this is where an
3073    /// endpoint decides it is one.
3074    ///
3075    /// On this draft only one rule reaches here: properties beside a status
3076    /// that is not Normal, Section 10.2.1.2. Drafts 18 and 19 also answer the
3077    /// Object ID delta wrap of their Section 11.4.2, which draft-17 describes
3078    /// without stating a consequence — see
3079    /// `codec_session_error_code`.
3080    pub fn close_for_data_stream(&self, err: &ConnectionError) -> bool {
3081        // As in `close_for_codec`: saturate rather than truncate, so a future
3082        // code above `u32::MAX` is never reported as a different assigned one.
3083        let protocol_violation = u32::try_from(
3084            moqtap_codec::draft17::error_codes::SessionErrorCode::ProtocolViolation.as_u64(),
3085        )
3086        .unwrap_or(u32::MAX);
3087        match err {
3088            ConnectionError::Codec(inner) => {
3089                let Some(code) = Self::codec_session_error_code(inner) else { return false };
3090                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
3091                self.close(wire_code, inner.to_string().as_bytes());
3092                true
3093            }
3094            // Not a `Codec` failure: the codec decodes such an Object without
3095            // complaint, because the frame is well formed. It is being an
3096            // endpoint that makes it a violation, so the variant is this
3097            // crate's own and the mapping table above never sees it.
3098            ConnectionError::PropertiesOnNonNormalStatus { .. } => {
3099                self.close(protocol_violation, err.to_string().as_bytes());
3100                true
3101            }
3102            _ => false,
3103        }
3104    }
3105
3106    /// Close the connection.
3107    pub fn close(&self, code: u32, reason: &[u8]) {
3108        self.emit(ClientEvent::Closed { code, reason: reason.to_vec() });
3109        self.transport.close(code, reason);
3110    }
3111}
3112
3113#[cfg(test)]
3114mod tests {
3115    use super::*;
3116
3117    /// Draft-17 uses MoQT's variable-length integer, whose length is the
3118    /// number of leading 1 bits in the first byte, not RFC 9000's two-bit
3119    /// prefix. Control framing measures the type field with it before any
3120    /// bytes past the first have arrived.
3121    #[test]
3122    fn varint_len_follows_the_moqt_encoding() {
3123        let draft = DraftVersion::Draft17;
3124        assert_eq!(draft.varint_len(0x00), 1);
3125        assert_eq!(draft.varint_len(0x7F), 1);
3126        assert_eq!(draft.varint_len(0x80), 2);
3127        assert_eq!(draft.varint_len(0xBF), 2);
3128        assert_eq!(draft.varint_len(0xC0), 3);
3129        assert_eq!(draft.varint_len(0xFF), 9);
3130        // SETUP's type id, 0x2F00, is two bytes here and four under RFC 9000.
3131        assert_eq!(draft.varint_len(0xAF), 2);
3132    }
3133
3134    #[test]
3135    fn client_config_alpn_quic_draft17() {
3136        let config = ClientConfig {
3137            draft: DraftVersion::Draft17,
3138            transport: TransportType::Quic,
3139            skip_cert_verification: false,
3140            ca_certs: Vec::new(),
3141            setup_parameters: Vec::new(),
3142        };
3143        assert_eq!(config.alpn(), vec![b"moqt-17".to_vec()]);
3144    }
3145
3146    #[test]
3147    fn client_config_alpn_webtransport() {
3148        let config = ClientConfig {
3149            draft: DraftVersion::Draft17,
3150            transport: TransportType::WebTransport { url: "https://example.com".to_string() },
3151            skip_cert_verification: false,
3152            ca_certs: Vec::new(),
3153            setup_parameters: Vec::new(),
3154        };
3155        assert_eq!(config.alpn(), vec![b"h3".to_vec()]);
3156    }
3157
3158    /// `MOQT_ALPN` is the ALPN a client configured for this draft offers.
3159    ///
3160    /// Putting `moq-00` back — the value this constant held on all five of
3161    /// drafts 15-19 — fails with:
3162    ///
3163    /// ```text
3164    /// assertion `left == right` failed: MOQT_ALPN is "moq-00"; a draft-19 client offers ["moqt-19"]
3165    /// ```
3166    #[test]
3167    fn moqt_alpn_is_the_one_a_client_offers() {
3168        // A literal on its own is what let this constant keep `moq-00` for
3169        // five drafts after draft-15 stopped using it, so the value is
3170        // checked against what a client configured for this draft actually
3171        // puts on the wire, and only then against the literal.
3172        let config = ClientConfig {
3173            draft: DraftVersion::Draft17,
3174            transport: TransportType::Quic,
3175            skip_cert_verification: false,
3176            ca_certs: Vec::new(),
3177            setup_parameters: Vec::new(),
3178        };
3179        assert_eq!(
3180            config.alpn(),
3181            vec![MOQT_ALPN.to_vec()],
3182            "MOQT_ALPN is {:?}; a draft-{} client offers {:?}",
3183            String::from_utf8_lossy(MOQT_ALPN),
3184            17,
3185            config
3186                .alpn()
3187                .iter()
3188                .map(|a| String::from_utf8_lossy(a).into_owned())
3189                .collect::<Vec<_>>(),
3190        );
3191        assert_eq!(MOQT_ALPN, b"moqt-17");
3192    }
3193
3194    /// Draft-17 Section 3.3 names six message types a bidirectional stream
3195    /// may begin with, and no others. The set is checked against the raw
3196    /// numbers this draft's registry assigns rather than against the names,
3197    /// so a variant that is renumbered — SUBSCRIBE_NAMESPACE moved from 0x11
3198    /// to 0x50 between draft-17 and draft-18 — is caught even though the
3199    /// spelling did not change.
3200    ///
3201    /// Every type this draft assigns is classified: the loop walks the whole
3202    /// assigned range and asks the classifier about each one it finds.
3203    ///
3204    /// Moving `MessageType::GoAway` into the true arm fails with:
3205    ///
3206    /// ```text
3207    /// assertion `left == right` failed: the types that open a request stream are [3, 6, 13, 16, 17, 22, 29]; draft-17 Section 3.3 names [3, 6, 13, 17, 22, 29]
3208    ///   left: [3, 6, 13, 16, 17, 22, 29]
3209    ///  right: [3, 6, 13, 17, 22, 29]
3210    /// ```
3211    #[test]
3212    fn only_six_message_types_open_a_request_stream() {
3213        // TRACK_STATUS, SUBSCRIBE, PUBLISH, FETCH, PUBLISH_NAMESPACE and
3214        // SUBSCRIBE_NAMESPACE, written as the numbers draft-17 assigns them.
3215        let mut expected = vec![0x0D, 0x03, 0x1D, 0x16, 0x06, 0x11];
3216        expected.sort_unstable();
3217
3218        let mut opens = Vec::new();
3219        for id in 0..=CONTROL_STREAM_TYPE {
3220            if let Some(ty) = MessageType::from_id(id) {
3221                if starts_a_request_stream(ty) {
3222                    opens.push(id);
3223                }
3224            }
3225        }
3226        opens.sort_unstable();
3227
3228        assert_eq!(
3229            opens, expected,
3230            "the types that open a request stream are {opens:?}; \
3231             draft-17 Section 3.3 names {expected:?}"
3232        );
3233    }
3234
3235    /// The kind a request helper labels its stream with must name the message
3236    /// that helper actually writes, and the check is made against the type
3237    /// varint the encoded message leads with — the byte a peer reads to
3238    /// decide whether the bidirectional stream is legal.
3239    ///
3240    /// This is the mislabelling a port to another draft is most likely to
3241    /// introduce, because the numbers move between drafts while the names do
3242    /// not.
3243    ///
3244    /// Pointing `RequestKind::Fetch` at `MessageType::FetchOk` fails with:
3245    ///
3246    /// ```text
3247    /// assertion `left == right` failed: Fetch is labelled 24 but its message leads with 22
3248    ///   left: 24
3249    ///  right: 22
3250    /// ```
3251    #[test]
3252    fn each_request_kind_labels_the_message_its_helper_writes() {
3253        use crate::draft17::endpoint::Endpoint;
3254        use moqtap_codec::draft17::message::Setup;
3255
3256        let v = |n: u64| VarInt::from_u64(n).unwrap();
3257        let ns = TrackNamespace(vec![b"ns".to_vec()]);
3258
3259        let mut ep = Endpoint::new(Role::Client);
3260        ep.connect().unwrap();
3261        let _ = ep.send_setup(vec![]).unwrap();
3262        ep.receive_setup(&Setup { options: vec![] }).unwrap();
3263
3264        let (sub_id, subscribe) = ep.subscribe(ns.clone(), b"t".to_vec(), vec![]).unwrap();
3265        let built = vec![
3266            (RequestKind::Subscribe, subscribe),
3267            (
3268                RequestKind::Fetch,
3269                ep.fetch(ns.clone(), b"t".to_vec(), v(0), v(0), v(1), v(1), vec![]).unwrap().1,
3270            ),
3271            (RequestKind::Fetch, ep.joining_fetch(sub_id, v(2), Vec::new()).unwrap().1),
3272            (
3273                RequestKind::SubscribeNamespace,
3274                ep.subscribe_namespace(ns.clone(), v(0), vec![]).unwrap().1,
3275            ),
3276            (RequestKind::PublishNamespace, ep.publish_namespace(ns.clone(), vec![]).unwrap().1),
3277            (
3278                RequestKind::TrackStatus,
3279                ep.track_status(ns.clone(), b"t".to_vec(), vec![]).unwrap().1,
3280            ),
3281            (
3282                RequestKind::Publish,
3283                ep.publish(ns.clone(), b"t".to_vec(), v(7), vec![], vec![]).unwrap().1,
3284            ),
3285        ];
3286
3287        for (kind, msg) in built {
3288            let mut wire = Vec::new();
3289            msg.encode(&mut wire).unwrap();
3290            let mut cursor = &wire[..];
3291            let on_the_wire =
3292                DraftVersion::Draft17.decode_varint(&mut cursor).unwrap().into_inner();
3293            assert_eq!(
3294                kind.message_type().id(),
3295                on_the_wire,
3296                "{kind:?} is labelled {} but its message leads with {on_the_wire}",
3297                kind.message_type().id(),
3298            );
3299            assert!(
3300                starts_a_request_stream(kind.message_type()),
3301                "{kind:?} labels a message type that may not begin a bidirectional stream",
3302            );
3303        }
3304    }
3305
3306    /// The classifier the accept path runs and the one
3307    /// [`Connection::send_control`] runs must answer alike for every message
3308    /// type this draft assigns, or a message could be refused on the control
3309    /// stream and refused again as the opening of a request stream — leaving
3310    /// no legal place for it.
3311    ///
3312    /// Dropping `MessageType::Publish` to `None` in `from_message_type` fails
3313    /// with:
3314    ///
3315    /// ```text
3316    /// assertion `left == right` failed: type 29 opens a request stream but from_message_type calls it None
3317    ///   left: false
3318    ///  right: true
3319    /// ```
3320    #[test]
3321    fn the_two_request_stream_classifiers_agree() {
3322        let mut classified = 0;
3323        for id in 0..=CONTROL_STREAM_TYPE {
3324            let Some(ty) = MessageType::from_id(id) else { continue };
3325            classified += 1;
3326            let kind = RequestKind::from_message_type(ty);
3327            assert_eq!(
3328                kind.is_some(),
3329                starts_a_request_stream(ty),
3330                "type {id} {} a request stream but from_message_type calls it {kind:?}",
3331                if starts_a_request_stream(ty) { "opens" } else { "does not open" },
3332            );
3333            if let Some(kind) = kind {
3334                assert_eq!(
3335                    kind.message_type(),
3336                    ty,
3337                    "from_message_type sent type {id} to {kind:?}, which names a different message",
3338                );
3339            }
3340        }
3341        assert!(classified > 6, "the loop found only {classified} assigned message types");
3342    }
3343
3344    /// A stream this endpoint opened is cancelled when its handle is dropped;
3345    /// one the peer opened is reset as unserved. Both codes are on the wire,
3346    /// so they may not be the same number.
3347    #[test]
3348    fn the_two_abandonment_codes_are_distinct() {
3349        assert_eq!(REQUEST_CANCELLED, 0x1);
3350        assert_eq!(REQUEST_UNANSWERED, 0x0);
3351        assert_ne!(
3352            REQUEST_CANCELLED, REQUEST_UNANSWERED,
3353            "a peer cannot tell a rejected request from a dropped one if both reset with the same code",
3354        );
3355    }
3356
3357    #[test]
3358    fn transport_type_debug() {
3359        let quic = TransportType::Quic;
3360        assert!(format!("{quic:?}").contains("Quic"));
3361
3362        let wt = TransportType::WebTransport { url: "https://example.com".to_string() };
3363        assert!(format!("{wt:?}").contains("WebTransport"));
3364    }
3365}
3366
3367#[cfg(test)]
3368mod accept_on_the_wire {
3369    //! The accept path against a real QUIC peer.
3370    //!
3371    //! The last of the request-stream drafts to get one. What a peer can see —
3372    //! which stream a response goes out on, and whether one is written at all —
3373    //! is not observable from an endpoint held on its own.
3374
3375    use super::*;
3376    use std::sync::Arc;
3377
3378    use std::net::SocketAddr;
3379    use std::time::Duration;
3380
3381    use moqtap_codec::draft17::message::{PublishOk, Setup};
3382
3383    /// Long enough that a loaded machine cannot fail a test that would
3384    /// otherwise pass, short enough that a hang is reported rather than run to
3385    /// the harness timeout.
3386    const PATIENCE: Duration = Duration::from_secs(10);
3387
3388    fn v(n: u64) -> VarInt {
3389        VarInt::from_u64(n).unwrap()
3390    }
3391
3392    fn ns() -> TrackNamespace {
3393        TrackNamespace(vec![b"live".to_vec()])
3394    }
3395
3396    fn encode(msg: ControlMessage) -> Vec<u8> {
3397        let mut buf = Vec::new();
3398        AnyControlMessage::Draft17(msg).encode(&mut buf).expect("encode");
3399        buf
3400    }
3401
3402    fn request_update(id: u64) -> ControlMessage {
3403        ControlMessage::RequestUpdate(moqtap_codec::draft17::message::RequestUpdate {
3404            request_id: v(id),
3405            required_request_id_delta: v(0),
3406            parameters: vec![],
3407        })
3408    }
3409
3410    fn request_error() -> RequestError {
3411        RequestError { error_code: v(0x1), retry_interval: v(0), reason_phrase: b"no".to_vec() }
3412    }
3413
3414    fn init_crypto() {
3415        let _ = rustls::crypto::ring::default_provider().install_default();
3416    }
3417
3418    /// A quinn server on a loopback port, offering this draft's ALPN.
3419    fn server_endpoint() -> (quinn::Endpoint, SocketAddr) {
3420        use rcgen::{CertificateParams, KeyPair, PKCS_ECDSA_P256_SHA256};
3421        use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
3422
3423        let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).expect("keypair");
3424        let params = CertificateParams::new(vec!["localhost".into()]).expect("params");
3425        let cert = params.self_signed(&key_pair).expect("self-sign");
3426        let cert_der = CertificateDer::from(cert.der().to_vec());
3427        let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_pair.serialize_der()));
3428
3429        let mut server_crypto = rustls::ServerConfig::builder()
3430            .with_no_client_auth()
3431            .with_single_cert(vec![cert_der], key_der)
3432            .expect("server cert");
3433        server_crypto.alpn_protocols = vec![DraftVersion::Draft17.quic_alpn().to_vec()];
3434        let server_crypto =
3435            quinn::crypto::rustls::QuicServerConfig::try_from(server_crypto).expect("quic crypto");
3436        let server_config = quinn::ServerConfig::with_crypto(Arc::new(server_crypto));
3437        let endpoint = quinn::Endpoint::server(server_config, "127.0.0.1:0".parse().unwrap())
3438            .expect("bind server");
3439        let addr = endpoint.local_addr().expect("local_addr");
3440        (endpoint, addr)
3441    }
3442
3443    async fn connect_client(addr: SocketAddr) -> Result<Connection, ConnectionError> {
3444        Connection::connect(
3445            &addr.to_string(),
3446            ClientConfig {
3447                draft: DraftVersion::Draft17,
3448                transport: TransportType::Quic,
3449                skip_cert_verification: true,
3450                ca_certs: Vec::new(),
3451                setup_parameters: Vec::new(),
3452            },
3453        )
3454        .await
3455    }
3456
3457    /// The peer's half of the setup exchange: read the client's SETUP off its
3458    /// unidirectional control stream, answer with one of our own.
3459    ///
3460    /// Both control streams are handed back so they stay open for the
3461    /// connection's life. Dropping a quinn receive stream sends STOP_SENDING
3462    /// and dropping a send stream resets it, either of which would look to the
3463    /// client like the control plane failing.
3464    async fn peer_handshake(
3465        endpoint: &quinn::Endpoint,
3466    ) -> (quinn::Connection, quinn::SendStream, quinn::RecvStream) {
3467        let conn = endpoint.accept().await.expect("accept").await.expect("tls handshake");
3468        let mut client_control = conn.accept_uni().await.expect("accept_uni");
3469        let mut seen = Vec::new();
3470        let mut chunk = [0u8; 1024];
3471        while seen.len() < 3 {
3472            match client_control.read(&mut chunk).await.expect("read SETUP") {
3473                Some(n) => seen.extend_from_slice(&chunk[..n]),
3474                None => break,
3475            }
3476        }
3477        assert!(!seen.is_empty(), "the client sent no SETUP");
3478        let mut ours = conn.open_uni().await.expect("open_uni");
3479        ours.write_all(&encode(ControlMessage::Setup(Setup { options: Vec::new() })))
3480            .await
3481            .expect("write SETUP");
3482        (conn, ours, client_control)
3483    }
3484
3485    /// A connected client and the peer holding the other end.
3486    struct Loopback {
3487        conn: Connection,
3488        peer: quinn::Connection,
3489        _endpoint: quinn::Endpoint,
3490        _control_send: quinn::SendStream,
3491        _control_recv: quinn::RecvStream,
3492    }
3493
3494    async fn loopback() -> Loopback {
3495        init_crypto();
3496        let (endpoint, addr) = server_endpoint();
3497        let (client, peer) = tokio::join!(connect_client(addr), peer_handshake(&endpoint));
3498        let (peer, control_send, control_recv) = peer;
3499        Loopback {
3500            conn: client.expect("client connect"),
3501            peer,
3502            _endpoint: endpoint,
3503            _control_send: control_send,
3504            _control_recv: control_recv,
3505        }
3506    }
3507
3508    /// One handshake, several ALPNs offered, and the server's pick reported.
3509    ///
3510    /// This is the capability `connect` cannot express: it derives its ALPN
3511    /// from the draft it was told to use, so it can only ever confirm a guess.
3512    /// The server here speaks draft-17 alone, and the dial that finds it offers
3513    /// 19 and 18 first — so a caller learns the draft from one handshake
3514    /// instead of dialling once per candidate and discarding the failures.
3515    #[tokio::test]
3516    async fn a_dial_offering_several_alpns_reports_the_one_chosen() {
3517        init_crypto();
3518        let (server, addr) = server_endpoint();
3519        let accepting = tokio::spawn(async move {
3520            let incoming = server.accept().await.expect("accept");
3521            let conn = incoming.await.expect("tls handshake");
3522            // Hold the server end open until the assertions have run.
3523            tokio::time::sleep(PATIENCE).await;
3524            drop(conn);
3525        });
3526
3527        let (transport, negotiated) = crate::transport::dial_quic(
3528            &addr.to_string(),
3529            &crate::transport::QuicDialOptions {
3530                skip_cert_verification: true,
3531                ca_certs: Vec::new(),
3532                alpn: vec![
3533                    DraftVersion::Draft19.quic_alpn().to_vec(),
3534                    DraftVersion::Draft18.quic_alpn().to_vec(),
3535                    DraftVersion::Draft17.quic_alpn().to_vec(),
3536                ],
3537            },
3538        )
3539        .await
3540        .expect("dial");
3541
3542        let negotiated = negotiated.expect("the server selected no ALPN");
3543        assert_eq!(
3544            negotiated.as_slice(),
3545            DraftVersion::Draft17.quic_alpn(),
3546            "the server's pick was not reported back"
3547        );
3548        assert_eq!(
3549            DraftVersion::from_alpn(&negotiated),
3550            Some(DraftVersion::Draft17),
3551            "the reported ALPN does not name the draft that answered"
3552        );
3553
3554        drop(transport);
3555        accepting.abort();
3556    }
3557
3558    /// `adopt` completes a session on a transport somebody else dialled.
3559    ///
3560    /// The pair above and this one are the whole point of the split: dial once
3561    /// offering everything, read which draft answered, then bring the *same*
3562    /// connection to that draft's module. Nothing is closed and redialled.
3563    #[tokio::test]
3564    async fn adopt_runs_the_setup_handshake_on_a_transport_it_did_not_dial() {
3565        init_crypto();
3566        let (endpoint, addr) = server_endpoint();
3567
3568        let dial = async {
3569            let (transport, negotiated) = crate::transport::dial_quic(
3570                &addr.to_string(),
3571                &crate::transport::QuicDialOptions {
3572                    skip_cert_verification: true,
3573                    ca_certs: Vec::new(),
3574                    alpn: vec![
3575                        DraftVersion::Draft18.quic_alpn().to_vec(),
3576                        DraftVersion::Draft17.quic_alpn().to_vec(),
3577                    ],
3578                },
3579            )
3580            .await
3581            .expect("dial");
3582
3583            // The draft is chosen from the answer, not assumed beforehand.
3584            assert_eq!(
3585                DraftVersion::from_alpn(&negotiated.expect("no ALPN")),
3586                Some(DraftVersion::Draft17)
3587            );
3588
3589            Connection::adopt(
3590                transport,
3591                ClientConfig {
3592                    draft: DraftVersion::Draft17,
3593                    transport: TransportType::Quic,
3594                    skip_cert_verification: true,
3595                    ca_certs: Vec::new(),
3596                    setup_parameters: Vec::new(),
3597                },
3598            )
3599            .await
3600        };
3601
3602        let (client, peer) = tokio::join!(dial, peer_handshake(&endpoint));
3603        let conn = client.expect("adopt");
3604        assert_eq!(conn.draft(), DraftVersion::Draft17);
3605
3606        drop(conn);
3607        drop(peer);
3608    }
3609
3610    fn framed(recv: quinn::RecvStream) -> FramedRecvStream {
3611        FramedRecvStream::new(RecvStream::Quic(recv), DraftVersion::Draft17)
3612    }
3613
3614    /// Read one control message the client wrote, failing rather than hanging.
3615    async fn next_control(recv: &mut FramedRecvStream) -> ControlMessage {
3616        let (any, _) = tokio::time::timeout(PATIENCE, recv.read_control(false))
3617            .await
3618            .expect("the client wrote nothing")
3619            .expect("read control");
3620        match any {
3621            AnyControlMessage::Draft17(msg) => msg,
3622            #[allow(unreachable_patterns)]
3623            other => panic!("expected a draft-17 message, got {other:?}"),
3624        }
3625    }
3626
3627    /// An update on a PUBLISH this endpoint sent is answered here.
3628    ///
3629    /// Section 9.10 names the one case where a requester answers rather than
3630    /// asks: "A subscriber can also send REQUEST_UPDATE to modify parameters
3631    /// of a subscription established with PUBLISH." The receiver of that
3632    /// update "MUST respond with exactly one REQUEST_OK or REQUEST_ERROR
3633    /// message indicating if the update was successful", and on a PUBLISH this
3634    /// endpoint sent, the receiver is this endpoint.
3635    ///
3636    /// # What it catches
3637    ///
3638    /// Restoring the origin guard on this draft's `respond`, so that no
3639    /// response is written on a stream this endpoint opened:
3640    ///
3641    /// ```text
3642    /// the subscriber's update is this endpoint's to answer:
3643    /// RespondedToOwnRequest(0)
3644    /// ```
3645    ///
3646    /// It reddens this gate and the one below it, on draft-17's own line, and
3647    /// nothing else in the client or the proxy.
3648    #[tokio::test]
3649    async fn an_update_on_a_publish_we_sent_is_answered_here() {
3650        let mut lb = loopback().await;
3651
3652        let mut outbound =
3653            lb.conn.publish(ns(), b"video".to_vec(), v(7), vec![], vec![]).await.expect("publish");
3654        let (mut their_send, their_recv) = tokio::time::timeout(PATIENCE, lb.peer.accept_bi())
3655            .await
3656            .expect("the client opened no request stream")
3657            .expect("accept_bi");
3658        let mut their_recv = framed(their_recv);
3659        assert!(matches!(next_control(&mut their_recv).await, ControlMessage::Publish(_)));
3660
3661        // The subscriber accepts the publication, then updates it.
3662        their_send
3663            .write_all(&encode(ControlMessage::PublishOk(PublishOk { parameters: vec![] })))
3664            .await
3665            .expect("write PUBLISH_OK");
3666        let msg = tokio::time::timeout(PATIENCE, lb.conn.recv_on_request_stream(&mut outbound))
3667            .await
3668            .expect("no PUBLISH_OK arrived")
3669            .expect("read PUBLISH_OK");
3670        assert!(matches!(msg, ControlMessage::PublishOk(_)), "{msg:?}");
3671
3672        their_send.write_all(&encode(request_update(0))).await.expect("write REQUEST_UPDATE");
3673        let msg = tokio::time::timeout(PATIENCE, lb.conn.recv_on_request_stream(&mut outbound))
3674            .await
3675            .expect("no REQUEST_UPDATE arrived")
3676            .expect("read REQUEST_UPDATE");
3677        assert!(matches!(msg, ControlMessage::RequestUpdate(_)), "{msg:?}");
3678
3679        lb.conn
3680            .respond_ok(&mut outbound, RequestOk { parameters: vec![] })
3681            .await
3682            .expect("the subscriber's update is this endpoint's to answer");
3683        assert!(matches!(next_control(&mut their_recv).await, ControlMessage::RequestOk(_)));
3684
3685        // The publication is untouched by the update, and the ending it still
3686        // owes goes out without complaint. Draft-19 keeps that obligation on
3687        // the request stream and can be asked; this draft does not, so the
3688        // ending being accepted is the observation.
3689        lb.conn
3690            .publish_done(&mut outbound, v(0), v(0), Vec::new())
3691            .await
3692            .expect("an accepted update leaves the ending free");
3693    }
3694
3695    /// Refusing that update owes the same ending as refusing any other.
3696    ///
3697    /// Section 9.10.1: "When a subscription update is unsuccessful, the
3698    /// publisher MUST also terminate the subscription with PUBLISH_DONE with
3699    /// error code UPDATE_FAILED." Drafts 18 and 19 reword it around
3700    /// REQUEST_UPDATE; the obligation is the same one. The publisher of a
3701    /// subscription established
3702    /// with PUBLISH is the endpoint that sent it, and the ending goes on the
3703    /// stream that endpoint opened rather than on one the peer opened.
3704    ///
3705    /// # What it catches
3706    ///
3707    /// The same cut as the gate above, restoring the origin guard:
3708    ///
3709    /// ```text
3710    /// refuse the subscriber's update: RespondedToOwnRequest(0)
3711    /// ```
3712    ///
3713    /// And narrowing the refusal's debt back to a peer's SUBSCRIBE, so the
3714    /// endpoint that sent the PUBLISH owes nothing for refusing an update on
3715    /// it — after which the ending goes out under any status and the stream
3716    /// has already been finished:
3717    ///
3718    /// ```text
3719    /// transport error: write error: closed stream
3720    /// ```
3721    ///
3722    /// That second cut reddens this gate alone here, where on drafts 18 and 19
3723    /// it also takes the namespace close with it. Those are not rules of this
3724    /// draft, so there is nothing else on the same predicate to break.
3725    #[tokio::test]
3726    async fn a_refused_update_on_a_publish_we_sent_owes_its_ending() {
3727        let mut lb = loopback().await;
3728
3729        let mut outbound =
3730            lb.conn.publish(ns(), b"video".to_vec(), v(7), vec![], vec![]).await.expect("publish");
3731        let (mut their_send, their_recv) = tokio::time::timeout(PATIENCE, lb.peer.accept_bi())
3732            .await
3733            .expect("the client opened no request stream")
3734            .expect("accept_bi");
3735        let mut their_recv = framed(their_recv);
3736        assert!(matches!(next_control(&mut their_recv).await, ControlMessage::Publish(_)));
3737
3738        their_send
3739            .write_all(&encode(ControlMessage::PublishOk(PublishOk { parameters: vec![] })))
3740            .await
3741            .expect("write PUBLISH_OK");
3742        tokio::time::timeout(PATIENCE, lb.conn.recv_on_request_stream(&mut outbound))
3743            .await
3744            .expect("no PUBLISH_OK arrived")
3745            .expect("read PUBLISH_OK");
3746
3747        their_send.write_all(&encode(request_update(0))).await.expect("write REQUEST_UPDATE");
3748        tokio::time::timeout(PATIENCE, lb.conn.recv_on_request_stream(&mut outbound))
3749            .await
3750            .expect("no REQUEST_UPDATE arrived")
3751            .expect("read REQUEST_UPDATE");
3752
3753        lb.conn
3754            .respond_error(&mut outbound, request_error())
3755            .await
3756            .expect("refuse the subscriber's update");
3757        assert!(matches!(next_control(&mut their_recv).await, ControlMessage::RequestError(_)));
3758
3759        // The ending is owed under one status, and asking for another leaves
3760        // the publication exactly where it was rather than half ended.
3761        let err = lb
3762            .conn
3763            .publish_done(&mut outbound, v(0), v(0), Vec::new())
3764            .await
3765            .expect_err("a refused update fixes the status of the ending");
3766        assert!(
3767            matches!(
3768                err,
3769                ConnectionError::Endpoint(EndpointError::WrongUpdateFailureStatus {
3770                    request: 0,
3771                    required: 0x8,
3772                })
3773            ),
3774            "{err}",
3775        );
3776        lb.conn
3777            .publish_done(&mut outbound, v(0x8), v(0), Vec::new())
3778            .await
3779            .expect("the termination the refusal owes");
3780        assert!(matches!(next_control(&mut their_recv).await, ControlMessage::PublishDone(_)));
3781    }
3782}