Skip to main content

moqtap_client/draft09/
connection.rs

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