Skip to main content

moqtap_client/draft10/
connection.rs

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