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