Skip to main content

moqtap_client/draft07/
connection.rs

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