Skip to main content

moqtap_client/draft14/
connection.rs

1use bytes::{Buf, Bytes, BytesMut};
2
3use crate::draft14::endpoint::{Endpoint, EndpointError};
4use crate::draft14::event::{ClientEvent, Direction, StreamKind};
5use crate::draft14::observer::ConnectionObserver;
6use crate::draft14::session::request_id::Role;
7use crate::draft14::session::setup;
8use crate::forwarding_preference::ObjectForwardingPreference;
9use crate::malformed_tracks::MalformedTrackCondition;
10use crate::track_locations::{ObjectLocation, ObjectRole, TrackObjects};
11use crate::transport::{RecvStream, SendStream, Transport, TransportError};
12use moqtap_codec::dispatch::{
13    AnyControlMessage, AnyDatagramHeader, AnyFetchHeader, AnySubgroupHeader,
14};
15use moqtap_codec::draft14::data_stream::{FetchObject, SubgroupObject, SubgroupObjectReader};
16use moqtap_codec::draft14::message::ControlMessage;
17use moqtap_codec::error::CodecError;
18use moqtap_codec::kvp::KeyValuePair;
19use moqtap_codec::types::*;
20use moqtap_codec::varint::VarInt;
21use moqtap_codec::version::DraftVersion;
22
23/// MoQT ALPN identifier (used by raw QUIC transport).
24pub const MOQT_ALPN: &[u8] = b"moq-00";
25
26/// Errors from the connection layer.
27#[derive(Debug, thiserror::Error)]
28pub enum ConnectionError {
29    /// Endpoint state machine error.
30    #[error("endpoint error: {0}")]
31    Endpoint(#[from] EndpointError),
32    /// Wire codec error.
33    #[error("codec error: {0}")]
34    Codec(#[from] CodecError),
35    /// Transport-level error.
36    #[error("transport error: {0}")]
37    Transport(#[from] TransportError),
38    /// Variable-length integer decoding error.
39    #[error("varint error: {0}")]
40    VarInt(#[from] moqtap_codec::varint::VarIntError),
41    /// Control stream was not opened.
42    #[error("control stream not open")]
43    NoControlStream,
44    /// Stream ended before a complete message was read.
45    #[error("unexpected end of stream")]
46    UnexpectedEnd,
47    /// Stream was finished by the peer.
48    #[error("stream finished")]
49    StreamFinished,
50    /// Invalid server address string.
51    #[error("invalid server address: {0}")]
52    InvalidAddress(String),
53    /// TLS configuration error.
54    #[error("TLS config error: {0}")]
55    TlsConfig(String),
56    /// Data stream used out of order (e.g. object before header).
57    #[error("data stream state error: {0}")]
58    DataStreamState(&'static str),
59}
60
61impl From<crate::transport::DialError> for ConnectionError {
62    /// Preserves the variants this error had when the dial was inlined here,
63    /// so a caller matching on `InvalidAddress` or `TlsConfig` sees no change.
64    fn from(e: crate::transport::DialError) -> Self {
65        match e {
66            crate::transport::DialError::InvalidAddress(s) => ConnectionError::InvalidAddress(s),
67            crate::transport::DialError::TlsConfig(s) => ConnectionError::TlsConfig(s),
68            crate::transport::DialError::Transport(e) => ConnectionError::Transport(e),
69        }
70    }
71}
72
73/// Transport type for the connection.
74#[derive(Debug, Clone)]
75pub enum TransportType {
76    /// Raw QUIC via quinn. The `addr` field should be `host:port`.
77    Quic,
78    /// WebTransport via wtransport. The `url` field is the WebTransport URL.
79    WebTransport {
80        /// The WebTransport endpoint URL (e.g., `https://host:port/path`).
81        url: String,
82    },
83}
84
85/// Configuration for a MoQT client connection.
86///
87/// Both `draft` and `transport` are required — there is no `Default` impl.
88pub struct ClientConfig {
89    /// The MoQT draft version to use (primary, determines codec/framing).
90    pub draft: DraftVersion,
91    /// Additional draft versions to offer in CLIENT_SETUP.
92    /// The primary `draft` is always included first.
93    pub additional_versions: Vec<DraftVersion>,
94    /// The transport type (QUIC or WebTransport).
95    pub transport: TransportType,
96    /// Whether to skip TLS certificate verification (for testing).
97    pub skip_cert_verification: bool,
98    /// Custom CA certificates to trust (DER-encoded).
99    pub ca_certs: Vec<Vec<u8>>,
100    /// Setup parameters to include in CLIENT_SETUP (e.g., auth tokens).
101    pub setup_parameters: Vec<moqtap_codec::kvp::KeyValuePair>,
102}
103
104impl ClientConfig {
105    /// Returns the MoQT version varints for the CLIENT_SETUP message.
106    /// Primary draft first, then any additional versions.
107    pub fn supported_versions(&self) -> Vec<VarInt> {
108        let mut versions = vec![self.draft.version_varint()];
109        for v in &self.additional_versions {
110            let varint = v.version_varint();
111            if !versions.contains(&varint) {
112                versions.push(varint);
113            }
114        }
115        versions
116    }
117
118    /// Returns the ALPN protocol identifiers for the transport.
119    pub fn alpn(&self) -> Vec<Vec<u8>> {
120        match &self.transport {
121            TransportType::Quic => vec![self.draft.quic_alpn().to_vec()],
122            TransportType::WebTransport { .. } => vec![b"h3".to_vec()],
123        }
124    }
125}
126
127/// A framed writer for a send stream. Handles MoQT length-prefixed framing.
128pub struct FramedSendStream {
129    inner: SendStream,
130    draft: DraftVersion,
131    /// Stateful subgroup object encoder. Initialized by
132    /// [`FramedSendStream::write_subgroup_header`] and used by
133    /// [`FramedSendStream::write_subgroup_object`] to track the delta-encoded
134    /// object ID state.
135    subgroup_io: Option<SubgroupObjectReader>,
136}
137
138impl FramedSendStream {
139    /// Create a new framed send stream for the given draft version.
140    pub fn new(inner: SendStream, draft: DraftVersion) -> Self {
141        Self { inner, draft, subgroup_io: None }
142    }
143
144    /// Get the transport-level stream ID.
145    pub fn stream_id(&self) -> u64 {
146        self.inner.stream_id()
147    }
148
149    /// Write a control message to the stream with type+length framing.
150    /// Returns the raw bytes that were written (for event capture).
151    pub async fn write_control(
152        &mut self,
153        msg: &AnyControlMessage,
154    ) -> Result<Vec<u8>, ConnectionError> {
155        let mut buf = Vec::new();
156        msg.encode(&mut buf)?;
157        self.inner.write_all(&buf).await?;
158        Ok(buf)
159    }
160
161    /// Write a subgroup stream header. Also initializes the internal
162    /// delta-encoding state used by [`FramedSendStream::write_subgroup_object`].
163    ///
164    /// The header is refused, and nothing is written, if its fields disagree
165    /// with its own stream type. That check has to happen here rather than at
166    /// the first object: the type is what every object after it is framed
167    /// against, so a header that went out saying the wrong thing cannot be
168    /// taken back.
169    pub async fn write_subgroup_header(
170        &mut self,
171        header: &AnySubgroupHeader,
172    ) -> Result<(), ConnectionError> {
173        let mut buf = Vec::new();
174        header.encode_stream_checked(&mut buf)?;
175        self.inner.write_all(&buf).await?;
176        // Clippy would rather see these two arms as an `if let`, and rustc rejects
177        // that in a single-draft build, where the pattern is irrefutable. Only a
178        // `match` satisfies both.
179        #[allow(clippy::single_match)]
180        match header {
181            AnySubgroupHeader::Draft14(ref d14) => {
182                self.subgroup_io = Some(SubgroupObjectReader::new(d14));
183            }
184            // Only this draft's header seeds the object reader. With draft 14 the only enabled
185            // draft `AnySubgroupHeader` has a single variant, the arm above is exhaustive and this
186            // one unreachable. Compiled in every configuration with the lint allowed, rather than
187            // gated on a `cfg` naming the other thirteen drafts: that list had to be edited in
188            // every draft module whenever a draft was added, and a copy that omitted one left this
189            // match non-exhaustive.
190            #[allow(unreachable_patterns)]
191            _ => {}
192        }
193        Ok(())
194    }
195
196    /// Write a fetch response header.
197    pub async fn write_fetch_header(
198        &mut self,
199        header: &AnyFetchHeader,
200    ) -> Result<(), ConnectionError> {
201        let mut buf = Vec::new();
202        header.encode_stream(&mut buf);
203        self.inner.write_all(&buf).await?;
204        Ok(())
205    }
206
207    /// Append a draft-14 subgroup object to the stream. Uses the stateful
208    /// reader installed by [`FramedSendStream::write_subgroup_header`] to
209    /// produce the correct delta-encoded object ID. Returns an error if
210    /// called before a subgroup header was written.
211    pub async fn write_subgroup_object(
212        &mut self,
213        object: &SubgroupObject,
214    ) -> Result<(), ConnectionError> {
215        let writer = self
216            .subgroup_io
217            .as_mut()
218            .ok_or(ConnectionError::DataStreamState("subgroup header not written yet"))?;
219        let mut buf = Vec::new();
220        writer.write_object(object, &mut buf)?;
221        self.inner.write_all(&buf).await?;
222        Ok(())
223    }
224
225    /// Append a draft-14 fetch object to the stream. Fetch objects are
226    /// self-contained (each carries its own group/subgroup/object IDs and
227    /// priority), so no prior `write_fetch_header` bookkeeping is required.
228    /// Append a fetch object to the stream.
229    ///
230    /// Draft-14 derives the declared length from the payload itself rather than
231    /// holding it as a field, so there is nothing here for the two to disagree
232    /// about - unlike the subgroup object, whose header carries the length and
233    /// whose writer has to overwrite it.
234    ///
235    /// # Errors
236    ///
237    /// Whatever the checked encoder refuses: a payload beside a status that
238    /// forbids one, or a status the draft leaves unassigned. This went through
239    /// the unchecked encoder, so both were written.
240    pub async fn write_fetch_object(
241        &mut self,
242        object: &FetchObject,
243    ) -> Result<(), ConnectionError> {
244        let mut buf = Vec::new();
245        object.encode_checked(&mut buf)?;
246        self.inner.write_all(&buf).await?;
247        Ok(())
248    }
249
250    /// Finish the stream (send FIN).
251    pub async fn finish(&mut self) -> Result<(), ConnectionError> {
252        self.inner.finish()?;
253        Ok(())
254    }
255
256    /// Returns the draft version this stream is framed for.
257    pub fn draft(&self) -> DraftVersion {
258        self.draft
259    }
260}
261
262/// What an Object Status makes of an object here.
263///
264/// Two answers where drafts 08 through 13 have three, and the missing one is
265/// the point: the end-of-track status settles where the track ended and is
266/// judged against nothing, because the rule about where one may be placed is
267/// not in this draft. `a_track_may_end_where_it_has_already_been.rs` asserts
268/// that acceptance.
269///
270/// Every other status is a statement about objects rather than one of them.
271fn object_role(status: Option<u64>) -> ObjectRole {
272    match status {
273        None | Some(0x0) => ObjectRole::Produced,
274        Some(0x4) => ObjectRole::EndsTrack(None),
275        _ => ObjectRole::Neither,
276    }
277}
278
279/// A framed reader for a recv stream. Handles MoQT varint-length decoding.
280pub struct FramedRecvStream {
281    inner: RecvStream,
282    buf: BytesMut,
283    draft: DraftVersion,
284    /// Stateful subgroup object decoder. Initialized by
285    /// [`FramedRecvStream::read_subgroup_header`] and used by
286    /// [`FramedRecvStream::read_subgroup_object`] to track delta-encoded
287    /// object IDs and whether extension headers are present.
288    subgroup_io: Option<SubgroupObjectReader>,
289    /// The record this stream's objects are measured against, and the Group ID
290    /// its header named.
291    ///
292    /// One group for the whole stream: a subgroup header names it once and no
293    /// object header repeats it. `None` on a stream that was never given one -
294    /// a stream for an alias no live binding names, and every stream built
295    /// outside [`Connection::accept_subgroup_stream`] - and such a stream reads
296    /// exactly as it did before this existed.
297    tracking: Option<(TrackObjects, u64)>,
298}
299
300impl FramedRecvStream {
301    /// Create a new framed receive stream for the given draft version.
302    pub fn new(inner: RecvStream, draft: DraftVersion) -> Self {
303        Self { inner, buf: BytesMut::with_capacity(4096), draft, subgroup_io: None, tracking: None }
304    }
305
306    /// Get the transport-level stream ID.
307    pub fn stream_id(&self) -> u64 {
308        self.inner.stream_id()
309    }
310
311    /// Measure this stream's objects against `objects`, all of them in `group`.
312    ///
313    /// Called by [`Connection::accept_subgroup_stream`] once the header has
314    /// been read, which is the only point at which both the track and the group
315    /// are known.
316    fn measure_objects_against(&mut self, objects: TrackObjects, group: u64) {
317        self.tracking = Some((objects, group));
318    }
319
320    /// Judge one object this stream carried against where its track ended.
321    ///
322    /// The object's Group ID is the stream's and its Object ID is its own,
323    /// already resolved from the delta the wire carries; what they are measured
324    /// against is the end an end-of-track object settled on any stream.
325    fn note_subgroup_object(
326        &self,
327        object: u64,
328        status: Option<u64>,
329    ) -> Result<(), ConnectionError> {
330        let Some((objects, group)) = &self.tracking else { return Ok(()) };
331        let at = ObjectLocation { group: *group, object };
332        objects.note_past_final(at, object_role(status)).map_err(|end| {
333            ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject {
334                alias: objects.alias(),
335                group: at.group,
336                object: at.object,
337                final_group: end.group,
338                final_object: end.object,
339            })
340        })
341    }
342
343    /// Read more data from the stream into the internal buffer.
344    async fn fill(&mut self) -> Result<bool, ConnectionError> {
345        let mut tmp = [0u8; 4096];
346        match self.inner.read(&mut tmp).await {
347            Ok(Some(n)) => {
348                self.buf.extend_from_slice(&tmp[..n]);
349                Ok(true)
350            }
351            Ok(None) => Ok(false),
352            Err(e) => Err(ConnectionError::Transport(e)),
353        }
354    }
355
356    /// Ensure at least `n` bytes are available in the buffer.
357    async fn ensure(&mut self, n: usize) -> Result<(), ConnectionError> {
358        while self.buf.len() < n {
359            if !self.fill().await? {
360                return Err(ConnectionError::UnexpectedEnd);
361            }
362        }
363        Ok(())
364    }
365
366    /// Read a control message from the stream.
367    ///
368    /// When `capture_raw` is true, the returned tuple includes a clone of the
369    /// framed wire bytes (for observer emission). When false, the second
370    /// element is `None` and the payload clone is skipped.
371    pub async fn read_control(
372        &mut self,
373        capture_raw: bool,
374    ) -> Result<(AnyControlMessage, Option<Vec<u8>>), ConnectionError> {
375        // Read type ID varint
376        self.ensure(1).await?;
377        let type_len = varint_len(self.buf[0]);
378        self.ensure(type_len).await?;
379
380        let mut cursor = &self.buf[..type_len];
381        let _type_id = VarInt::decode(&mut cursor)?;
382
383        // Read payload length (16-bit BE for draft-11+, varint for earlier drafts)
384        let (payload_len, len_field_size) = if self.draft.uses_fixed_length_framing() {
385            self.ensure(type_len + 2).await?;
386            let hi = self.buf[type_len] as usize;
387            let lo = self.buf[type_len + 1] as usize;
388            ((hi << 8) | lo, 2)
389        } else {
390            self.ensure(type_len + 1).await?;
391            let payload_len_start = type_len;
392            let payload_len_varint_len = varint_len(self.buf[payload_len_start]);
393            self.ensure(type_len + payload_len_varint_len).await?;
394            let mut cursor = &self.buf[payload_len_start..type_len + payload_len_varint_len];
395            let payload_len = VarInt::decode(&mut cursor)?.into_inner() as usize;
396            (payload_len, payload_len_varint_len)
397        };
398
399        // Read full payload
400        let total = type_len + len_field_size + payload_len;
401        self.ensure(total).await?;
402
403        // Capture raw bytes only if requested (observer attached).
404        let raw = capture_raw.then(|| self.buf[..total].to_vec());
405
406        // Now decode the whole message
407        let mut frame = &self.buf[..total];
408        let msg = AnyControlMessage::decode(self.draft, &mut frame)?;
409        self.buf.advance(total);
410        Ok((msg, raw))
411    }
412
413    /// Read a subgroup stream header. Also initializes the internal
414    /// delta-decoding state used by
415    /// [`FramedRecvStream::read_subgroup_object`].
416    pub async fn read_subgroup_header(&mut self) -> Result<AnySubgroupHeader, ConnectionError> {
417        self.ensure(1).await?;
418        loop {
419            let mut cursor = &self.buf[..];
420            match AnySubgroupHeader::decode(self.draft, &mut cursor) {
421                Ok(header) => {
422                    let consumed = self.buf.len() - cursor.remaining();
423                    self.buf.advance(consumed);
424                    // Clippy would rather see these two arms as an `if let`, and rustc rejects
425                    // that in a single-draft build, where the pattern is irrefutable. Only a
426                    // `match` satisfies both.
427                    #[allow(clippy::single_match)]
428                    match header {
429                        AnySubgroupHeader::Draft14(ref d14) => {
430                            self.subgroup_io = Some(SubgroupObjectReader::new(d14));
431                        }
432                        // Only this draft's header seeds the object reader. With draft 14 the only
433                        // enabled draft `AnySubgroupHeader` has a single variant, the arm above is
434                        // exhaustive and this one unreachable. Compiled in every configuration with
435                        // the lint allowed, rather than gated on a `cfg` naming the other thirteen
436                        // drafts: that list had to be edited in every draft module whenever a draft
437                        // was added, and a copy that omitted one left this match non-exhaustive.
438                        #[allow(unreachable_patterns)]
439                        _ => {}
440                    }
441                    return Ok(header);
442                }
443                Err(CodecError::UnexpectedEnd) => {
444                    if !self.fill().await? {
445                        return Err(ConnectionError::UnexpectedEnd);
446                    }
447                }
448                Err(e) => return Err(ConnectionError::Codec(e)),
449            }
450        }
451    }
452
453    /// Read a fetch response header.
454    pub async fn read_fetch_header(&mut self) -> Result<AnyFetchHeader, ConnectionError> {
455        self.ensure(1).await?;
456        loop {
457            let mut cursor = &self.buf[..];
458            match AnyFetchHeader::decode(self.draft, &mut cursor) {
459                Ok(header) => {
460                    let consumed = self.buf.len() - cursor.remaining();
461                    self.buf.advance(consumed);
462                    return Ok(header);
463                }
464                Err(CodecError::UnexpectedEnd) => {
465                    if !self.fill().await? {
466                        return Err(ConnectionError::UnexpectedEnd);
467                    }
468                }
469                Err(e) => return Err(ConnectionError::Codec(e)),
470            }
471        }
472    }
473
474    /// Read the next draft-14 subgroup object from this stream. Uses the
475    /// stateful reader installed by
476    /// [`FramedRecvStream::read_subgroup_header`] to decode the delta-
477    /// encoded object ID and handle extension headers per the stream type.
478    /// Returns an error if called before a subgroup header was read.
479    pub async fn read_subgroup_object(&mut self) -> Result<SubgroupObject, ConnectionError> {
480        if self.subgroup_io.is_none() {
481            return Err(ConnectionError::DataStreamState("subgroup header not read yet"));
482        }
483        loop {
484            let reader = self.subgroup_io.as_mut().unwrap();
485            let mut probe = reader.clone();
486            let mut cursor = &self.buf[..];
487            match probe.read_object(&mut cursor) {
488                Ok(obj) => {
489                    let consumed = self.buf.len() - cursor.remaining();
490                    self.buf.advance(consumed);
491                    // Commit state from the successful probe decode.
492                    *reader = probe;
493                    self.note_subgroup_object(
494                        obj.object_id.into_inner(),
495                        obj.status.map(|s| s as u64),
496                    )?;
497                    return Ok(obj);
498                }
499                Err(CodecError::UnexpectedEnd) => {
500                    if !self.fill().await? {
501                        return Err(ConnectionError::UnexpectedEnd);
502                    }
503                }
504                Err(e) => return Err(ConnectionError::Codec(e)),
505            }
506        }
507    }
508
509    /// Read the next draft-14 fetch object from this stream. Fetch objects
510    /// are self-describing, so no prior `read_fetch_header` state is needed
511    /// to decode each object.
512    pub async fn read_fetch_object(&mut self) -> Result<FetchObject, ConnectionError> {
513        loop {
514            let mut cursor = &self.buf[..];
515            match FetchObject::decode(&mut cursor) {
516                Ok(obj) => {
517                    let consumed = self.buf.len() - cursor.remaining();
518                    self.buf.advance(consumed);
519                    return Ok(obj);
520                }
521                Err(CodecError::UnexpectedEnd) => {
522                    if !self.fill().await? {
523                        return Err(ConnectionError::UnexpectedEnd);
524                    }
525                }
526                Err(e) => return Err(ConnectionError::Codec(e)),
527            }
528        }
529    }
530
531    /// Returns the draft version this stream is framed for.
532    pub fn draft(&self) -> DraftVersion {
533        self.draft
534    }
535}
536
537/// A live MoQT connection over QUIC or WebTransport, combining the endpoint
538/// state machine with actual network I/O.
539pub struct Connection {
540    transport: Transport,
541    endpoint: Endpoint,
542    draft: DraftVersion,
543    /// Behind a lock because a control message is written from two kinds of
544    /// place. Most of them are the caller's own request, made through `&mut
545    /// self`. The messages that answer a Malformed Track are not: the
546    /// conditions that make a track malformed are detected on the data plane,
547    /// where this connection is reached through a shared reference. The lock
548    /// also makes one message the unit of writing, so two of them cannot
549    /// interleave on the stream.
550    control_send: Option<tokio::sync::Mutex<FramedSendStream>>,
551    control_recv: Option<FramedRecvStream>,
552    observer: Option<Box<dyn ConnectionObserver>>,
553    /// Setup events buffered during `connect()` and replayed when an
554    /// observer attaches via `set_observer` — without this, an observer
555    /// attached after `connect` returns would never see the handshake.
556    pending_events: Vec<ClientEvent>,
557}
558
559impl Connection {
560    /// Connect to a MoQT server as a client.
561    ///
562    /// Establishes a QUIC or WebTransport connection (based on `config.transport`),
563    /// opens a bidirectional control stream, performs the CLIENT_SETUP /
564    /// SERVER_SETUP handshake, and returns a ready-to-use connection.
565    pub async fn connect(addr: &str, config: ClientConfig) -> Result<Self, ConnectionError> {
566        // PATH is for native QUIC only, and the transport is known here and
567        // nowhere further in. Refusing before dialling means a session that
568        // the server would close on sight is never opened.
569        setup::validate_client_path_transport(
570            &config.setup_parameters,
571            matches!(config.transport, TransportType::WebTransport { .. }),
572        )
573        .map_err(EndpointError::from)?;
574
575        let transport = match &config.transport {
576            TransportType::Quic => Self::connect_quic(addr, &config).await?,
577            TransportType::WebTransport { url } => {
578                let url = url.clone();
579                Self::connect_webtransport(&url, &config).await?
580            }
581        };
582
583        Self::adopt(transport, config).await
584    }
585
586    /// Run the MoQT setup handshake over a transport somebody else established.
587    ///
588    /// For choosing the draft from what the server selected: dial once through
589    /// [`crate::transport::dial_quic`] offering every ALPN, then bring the
590    /// connection to the module its answer names. [`Self::connect`] cannot do
591    /// this — it derives its single ALPN from the draft it was given.
592    ///
593    /// `config.draft` must match this module. The transport is adopted as
594    /// given; nothing here re-checks the ALPN it was negotiated with.
595    pub async fn adopt(
596        transport: Transport,
597        config: ClientConfig,
598    ) -> Result<Self, ConnectionError> {
599        let draft = config.draft;
600        // PATH is for native QUIC only, and the transport is known here and
601        // nowhere further in. Refusing before dialling means a session that
602        // the server would close on sight is never opened.
603        setup::validate_client_path_transport(
604            &config.setup_parameters,
605            matches!(config.transport, TransportType::WebTransport { .. }),
606        )
607        .map_err(EndpointError::from)?;
608
609        // Open bidirectional control stream
610        let (send, recv) = transport.open_bi().await?;
611        let mut control_send = FramedSendStream::new(send, draft);
612        let mut control_recv = FramedRecvStream::new(recv, draft);
613
614        // Perform setup handshake
615        let mut endpoint = Endpoint::new(Role::Client);
616        endpoint.connect()?;
617        let setup_msg = endpoint
618            .send_client_setup(config.supported_versions(), config.setup_parameters.clone())?;
619        let any_setup = AnyControlMessage::Draft14(setup_msg);
620        let raw_setup = control_send.write_control(&any_setup).await?;
621
622        let (server_setup, raw_server_setup) = control_recv.read_control(true).await?;
623        // Unwrap to draft-14 for the endpoint (which is draft-14 only)
624        match &server_setup {
625            AnyControlMessage::Draft14(ControlMessage::ServerSetup(ref ss)) => {
626                endpoint.receive_server_setup(ss)?;
627            }
628            _ => {
629                return Err(ConnectionError::Endpoint(EndpointError::NotActive));
630            }
631        }
632
633        let mut pending_events = Vec::with_capacity(3);
634        pending_events.push(ClientEvent::ControlMessage {
635            direction: Direction::Send,
636            message: any_setup,
637            raw: Some(raw_setup),
638        });
639        pending_events.push(ClientEvent::ControlMessage {
640            direction: Direction::Receive,
641            message: server_setup,
642            raw: raw_server_setup,
643        });
644        if let Some(v) = endpoint.negotiated_version() {
645            pending_events.push(ClientEvent::SetupComplete { negotiated_version: v.into_inner() });
646        }
647
648        Ok(Self {
649            transport,
650            endpoint,
651            draft,
652            control_send: Some(tokio::sync::Mutex::new(control_send)),
653            control_recv: Some(control_recv),
654            observer: None,
655            pending_events,
656        })
657    }
658
659    /// Establish a raw QUIC connection.
660    ///
661    /// Offers this draft's ALPN alone; [`crate::transport::dial_quic`] holds the
662    /// TLS and endpoint setup.
663    async fn connect_quic(addr: &str, config: &ClientConfig) -> Result<Transport, ConnectionError> {
664        let (transport, _negotiated) = crate::transport::dial_quic(
665            addr,
666            &crate::transport::QuicDialOptions {
667                skip_cert_verification: config.skip_cert_verification,
668                ca_certs: config.ca_certs.clone(),
669                alpn: config.alpn(),
670            },
671        )
672        .await?;
673        Ok(transport)
674    }
675
676    /// Establish a WebTransport connection.
677    #[cfg(feature = "webtransport")]
678    async fn connect_webtransport(
679        url: &str,
680        config: &ClientConfig,
681    ) -> Result<Transport, ConnectionError> {
682        use crate::transport::webtransport::WebTransportTransport;
683
684        let wt_config = if config.skip_cert_verification {
685            wtransport::ClientConfig::builder()
686                .with_bind_default()
687                .with_no_cert_validation()
688                .build()
689        } else {
690            wtransport::ClientConfig::builder().with_bind_default().with_native_certs().build()
691        };
692
693        let endpoint = wtransport::Endpoint::client(wt_config)
694            .map_err(|e| ConnectionError::Transport(TransportError::Connect(e.to_string())))?;
695
696        let connection = endpoint
697            .connect(url)
698            .await
699            .map_err(|e| ConnectionError::Transport(TransportError::Connect(e.to_string())))?;
700
701        Ok(Transport::WebTransport(WebTransportTransport::new(connection)))
702    }
703
704    /// Stub for when the webtransport feature is not enabled.
705    #[cfg(not(feature = "webtransport"))]
706    async fn connect_webtransport(
707        _url: &str,
708        _config: &ClientConfig,
709    ) -> Result<Transport, ConnectionError> {
710        Err(ConnectionError::Transport(TransportError::Connect(
711            "webtransport feature not enabled".into(),
712        )))
713    }
714
715    // ── Observer ───────────────────────────────────────────────
716
717    /// Attach an observer. Buffered handshake events from `connect()` are
718    /// flushed in arrival order before this returns.
719    pub fn set_observer(&mut self, observer: Box<dyn ConnectionObserver>) {
720        self.observer = Some(observer);
721        for event in self.pending_events.drain(..) {
722            if let Some(ref obs) = self.observer {
723                obs.on_event_owned(event);
724            }
725        }
726    }
727
728    /// Remove the observer.
729    pub fn clear_observer(&mut self) {
730        self.observer = None;
731    }
732
733    /// Emit an event to the observer, if one is attached.
734    fn emit(&self, event: ClientEvent) {
735        if let Some(ref obs) = self.observer {
736            obs.on_event_owned(event);
737        }
738    }
739
740    // ── Control message I/O ─────────────────────────────────
741
742    /// Send a control message on the control stream.
743    ///
744    /// Wraps the draft-14 message in `AnyControlMessage::Draft14` for framing.
745    pub async fn send_control(&self, msg: &ControlMessage) -> Result<(), ConnectionError> {
746        let any = AnyControlMessage::Draft14(msg.clone());
747        let mut send =
748            self.control_send.as_ref().ok_or(ConnectionError::NoControlStream)?.lock().await;
749        let raw = send.write_control(&any).await?;
750        drop(send);
751        self.emit(ClientEvent::ControlMessage {
752            direction: Direction::Send,
753            message: any,
754            raw: Some(raw),
755        });
756        Ok(())
757    }
758
759    /// Read the next control message from the control stream.
760    ///
761    /// Returns the `AnyControlMessage` and also extracts the draft-14
762    /// `ControlMessage` for internal endpoint dispatch.
763    pub async fn recv_control(&mut self) -> Result<ControlMessage, ConnectionError> {
764        let recv = self.control_recv.as_mut().ok_or(ConnectionError::NoControlStream)?;
765        let capture_raw = self.observer.is_some();
766        let (any, raw) = match recv.read_control(capture_raw).await {
767            Ok(v) => v,
768            Err(e) => return Err(self.close_for_codec(e)),
769        };
770        if capture_raw {
771            self.emit(ClientEvent::ControlMessage {
772                direction: Direction::Receive,
773                message: any.clone(),
774                raw,
775            });
776        }
777        // Unwrap to draft-14 for the endpoint
778        match any {
779            AnyControlMessage::Draft14(msg) => Ok(msg),
780            // `AnyControlMessage` carries one variant per enabled draft feature. With draft 14 the
781            // only one enabled the arm above is exhaustive and this rejection arm unreachable.
782            // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
783            // naming the other thirteen drafts: that list had to be edited in every draft module
784            // whenever a draft was added, and a copy that omitted one left this match
785            // non-exhaustive.
786            #[allow(unreachable_patterns)]
787            _ => Err(ConnectionError::Codec(CodecError::UnknownMessageType(0))),
788        }
789    }
790
791    /// Read and dispatch the next incoming control message through the endpoint
792    /// state machine. Returns the decoded message for inspection.
793    pub async fn recv_and_dispatch(&mut self) -> Result<ControlMessage, ConnectionError> {
794        let msg = self.recv_control().await?;
795        self.endpoint.receive_message(msg.clone()).map_err(|e| self.close_if_session_fatal(e))?;
796
797        // Emit draining event if this was a GoAway
798        if let ControlMessage::GoAway(ref ga) = msg {
799            self.emit(ClientEvent::Draining { new_session_uri: ga.new_session_uri.clone() });
800        }
801
802        Ok(msg)
803    }
804
805    // ── Subscribe flow ──────────────────────────────────────
806
807    /// Send a SUBSCRIBE and return the allocated request ID.
808    pub async fn subscribe(
809        &mut self,
810        track_namespace: TrackNamespace,
811        track_name: Vec<u8>,
812        subscriber_priority: u8,
813        group_order: GroupOrder,
814        filter_type: FilterType,
815        parameters: Vec<KeyValuePair>,
816    ) -> Result<VarInt, ConnectionError> {
817        let (req_id, msg) = self.endpoint.subscribe(
818            track_namespace,
819            track_name,
820            subscriber_priority,
821            group_order,
822            filter_type,
823            parameters,
824        )?;
825        self.send_control(&msg).await?;
826        Ok(req_id)
827    }
828
829    /// Send a SUBSCRIBE for a range of the track, starting at a given
830    /// location.
831    ///
832    /// The Filter Type is derived from the range, so the message cannot name a
833    /// filter whose fields it does not carry.
834    #[allow(clippy::too_many_arguments)]
835    pub async fn subscribe_range(
836        &mut self,
837        track_namespace: TrackNamespace,
838        track_name: Vec<u8>,
839        subscriber_priority: u8,
840        group_order: GroupOrder,
841        start_location: Location,
842        end_group: Option<VarInt>,
843        parameters: Vec<KeyValuePair>,
844    ) -> Result<VarInt, ConnectionError> {
845        let (req_id, msg) = self.endpoint.subscribe_range(
846            track_namespace,
847            track_name,
848            subscriber_priority,
849            group_order,
850            start_location,
851            end_group,
852            parameters,
853        )?;
854        self.send_control(&msg).await?;
855        Ok(req_id)
856    }
857
858    /// Send an UNSUBSCRIBE for the given request ID.
859    pub async fn unsubscribe(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
860        let msg = self.endpoint.unsubscribe(request_id)?;
861        self.send_control(&msg).await
862    }
863
864    /// Accept a subscription the peer opened, sending SUBSCRIBE_OK and giving
865    /// its track a Track Alias.
866    ///
867    /// The endpoint refuses an alias a live track of its own already holds and
868    /// refuses a second answer to one SUBSCRIBE, so nothing is written on the
869    /// wire when it does either.
870    pub async fn subscribe_ok(
871        &mut self,
872        request_id: VarInt,
873        track_alias: VarInt,
874        expires: VarInt,
875        group_order: GroupOrder,
876        parameters: Vec<KeyValuePair>,
877    ) -> Result<(), ConnectionError> {
878        let msg = self.endpoint.send_subscribe_ok(
879            request_id,
880            track_alias,
881            expires,
882            group_order,
883            parameters,
884        )?;
885        self.send_control(&msg).await
886    }
887
888    /// Reject a subscription the peer opened, sending SUBSCRIBE_ERROR.
889    ///
890    /// The endpoint refuses a second answer to one SUBSCRIBE, so nothing is
891    /// written on the wire when it does.
892    pub async fn subscribe_error(
893        &mut self,
894        request_id: VarInt,
895        error_code: VarInt,
896        reason_phrase: Vec<u8>,
897    ) -> Result<(), ConnectionError> {
898        let msg = self.endpoint.send_subscribe_error(request_id, error_code, reason_phrase)?;
899        self.send_control(&msg).await
900    }
901
902    /// Accept a PUBLISH the peer sent, which establishes the subscription it
903    /// opened.
904    ///
905    /// The endpoint refuses a second answer to one PUBLISH, so nothing is
906    /// written on the wire when it does.
907    #[allow(clippy::too_many_arguments)]
908    pub async fn publish_ok(
909        &mut self,
910        request_id: VarInt,
911        forward: Forward,
912        subscriber_priority: u8,
913        group_order: GroupOrder,
914        filter_type: FilterType,
915        start_location: Option<Location>,
916        end_group: Option<VarInt>,
917    ) -> Result<(), ConnectionError> {
918        let msg = self.endpoint.send_publish_ok(
919            request_id,
920            forward,
921            subscriber_priority,
922            group_order,
923            filter_type,
924            start_location,
925            end_group,
926        )?;
927        self.send_control(&msg).await
928    }
929
930    /// Reject a PUBLISH the peer sent, which ends the subscription it opened
931    /// before it was established.
932    ///
933    /// The endpoint refuses a second answer to one PUBLISH, so nothing is
934    /// written on the wire when it does.
935    pub async fn publish_error(
936        &mut self,
937        request_id: VarInt,
938        error_code: VarInt,
939        reason_phrase: Vec<u8>,
940    ) -> Result<(), ConnectionError> {
941        let msg = self.endpoint.send_publish_error(request_id, error_code, reason_phrase)?;
942        self.send_control(&msg).await
943    }
944
945    /// Send a SUBSCRIBE_UPDATE for an active subscription. Returns the
946    /// allocated request ID of the update message.
947    pub async fn subscribe_update(
948        &mut self,
949        subscription_request_id: VarInt,
950        start_location: Location,
951        end_group: VarInt,
952        subscriber_priority: u8,
953        forward: Forward,
954        parameters: Vec<moqtap_codec::kvp::KeyValuePair>,
955    ) -> Result<VarInt, ConnectionError> {
956        let (req_id, msg) = self.endpoint.subscribe_update(
957            subscription_request_id,
958            start_location,
959            end_group,
960            subscriber_priority,
961            forward,
962            parameters,
963        )?;
964        self.send_control(&msg).await?;
965        Ok(req_id)
966    }
967
968    // ── Fetch flow ──────────────────────────────────────────
969
970    /// Send a FETCH and return the allocated request ID.
971    #[allow(clippy::too_many_arguments)]
972    pub async fn fetch(
973        &mut self,
974        track_namespace: TrackNamespace,
975        track_name: Vec<u8>,
976        subscriber_priority: u8,
977        group_order: GroupOrder,
978        start_group: VarInt,
979        start_object: VarInt,
980        end_group: VarInt,
981        end_object: VarInt,
982        parameters: Vec<KeyValuePair>,
983    ) -> Result<VarInt, ConnectionError> {
984        let (req_id, msg) = self.endpoint.fetch(
985            track_namespace,
986            track_name,
987            subscriber_priority,
988            group_order,
989            start_group,
990            start_object,
991            end_group,
992            end_object,
993            parameters,
994        )?;
995        self.send_control(&msg).await?;
996        Ok(req_id)
997    }
998
999    /// Send a Relative Joining Fetch and return the allocated request ID.
1000    ///
1001    /// `joining_start` is a count of groups back from the subscription's live
1002    /// edge. For the form that names the group outright, see
1003    /// [`absolute_joining_fetch`](Self::absolute_joining_fetch).
1004    pub async fn joining_fetch(
1005        &mut self,
1006        subscriber_priority: u8,
1007        group_order: GroupOrder,
1008        joining_request_id: VarInt,
1009        joining_start: VarInt,
1010        parameters: Vec<KeyValuePair>,
1011    ) -> Result<VarInt, ConnectionError> {
1012        let (req_id, msg) = self.endpoint.joining_fetch(
1013            subscriber_priority,
1014            group_order,
1015            joining_request_id,
1016            joining_start,
1017            parameters,
1018        )?;
1019        self.send_control(&msg).await?;
1020        Ok(req_id)
1021    }
1022
1023    /// Send an Absolute Joining Fetch and return the allocated request ID.
1024    ///
1025    /// `joining_start` is the group to begin at.
1026    pub async fn absolute_joining_fetch(
1027        &mut self,
1028        subscriber_priority: u8,
1029        group_order: GroupOrder,
1030        joining_request_id: VarInt,
1031        joining_start: VarInt,
1032        parameters: Vec<KeyValuePair>,
1033    ) -> Result<VarInt, ConnectionError> {
1034        let (req_id, msg) = self.endpoint.absolute_joining_fetch(
1035            subscriber_priority,
1036            group_order,
1037            joining_request_id,
1038            joining_start,
1039            parameters,
1040        )?;
1041        self.send_control(&msg).await?;
1042        Ok(req_id)
1043    }
1044
1045    /// Send a FETCH_CANCEL for the given request ID.
1046    pub async fn fetch_cancel(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
1047        let msg = self.endpoint.fetch_cancel(request_id)?;
1048        self.send_control(&msg).await
1049    }
1050
1051    /// Accept a fetch the peer opened, sending FETCH_OK.
1052    ///
1053    /// The endpoint refuses a Joining Fetch naming a subscription this session
1054    /// cannot join and refuses a second answer to one FETCH, so nothing is
1055    /// written on the wire when it does either.
1056    pub async fn fetch_ok(
1057        &mut self,
1058        request_id: VarInt,
1059        group_order: GroupOrder,
1060        end_of_track: u8,
1061        end_location: Location,
1062        parameters: Vec<KeyValuePair>,
1063    ) -> Result<(), ConnectionError> {
1064        let msg = self.endpoint.send_fetch_ok(
1065            request_id,
1066            group_order,
1067            end_of_track,
1068            end_location,
1069            parameters,
1070        )?;
1071        self.send_control(&msg).await
1072    }
1073
1074    /// Refuse a fetch the peer opened, sending FETCH_ERROR.
1075    ///
1076    /// The endpoint refuses a second answer to one FETCH, and refuses a
1077    /// Joining Fetch's refusal under any code but the one the draft names for
1078    /// it, so nothing is written on the wire when it does either.
1079    pub async fn fetch_error(
1080        &mut self,
1081        request_id: VarInt,
1082        error_code: VarInt,
1083        reason_phrase: Vec<u8>,
1084    ) -> Result<(), ConnectionError> {
1085        let msg = self.endpoint.send_fetch_error(request_id, error_code, reason_phrase)?;
1086        self.send_control(&msg).await
1087    }
1088
1089    // ── Namespace flows ─────────────────────────────────────
1090
1091    /// Send a SUBSCRIBE_NAMESPACE and return the request ID.
1092    pub async fn subscribe_namespace(
1093        &mut self,
1094        track_namespace: TrackNamespace,
1095        parameters: Vec<KeyValuePair>,
1096    ) -> Result<VarInt, ConnectionError> {
1097        let (req_id, msg) = self.endpoint.subscribe_namespace(track_namespace, parameters)?;
1098        self.send_control(&msg).await?;
1099        Ok(req_id)
1100    }
1101
1102    /// Accept a namespace subscription the peer made, sending SUBSCRIBE_NAMESPACE_OK.
1103    ///
1104    /// The endpoint refuses a second answer to one SUBSCRIBE_NAMESPACE, so nothing is
1105    /// written on the wire when it does.
1106    pub async fn subscribe_namespace_ok(
1107        &mut self,
1108        request_id: VarInt,
1109    ) -> Result<(), ConnectionError> {
1110        let msg = self.endpoint.send_subscribe_namespace_ok(request_id)?;
1111        self.send_control(&msg).await
1112    }
1113
1114    /// Refuse a namespace subscription the peer made, sending SUBSCRIBE_NAMESPACE_ERROR.
1115    ///
1116    /// The other half of the same sentence: one answer, and this is the other
1117    /// one it can be.
1118    pub async fn subscribe_namespace_error(
1119        &mut self,
1120        request_id: VarInt,
1121        error_code: VarInt,
1122        reason_phrase: Vec<u8>,
1123    ) -> Result<(), ConnectionError> {
1124        let msg =
1125            self.endpoint.send_subscribe_namespace_error(request_id, error_code, reason_phrase)?;
1126        self.send_control(&msg).await
1127    }
1128
1129    /// Send a PUBLISH_NAMESPACE and return the request ID.
1130    pub async fn publish_namespace(
1131        &mut self,
1132        track_namespace: TrackNamespace,
1133        parameters: Vec<KeyValuePair>,
1134    ) -> Result<VarInt, ConnectionError> {
1135        let (req_id, msg) = self.endpoint.publish_namespace(track_namespace, parameters)?;
1136        self.send_control(&msg).await?;
1137        Ok(req_id)
1138    }
1139
1140    /// Accept an announcement the peer made, sending PUBLISH_NAMESPACE_OK.
1141    ///
1142    /// The endpoint refuses a second answer to one PUBLISH_NAMESPACE, so
1143    /// nothing is written on the wire when it does.
1144    pub async fn publish_namespace_ok(
1145        &mut self,
1146        request_id: VarInt,
1147    ) -> Result<(), ConnectionError> {
1148        let msg = self.endpoint.send_publish_namespace_ok(request_id)?;
1149        self.send_control(&msg).await
1150    }
1151
1152    /// Refuse an announcement the peer made, sending PUBLISH_NAMESPACE_ERROR.
1153    ///
1154    /// The other half of the same sentence: one answer, and this is the other
1155    /// one it can be.
1156    pub async fn publish_namespace_error(
1157        &mut self,
1158        request_id: VarInt,
1159        error_code: VarInt,
1160        reason_phrase: Vec<u8>,
1161    ) -> Result<(), ConnectionError> {
1162        let msg =
1163            self.endpoint.send_publish_namespace_error(request_id, error_code, reason_phrase)?;
1164        self.send_control(&msg).await
1165    }
1166
1167    /// Revoke an acceptance, sending PUBLISH_NAMESPACE_CANCEL.
1168    ///
1169    /// The endpoint refuses one for an announcement it never accepted, so
1170    /// nothing is written on the wire when it does.
1171    pub async fn publish_namespace_cancel(
1172        &mut self,
1173        track_namespace: TrackNamespace,
1174        error_code: VarInt,
1175        reason_phrase: Vec<u8>,
1176    ) -> Result<(), ConnectionError> {
1177        let msg =
1178            self.endpoint.publish_namespace_cancel(track_namespace, error_code, reason_phrase)?;
1179        self.send_control(&msg).await
1180    }
1181
1182    /// Withdraw an announcement this endpoint made, sending
1183    /// PUBLISH_NAMESPACE_DONE.
1184    ///
1185    /// The mirror of [`Self::publish_namespace`], and the counterpart of
1186    /// [`Self::publish_namespace_cancel`]: this one ends an announcement of
1187    /// this endpoint's, that one revokes the acceptance of one the peer made.
1188    pub async fn publish_namespace_done(
1189        &mut self,
1190        track_namespace: TrackNamespace,
1191    ) -> Result<(), ConnectionError> {
1192        let msg = self.endpoint.publish_namespace_done(track_namespace)?;
1193        self.send_control(&msg).await
1194    }
1195    // ── Track Status flow ────────────────────────────────────
1196
1197    /// Send a TRACK_STATUS and return the allocated request ID.
1198    #[allow(clippy::too_many_arguments)]
1199    pub async fn track_status(
1200        &mut self,
1201        track_namespace: TrackNamespace,
1202        track_name: Vec<u8>,
1203        subscriber_priority: u8,
1204        group_order: GroupOrder,
1205        forward: Forward,
1206        filter_type: FilterType,
1207        parameters: Vec<KeyValuePair>,
1208    ) -> Result<VarInt, ConnectionError> {
1209        let (req_id, msg) = self.endpoint.track_status(
1210            track_namespace,
1211            track_name,
1212            subscriber_priority,
1213            group_order,
1214            forward,
1215            filter_type,
1216            parameters,
1217        )?;
1218        self.send_control(&msg).await?;
1219        Ok(req_id)
1220    }
1221
1222    /// Accept a track status the peer asked for, sending TRACK_STATUS_OK.
1223    ///
1224    /// The endpoint refuses a second answer to one request, so nothing is
1225    /// written on the wire when it does. It also chooses the Track Alias the
1226    /// message carries, because the draft leaves only one value open.
1227    pub async fn track_status_ok(
1228        &mut self,
1229        request_id: VarInt,
1230        expires: VarInt,
1231        group_order: GroupOrder,
1232        parameters: Vec<KeyValuePair>,
1233    ) -> Result<(), ConnectionError> {
1234        let msg =
1235            self.endpoint.send_track_status_ok(request_id, expires, group_order, parameters)?;
1236        self.send_control(&msg).await
1237    }
1238
1239    /// Refuse a track status the peer asked for, sending TRACK_STATUS_ERROR.
1240    ///
1241    /// The other answer the request can have, and the endpoint holds it to the
1242    /// same count of one.
1243    pub async fn track_status_error(
1244        &mut self,
1245        request_id: VarInt,
1246        error_code: VarInt,
1247        reason_phrase: Vec<u8>,
1248    ) -> Result<(), ConnectionError> {
1249        let msg = self.endpoint.send_track_status_error(request_id, error_code, reason_phrase)?;
1250        self.send_control(&msg).await
1251    }
1252
1253    // ── Publish flow (publisher side) ───────────────────────
1254
1255    /// Offer the peer a subscription to a track this endpoint publishes, and
1256    /// return the Request ID the offer was allocated.
1257    ///
1258    /// Nothing is written on the wire when the endpoint refuses to build the
1259    /// offer, which it does when the Track Alias is one another live track of
1260    /// this session already holds.
1261    #[allow(clippy::too_many_arguments)]
1262    pub async fn publish(
1263        &mut self,
1264        track_namespace: TrackNamespace,
1265        track_name: Vec<u8>,
1266        track_alias: VarInt,
1267        group_order: GroupOrder,
1268        largest_location: Option<Location>,
1269        forward: Forward,
1270        parameters: Vec<KeyValuePair>,
1271    ) -> Result<VarInt, ConnectionError> {
1272        let (req_id, msg) = self.endpoint.publish(
1273            track_namespace,
1274            track_name,
1275            track_alias,
1276            group_order,
1277            largest_location,
1278            forward,
1279            parameters,
1280        )?;
1281        self.send_control(&msg).await?;
1282        Ok(req_id)
1283    }
1284
1285    /// Send a PUBLISH_DONE for the given request ID.
1286    pub async fn publish_done(
1287        &mut self,
1288        request_id: VarInt,
1289        status_code: VarInt,
1290        reason_phrase: Vec<u8>,
1291    ) -> Result<(), ConnectionError> {
1292        let msg = self.endpoint.send_publish_done(request_id, status_code, reason_phrase)?;
1293        self.send_control(&msg).await
1294    }
1295
1296    // ── Malformed Tracks ────────────────────────────────────
1297
1298    /// Send what Section 2.5 asks for when this endpoint finds a track
1299    /// malformed.
1300    ///
1301    /// "it MUST UNSUBSCRIBE any subscription and FETCH_CANCEL any fetch for
1302    /// that Track from that publisher" — one message per request, in Request
1303    /// ID order, and the endpoint decides which message each request takes.
1304    ///
1305    /// A write that fails is not reported. The caller is on its way to
1306    /// returning an error that says what went wrong with the track, and a
1307    /// control stream that will not take an UNSUBSCRIBE is a session on its
1308    /// way out for a reason of its own; replacing the condition's report with
1309    /// a transport error would lose the only account of why the track was
1310    /// withdrawn. The rest of the withdrawal is abandoned, because a stream
1311    /// that refused one message will refuse the next.
1312    async fn withdraw_malformed_track(&self, alias: u64, condition: MalformedTrackCondition) {
1313        for msg in self.endpoint.withdraw_malformed_track(alias, condition) {
1314            if self.send_control(&msg).await.is_err() {
1315                break;
1316            }
1317        }
1318    }
1319
1320    /// Record the framing an arriving object was sent with, and withdraw from
1321    /// the track when it is the second framing that track has been sent.
1322    ///
1323    /// The receiving half of a pair. The two writing paths call the endpoint
1324    /// directly and answer a mixed track by refusing to write it, because
1325    /// Section 2.5's sentence is a subscriber's: an endpoint about to send an
1326    /// object is that object's Original Publisher, and a publisher has no
1327    /// subscription of its own to withdraw and no fetch of its own to cancel.
1328    async fn note_received_framing(
1329        &self,
1330        alias: u64,
1331        seen: ObjectForwardingPreference,
1332    ) -> Result<(), ConnectionError> {
1333        let Err(err) = self.endpoint.note_object_forwarding_preference(alias, seen) else {
1334            return Ok(());
1335        };
1336        self.withdraw_malformed_track(alias, MalformedTrackCondition::MixedForwardingPreference)
1337            .await;
1338        Err(err.into())
1339    }
1340
1341    // ── Data streams ────────────────────────────────────────
1342
1343    /// Open a new unidirectional stream for sending subgroup data.
1344    pub async fn open_subgroup_stream(
1345        &self,
1346        header: &AnySubgroupHeader,
1347    ) -> Result<FramedSendStream, ConnectionError> {
1348        // Before the stream is opened: the Original Publisher is who the rule
1349        // binds, so a header that would mix this track's framing is refused
1350        // here rather than written and answered by the peer.
1351        self.endpoint.note_object_forwarding_preference(
1352            header.track_alias(),
1353            ObjectForwardingPreference::Subgroup,
1354        )?;
1355        let send = self.transport.open_uni().await?;
1356        let mut framed = FramedSendStream::new(send, self.draft);
1357        let sid = framed.stream_id();
1358        framed.write_subgroup_header(header).await?;
1359        self.emit(ClientEvent::StreamOpened {
1360            direction: Direction::Send,
1361            stream_kind: StreamKind::Subgroup,
1362            stream_id: sid,
1363        });
1364        self.emit(ClientEvent::DataStreamHeader {
1365            stream_id: sid,
1366            direction: Direction::Send,
1367            header: header.clone(),
1368        });
1369        Ok(framed)
1370    }
1371
1372    /// Open a new unidirectional stream for sending a FETCH's objects.
1373    ///
1374    /// The objects answering a FETCH do not go on the request's own stream:
1375    /// they go on a unidirectional stream of their own, which opens with a
1376    /// FETCH_HEADER naming the request they belong to. This writes that header
1377    /// and hands back the stream, the same way
1378    /// [`open_subgroup_stream`](Self::open_subgroup_stream) does for a
1379    /// subgroup.
1380    ///
1381    /// The caller owns the stream that comes back. Nothing here remembers
1382    /// which request it belongs to, so an endpoint serving several fetches at
1383    /// once keeps its own map from Request ID to stream.
1384    pub async fn open_fetch_stream(
1385        &self,
1386        header: &AnyFetchHeader,
1387    ) -> Result<FramedSendStream, ConnectionError> {
1388        let send = self.transport.open_uni().await?;
1389        let mut framed = FramedSendStream::new(send, self.draft);
1390        let sid = framed.stream_id();
1391        framed.write_fetch_header(header).await?;
1392        self.emit(ClientEvent::StreamOpened {
1393            direction: Direction::Send,
1394            stream_kind: StreamKind::Fetch,
1395            stream_id: sid,
1396        });
1397        Ok(framed)
1398    }
1399
1400    /// Accept the next unidirectional stream and read its fetch header.
1401    ///
1402    /// [`accept_subgroup_stream`](Self::accept_subgroup_stream)'s twin. The two
1403    /// are separate because the header decides how every object after it is
1404    /// framed, so a caller has to know which it is expecting before the first
1405    /// byte is read.
1406    ///
1407    /// Objects come off the returned stream with
1408    /// [`FramedRecvStream::read_fetch_object`].
1409    pub async fn accept_fetch_stream(
1410        &self,
1411    ) -> Result<(AnyFetchHeader, FramedRecvStream), ConnectionError> {
1412        let recv = self.transport.accept_uni().await?;
1413        let mut framed = FramedRecvStream::new(recv, self.draft);
1414        let sid = framed.stream_id();
1415        let header = framed.read_fetch_header().await?;
1416        self.emit(ClientEvent::StreamOpened {
1417            direction: Direction::Receive,
1418            stream_kind: StreamKind::Fetch,
1419            stream_id: sid,
1420        });
1421        self.emit(ClientEvent::FetchStreamHeader {
1422            stream_id: sid,
1423            direction: Direction::Receive,
1424            header: header.clone(),
1425        });
1426        // A fetch header goes out as `FetchStreamHeader`; `DataStreamHeader`
1427        // carries an `AnySubgroupHeader` and cannot express one. What
1428        // `accept_subgroup_stream` does beyond this - the forwarding-preference
1429        // note, the object measurement - is about a subgroup and has no
1430        // counterpart on a fetch stream.
1431        Ok((header, framed))
1432    }
1433
1434    /// Accept an incoming unidirectional data stream and read its subgroup
1435    /// header.
1436    pub async fn accept_subgroup_stream(
1437        &self,
1438    ) -> Result<(AnySubgroupHeader, FramedRecvStream), ConnectionError> {
1439        let recv = self.transport.accept_uni().await?;
1440        let mut framed = FramedRecvStream::new(recv, self.draft);
1441        let sid = framed.stream_id();
1442        let header = framed.read_subgroup_header().await?;
1443        self.emit(ClientEvent::StreamOpened {
1444            direction: Direction::Receive,
1445            stream_kind: StreamKind::Subgroup,
1446            stream_id: sid,
1447        });
1448        self.emit(ClientEvent::DataStreamHeader {
1449            stream_id: sid,
1450            direction: Direction::Receive,
1451            header: header.clone(),
1452        });
1453        // Every object on a subgroup stream has the Subgroup preference, so
1454        // the header settles the track's framing before a single object is
1455        // read.
1456        self.note_received_framing(header.track_alias(), ObjectForwardingPreference::Subgroup)
1457            .await?;
1458        // The track is resolved here and not inside the stream: it takes the
1459        // endpoint's alias table, which a stream handle has no way back to.
1460        if let Some(objects) = self.endpoint.track_objects(header.track_alias()) {
1461            framed.measure_objects_against(objects, header.group_id());
1462        }
1463        Ok((header, framed))
1464    }
1465
1466    /// Send an object via datagram.
1467    ///
1468    /// The header goes through `AnyDatagramHeader::encode`, which refuses a
1469    /// header whose Object Status the framing it names cannot carry. Such a
1470    /// header errors here and nothing is sent, rather than going out as an
1471    /// ordinary payload datagram with the status quietly dropped.
1472    pub fn send_datagram(
1473        &self,
1474        header: &AnyDatagramHeader,
1475        payload: &[u8],
1476    ) -> Result<(), ConnectionError> {
1477        // Before anything is encoded, for the reason `open_subgroup_stream`
1478        // gives.
1479        self.endpoint.note_object_forwarding_preference(
1480            header.meta().track_alias,
1481            ObjectForwardingPreference::Datagram,
1482        )?;
1483        let mut buf = Vec::new();
1484        header.encode(&mut buf)?;
1485        buf.extend_from_slice(payload);
1486        self.emit(ClientEvent::DatagramReceived {
1487            direction: Direction::Send,
1488            header: header.clone(),
1489            payload_len: payload.len(),
1490        });
1491        self.transport.send_datagram(bytes::Bytes::from(buf))?;
1492        Ok(())
1493    }
1494
1495    /// Receive a datagram and decode its header.
1496    pub async fn recv_datagram(&self) -> Result<(AnyDatagramHeader, Bytes), ConnectionError> {
1497        let data = self.transport.recv_datagram().await?;
1498        let mut cursor = &data[..];
1499        let header = AnyDatagramHeader::decode(self.draft, &mut cursor)?;
1500        let consumed = data.len() - cursor.len();
1501        let payload = data.slice(consumed..);
1502        self.emit(ClientEvent::DatagramReceived {
1503            direction: Direction::Receive,
1504            header: header.clone(),
1505            payload_len: payload.len(),
1506        });
1507        // A datagram is the other framing, and it settles the track's just as a
1508        // subgroup header does.
1509        self.note_received_framing(header.meta().track_alias, ObjectForwardingPreference::Datagram)
1510            .await?;
1511        // A datagram is a whole object, so the connection can measure it
1512        // without help from the caller - and answer the condition itself,
1513        // because an UNSUBSCRIBE takes the connection an object on a stream
1514        // cannot reach.
1515        let meta = header.meta();
1516        if let Err(err) = self.endpoint.note_received_object(
1517            meta.track_alias,
1518            ObjectLocation { group: meta.group_id, object: meta.object_id },
1519            object_role(meta.status),
1520        ) {
1521            self.withdraw_malformed_track(
1522                meta.track_alias,
1523                MalformedTrackCondition::ObjectPastFinalObject,
1524            )
1525            .await;
1526            return Err(err.into());
1527        }
1528        Ok((header, payload))
1529    }
1530
1531    // ── Accessors ───────────────────────────────────────────
1532
1533    /// Access the underlying endpoint state machine.
1534    pub fn endpoint(&self) -> &Endpoint {
1535        &self.endpoint
1536    }
1537
1538    /// Mutable access to the endpoint state machine.
1539    pub fn endpoint_mut(&mut self) -> &mut Endpoint {
1540        &mut self.endpoint
1541    }
1542
1543    /// Get the negotiated MoQT version.
1544    pub fn negotiated_version(&self) -> Option<VarInt> {
1545        self.endpoint.negotiated_version()
1546    }
1547
1548    /// Returns the draft version this connection is using.
1549    pub fn draft(&self) -> DraftVersion {
1550        self.draft
1551    }
1552
1553    /// The code to close the session with when a message could not be decoded
1554    /// because the peer broke a rule draft-14 answers with a close.
1555    ///
1556    /// Every variant listed here comes from a sentence in this draft that names
1557    /// the consequence, and the list is per draft: answering a bound this draft
1558    /// does not state would close a session over traffic a conforming peer may
1559    /// send.
1560    ///
1561    ///   - Reason Phrase, maximum 1024 bytes: "If an endpoint receives a length
1562    ///     exceeding the maximum, it MUST close the session with a
1563    ///     PROTOCOL_VIOLATION."
1564    ///   - GOAWAY New Session URI, maximum 8,192 bytes, with the same sentence.
1565    ///     Drafts 11 through 19 state it; 07 through 10 state no maximum for
1566    ///     the field at all.
1567    ///   - Key-Value-Pair value, maximum 2^16-1 bytes, with the same sentence.
1568    ///   - Track Namespace tuple size: "If an endpoint receives a Track
1569    ///     Namespace tuple with an N of 0 or more than 32, it MUST close the
1570    ///     session with a Protocol Violation." Note the lower bound - an empty
1571    ///     tuple is refused here, where drafts 17 and later permit one.
1572    ///   - Full Track Name, maximum 4,096 bytes, "computed as the sum of the
1573    ///     lengths of each Track Namespace tuple field and the Track Name
1574    ///     length field". This draft bounds the pair and not the namespace
1575    ///     alone; draft-16 widened it.
1576    ///   - Duplicate parameters, a SHOULD rather than a MUST: "Receivers SHOULD
1577    ///     check that there are no unauthorized duplicate parameters and close
1578    ///     the session as a PROTOCOL_VIOLATION if found." The rule is
1579    ///     asymmetric here - "Receivers MUST allow duplicates of unknown
1580    ///     parameters", and one known type is granted repeats - and the codec
1581    ///     reports only the repeats this draft actually forbids.
1582    ///   - Unknown control message type: "An endpoint that receives an unknown
1583    ///     message type MUST close the session."
1584    ///   - Extension headers on an Object whose status is Object Does Not
1585    ///     Exist, Section 10.2.1.2. That one arrives on a data stream or a
1586    ///     datagram, so [`Connection::close_for_data_stream`] is what carries
1587    ///     it.
1588    ///
1589    /// **Not** the zero-length Track Namespace Field, the delta-encoded
1590    /// parameter type overflow, or the Object ID delta wrap. Those enter the
1591    /// specification at drafts 16 and 18, and this draft states none of them.
1592    ///
1593    /// **Not** the 2^16-1 control message length either. This draft states the
1594    /// limit - "the total length of a control message is limited to 2^16-1
1595    /// bytes" - and states no consequence for exceeding it, so an oversized
1596    /// message is refused by the decoder and stops there.
1597    ///
1598    /// **Not** [`CodecError::UnexpectedEnd`], which reports no rule at all: the
1599    /// reader raises it whenever a message is still arriving, and
1600    /// `read_control` loops on it. Closing over it would end a session on an
1601    /// ordinary short read.
1602    ///
1603    /// **Not** the unknown Message Parameter rule. Drafts 16 through 19 require
1604    /// a close for a Message Parameter whose type the negotiated version does
1605    /// not define. This draft states the opposite and states it about the same
1606    /// parameters: "Receivers MUST allow duplicates of unknown parameters",
1607    /// which presumes an unknown parameter arrives and is carried. Refusing one
1608    /// here would close a session over an extension this draft leaves room for.
1609    ///
1610    /// `None` for everything else, including [`CodecError::InvalidField`]. That
1611    /// variant is shared by a dozen unrelated malformations, only some of which
1612    /// the draft answers with a close, so a session cannot be ended on it
1613    /// without ending sessions the draft does not ask to be ended. Splitting it
1614    /// is the way to bring the rest of those rules under this function;
1615    /// widening the match is not - the extension-header rule above reached this
1616    /// table by being split out of it.
1617    fn codec_session_error_code(
1618        err: &CodecError,
1619    ) -> Option<moqtap_codec::draft14::error_codes::SessionErrorCode> {
1620        use moqtap_codec::draft14::error_codes::SessionErrorCode;
1621        use moqtap_codec::kvp::KvpError;
1622        match err {
1623            // The declared Length disagreeing with the fields, which every
1624            // draft answers with a close. Drafts 07 through 10 name no code for
1625            // it, so it takes the one their other unnamed rules take.
1626            // A Filter Type outside the four this draft assigns, Section 9.7:
1627            // "An endpoint that receives a filter type other than the above MUST
1628            // be close the session with PROTOCOL_VIOLATION" — the missing word
1629            // is the draft's. This is the draft the rule gained a consequence
1630            // on. Drafts 07 through 13 write the same sentence as "MUST be
1631            // treated as error", which names none - draft-13 Section 8.7 among
1632            // them - and their tables leave it unanswered.
1633            CodecError::InvalidFilterType(_) => Some(SessionErrorCode::ProtocolViolation),
1634            // A Fetch Type outside the three this draft assigns, Section
1635            // 9.16: "An endpoint that receives a Fetch Type other than 0x1,
1636            // 0x2 or 0x3 MUST be close the session with a PROTOCOL_VIOLATION."
1637            // The missing word is the draft's, as it is for the filter type
1638            // above. The value decides which
1639            // fields follow it — a Standalone fetch carries a track name and a
1640            // range where a joining fetch carries a Request ID and an offset —
1641            // so a reader that cannot name the type cannot find the end of the
1642            // message.
1643            CodecError::InvalidFetchType(_) => Some(SessionErrorCode::ProtocolViolation),
1644            CodecError::ControlMessageLengthMismatch { .. } => {
1645                Some(SessionErrorCode::ProtocolViolation)
1646            }
1647            CodecError::ReasonPhraseTooLong
1648            | CodecError::GoAwayUriTooLong
1649            | CodecError::InvalidNamespaceTupleSize(_)
1650            | CodecError::TrackNameTooLong
1651            | CodecError::DuplicateParameter(_)
1652            | CodecError::UnknownMessageType(_)
1653            | CodecError::ExtensionsOnNonExistentObject(_)
1654            | CodecError::Kvp(KvpError::ValueTooLong(_)) => {
1655                Some(SessionErrorCode::ProtocolViolation)
1656            }
1657            // An unknown data-plane type, Section 10: "An endpoint that
1658            // receives an unknown stream or datagram type MUST close the
1659            // session." One sentence covering two tables, which is why both
1660            // variants sit here.
1661            // A Content Exists field that is neither zero nor one, Sections 9.8
1662            // and 9.13: "Any other value is a protocol error and MUST terminate
1663            // the session with a PROTOCOL_VIOLATION".
1664            CodecError::InvalidContentExists(_) => Some(SessionErrorCode::ProtocolViolation),
1665            // A Forward field that is neither zero nor one. Sections 9.7 and
1666            // 9.10 use the sentence above; Section 9.13 says "Any value other
1667            // than 0 or 1 is a PROTOCOL_VIOLATION". Section 9.14 names the two
1668            // legal values without a consequence and is answered the same way,
1669            // for the reason given on the variant. Draft-15 replaces the field
1670            // with the FORWARD parameter, which carries the same rule in a
1671            // different shape and reports it as `ParameterValueOutOfRange`.
1672            CodecError::InvalidForward(_) => Some(SessionErrorCode::ProtocolViolation),
1673            CodecError::UnknownStreamType(_) | CodecError::UnknownDatagramType(_) => {
1674                Some(SessionErrorCode::ProtocolViolation)
1675            }
1676            // A key-value pair whose value is not the serialization its own
1677            // Type defines, Section 1.4.2: "If a receiver understands a Type,
1678            // and the following Value or Length/Value does not match the
1679            // serialization defined by that Type, the receiver MUST terminate the
1680            // session with error code KEY_VALUE_FORMATTING_ERROR."
1681            //
1682            // Section 9.2.1.1 states the same answer for the one structure this
1683            // draft spells out: "If the Token structure cannot be decoded, the
1684            // receiver MUST close the Session with Key-Value Formatting error."
1685            //
1686            // The one rule in this table that names a code other than Protocol
1687            // Violation.
1688            CodecError::KeyValueFormatting { .. } => {
1689                Some(SessionErrorCode::KeyValueFormattingError)
1690            }
1691            // Everything this draft does not answer, named rather than swept up
1692            // by a wildcard. The arm is exhaustive deliberately: a new
1693            // `CodecError` variant will not compile until it has been placed on
1694            // one side or the other, on this draft, which is the decision a `_`
1695            // arm makes silently and invisibly on all thirteen at once.
1696            //
1697            // Adding one variant to `CodecError` was tried, and produces
1698            // thirteen `E0004`s, one per draft, each naming the variant that has
1699            // nowhere to go. That is the whole mechanism.
1700            //
1701            // The nesting stops at `VarInt`, whose variants report how the bytes
1702            // ran out rather than a rule an endpoint states, so there is nothing
1703            // in it for a draft to answer. `Kvp` is spelled out because it does
1704            // carry one.
1705            // Not `ParameterValueOutOfRange`: no parameter this draft defines
1706            // restricts its value's range. Forwarding is a message field here
1707            // and is answered above.
1708            CodecError::ParameterValueOutOfRange { .. }
1709            | CodecError::UnexpectedEnd
1710            | CodecError::MessageTooLong(_)
1711            | CodecError::VarInt(_)
1712            | CodecError::InvalidField
1713            | CodecError::EmptyNamespaceField
1714            | CodecError::InvalidRange(..)
1715            | CodecError::ParameterLengthMismatch(_)
1716            | CodecError::EndOfTrackObjectId(_)
1717            | CodecError::KeyDeltaOverflow(..)
1718            // Not `TrackPropertyValueOutOfRange`: this draft has neither
1719            // namespace the variant is about. Draft-16 opens an extension header
1720            // registry with value rules of its own, and draft-17 renames it to
1721            // the Track Property registry. Before that, everything with a
1722            // restricted range is either a message field or a Message Parameter.
1723            | CodecError::TrackPropertyValueOutOfRange { .. }
1724            | CodecError::ParametersOutOfOrder(..)
1725            | CodecError::ObjectIdOverflow(..)
1726            | CodecError::InvalidRequiredRequestIdDelta(..)
1727            | CodecError::InvalidTypeValue { .. }
1728            | CodecError::UnknownMessageParameter(_)
1729            // Not `ParameterOutOfScope`: this draft states the scope rule and
1730            // answers it the other way. Section 9.2.1 Version Specific Parameters: "Each
1731            // version-specific parameter definition indicates the message types in which it can
1732            // appear. If it appears in some other type of message, it MUST be
1733            // ignored." The codec carries such a parameter on this draft and never
1734            // raises the variant, so this arm records a rule this draft has and
1735            // does not close over, not one it is missing. Draft-17 is where the
1736            // second sentence becomes a close.
1737            | CodecError::ParameterOutOfScope { .. }
1738            // Both belong to the parameter form of the filter. Draft-15 moved
1739            // the Filter Type, Start Location and End Group out of SUBSCRIBE and
1740            // into one length-prefixed parameter; this draft still carries them
1741            // as fields, where a value that runs short is a truncated frame and
1742            // an End Group is written out rather than added to anything.
1743            | CodecError::SubscriptionFilterMalformed { .. }
1744            | CodecError::FilterEndGroupOverflow { .. }
1745            // The object payload rule, Section 10.2.1.1: "Any object with a status
1746            // code other than zero MUST have an empty payload." A MUST on the
1747            // sender with no receiver action named anywhere — the "SHOULD be
1748            // treated as a protocol error" in the same paragraph belongs to the
1749            // sentence before it, which is about a status value this draft does
1750            // not assign — so an object carrying a payload it may not is refused
1751            // and the session stays open.
1752            //
1753            // That was already the answer. The bytes used to arrive as
1754            // `InvalidField`, which is on this side too; naming the rule changes
1755            // nothing a peer can observe and makes the decision legible.
1756            | CodecError::PayloadNotPermitted { .. }
1757            | CodecError::UnsupportedDraft(_)
1758            | CodecError::Kvp(
1759                KvpError::MissingLength | KvpError::UnexpectedEnd | KvpError::VarInt(_),
1760            ) => None,
1761        }
1762    }
1763
1764    /// Close the session on the wire when a decode failure is one draft-14
1765    /// answers with a close, and hand the error back unchanged.
1766    /// Without it every bound the decoder enforces would stop at *this endpoint
1767    /// refused the frame* while the peer, which is the one that broke the rule,
1768    /// saw a session that was still open and went on sending. "MUST close the
1769    /// session" is a statement about the wire.
1770    fn close_for_codec(&self, err: ConnectionError) -> ConnectionError {
1771        if let ConnectionError::Codec(inner) = &err {
1772            if let Some(code) = Self::codec_session_error_code(inner) {
1773                // QUIC application error codes are 62-bit; every code in this
1774                // registry is far below `u32::MAX`, and saturating rather than
1775                // truncating means a future code that is not could never be
1776                // reported as a different, assigned one.
1777                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1778                self.close(wire_code, inner.to_string().as_bytes());
1779            }
1780        }
1781        err
1782    }
1783
1784    /// Close the session on the wire when the endpoint says a violation is
1785    /// fatal to it, and hand the error back unchanged.
1786    ///
1787    /// [`EndpointError::session_error_code`] answers `Some` for exactly the
1788    /// errors this draft ends the session over, and the endpoint has already
1789    /// moved its own state machine to Closed by the time this runs. Without
1790    /// this step that move is purely internal: the local endpoint refuses to
1791    /// start anything new while the peer, which is the one that broke the
1792    /// rule, sees a session that is still open and goes on sending. A rule
1793    /// that names a session termination code is a statement about the wire,
1794    /// so it takes a CONNECTION_CLOSE to satisfy it.
1795    ///
1796    /// The reason phrase is the error's own `Display` text, which names the
1797    /// rule rather than repeating the numeric code the close already carries.
1798    ///
1799    /// Errors that answer `None` are recoverable and nothing is sent.
1800    fn close_for(&self, err: &EndpointError) {
1801        if let Some(code) = err.session_error_code() {
1802            // QUIC application error codes are 62-bit; every code in this
1803            // registry is far below `u32::MAX`, and saturating rather than
1804            // truncating means a future code that is not could never be
1805            // reported as a different, assigned one.
1806            let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1807            self.close(wire_code, err.to_string().as_bytes());
1808        }
1809    }
1810
1811    /// [`close_for`](Self::close_for), then the error unchanged, for the
1812    /// common case where the endpoint's error is also what the caller returns.
1813    fn close_if_session_fatal(&self, err: EndpointError) -> ConnectionError {
1814        self.close_for(&err);
1815        ConnectionError::Endpoint(err)
1816    }
1817
1818    /// Withdraw from a track a data stream found malformed, reporting whether
1819    /// it did.
1820    ///
1821    /// The Malformed Track twin of [`Connection::close_for_data_stream`], and
1822    /// separate from it for the same reason and one more. The same one: a
1823    /// [`FramedRecvStream`] holds no connection, so the reader that finds the
1824    /// fault is not the object that can send an UNSUBSCRIBE. The one more: the
1825    /// two answers are opposites - that call ends the session, this one gives
1826    /// up a track and leaves it running - and a single entry point would have
1827    /// to decide between them from the error alone, which is exactly the
1828    /// decision a caller reproducing a capture wants to make itself.
1829    ///
1830    /// The datagram path needs none of this. It is read through the connection,
1831    /// so [`Connection::recv_datagram`] answers the condition where it finds
1832    /// it, and this is only for the objects that arrive on a stream the caller
1833    /// holds.
1834    pub async fn withdraw_for_data_stream(&self, err: &ConnectionError) -> bool {
1835        let ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject { alias, .. }) = err
1836        else {
1837            return false;
1838        };
1839        self.withdraw_malformed_track(*alias, MalformedTrackCondition::ObjectPastFinalObject).await;
1840        true
1841    }
1842
1843    /// Close the session over a rule broken on a data stream, reporting whether
1844    /// it did.
1845    ///
1846    /// A data stream cannot close for itself the way `recv_control` does:
1847    /// [`Connection::accept_subgroup_stream`] hands the caller a
1848    /// [`FramedRecvStream`] holding no connection, so the reader that finds the
1849    /// violation is not the object that can act on it. Keeping it a separate
1850    /// call is deliberate as well - a permissive caller, one reproducing a
1851    /// capture, can read a violating stream and report it without tearing the
1852    /// session down.
1853    ///
1854    /// The rule this draft answers here is extension headers on an Object whose
1855    /// status is Object Does Not Exist, which reaches subgroup streams, fetch
1856    /// streams and status datagrams alike. It shares `codec_session_error_code`
1857    /// with the control path, so a rule is answered with one code whichever
1858    /// stream carried it.
1859    ///
1860    /// Not every rule that reaches here is the decoder's. A track whose objects
1861    /// mix forwarding preferences is the endpoint's to notice — it takes the
1862    /// alias table to know which track an object belongs to — and it arrives on
1863    /// exactly these streams. Both kinds are asked for a code the same way, and
1864    /// a rule with no code is declined rather than guessed at.
1865    pub fn close_for_data_stream(&self, err: &ConnectionError) -> bool {
1866        match err {
1867            ConnectionError::Codec(inner) => {
1868                let Some(code) = Self::codec_session_error_code(inner) else { return false };
1869                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1870                self.close(wire_code, inner.to_string().as_bytes());
1871                true
1872            }
1873            // A rule the endpoint raises rather than the decoder. The two
1874            // reach their codes through different tables and mean the same
1875            // thing here: `Some` is a rule this draft ends the session over.
1876            ConnectionError::Endpoint(inner) => {
1877                let Some(code) = inner.session_error_code() else { return false };
1878                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1879                self.close(wire_code, inner.to_string().as_bytes());
1880                true
1881            }
1882            _ => false,
1883        }
1884    }
1885
1886    /// Close the connection.
1887    pub fn close(&self, code: u32, reason: &[u8]) {
1888        self.emit(ClientEvent::Closed { code, reason: reason.to_vec() });
1889        self.transport.close(code, reason);
1890    }
1891}
1892
1893/// Determine the encoded length of a varint from its first byte.
1894fn varint_len(first_byte: u8) -> usize {
1895    1 << (first_byte >> 6)
1896}
1897
1898#[cfg(test)]
1899mod tests {
1900    use super::*;
1901
1902    #[test]
1903    fn varint_len_single_byte() {
1904        // 0b00xxxxxx -> 1 byte
1905        assert_eq!(varint_len(0x00), 1);
1906        assert_eq!(varint_len(0x3F), 1);
1907    }
1908
1909    #[test]
1910    fn varint_len_two_bytes() {
1911        // 0b01xxxxxx -> 2 bytes
1912        assert_eq!(varint_len(0x40), 2);
1913        assert_eq!(varint_len(0x7F), 2);
1914    }
1915
1916    #[test]
1917    fn varint_len_four_bytes() {
1918        // 0b10xxxxxx -> 4 bytes
1919        assert_eq!(varint_len(0x80), 4);
1920        assert_eq!(varint_len(0xBF), 4);
1921    }
1922
1923    #[test]
1924    fn varint_len_eight_bytes() {
1925        // 0b11xxxxxx -> 8 bytes
1926        assert_eq!(varint_len(0xC0), 8);
1927        assert_eq!(varint_len(0xFF), 8);
1928    }
1929
1930    #[test]
1931    fn client_config_supported_versions_draft14() {
1932        let config = ClientConfig {
1933            draft: DraftVersion::Draft14,
1934            additional_versions: Vec::new(),
1935            transport: TransportType::Quic,
1936            skip_cert_verification: false,
1937            ca_certs: Vec::new(),
1938            setup_parameters: Vec::new(),
1939        };
1940        let versions = config.supported_versions();
1941        assert_eq!(versions.len(), 1);
1942        assert_eq!(versions[0].into_inner(), 0xff000000 + 14);
1943    }
1944
1945    #[test]
1946    fn client_config_supported_versions_draft07() {
1947        let config = ClientConfig {
1948            draft: DraftVersion::Draft07,
1949            additional_versions: Vec::new(),
1950            transport: TransportType::Quic,
1951            skip_cert_verification: false,
1952            ca_certs: Vec::new(),
1953            setup_parameters: Vec::new(),
1954        };
1955        let versions = config.supported_versions();
1956        assert_eq!(versions.len(), 1);
1957        assert_eq!(versions[0].into_inner(), 0xff000000 + 7);
1958    }
1959
1960    #[test]
1961    fn client_config_alpn_quic() {
1962        let config = ClientConfig {
1963            draft: DraftVersion::Draft14,
1964            additional_versions: Vec::new(),
1965            transport: TransportType::Quic,
1966            skip_cert_verification: false,
1967            ca_certs: Vec::new(),
1968            setup_parameters: Vec::new(),
1969        };
1970        assert_eq!(config.alpn(), vec![b"moq-00".to_vec()]);
1971    }
1972
1973    #[test]
1974    fn client_config_alpn_webtransport() {
1975        let config = ClientConfig {
1976            draft: DraftVersion::Draft14,
1977            additional_versions: Vec::new(),
1978            transport: TransportType::WebTransport { url: "https://example.com".to_string() },
1979            skip_cert_verification: false,
1980            ca_certs: Vec::new(),
1981            setup_parameters: Vec::new(),
1982        };
1983        assert_eq!(config.alpn(), vec![b"h3".to_vec()]);
1984    }
1985
1986    #[test]
1987    fn moqt_alpn_value() {
1988        assert_eq!(MOQT_ALPN, b"moq-00");
1989    }
1990
1991    #[test]
1992    fn transport_type_debug() {
1993        let quic = TransportType::Quic;
1994        assert!(format!("{quic:?}").contains("Quic"));
1995
1996        let wt = TransportType::WebTransport { url: "https://example.com".to_string() };
1997        assert!(format!("{wt:?}").contains("WebTransport"));
1998    }
1999}