Skip to main content

moqtap_client/draft17/
connection.rs

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