Skip to main content

moqtap_client/draft18/
connection.rs

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