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