Skip to main content

h3i/
frame.rs

1// Copyright (C) 2024, Cloudflare, Inc.
2// All rights reserved.
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are
6// met:
7//
8//     * Redistributions of source code must retain the above copyright notice,
9//       this list of conditions and the following disclaimer.
10//
11//     * Redistributions in binary form must reproduce the above copyright
12//       notice, this list of conditions and the following disclaimer in the
13//       documentation and/or other materials provided with the distribution.
14//
15// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
16// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
17// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
19// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
27//! Helpers for dealing with quiche stream events and HTTP/3 frames.
28
29use std::cmp;
30use std::convert::TryFrom;
31use std::error::Error;
32use std::fmt::Debug;
33use std::sync::Arc;
34
35use crate::quiche;
36use multimap::MultiMap;
37
38use quiche::h3::frame::Frame as QFrame;
39use quiche::h3::Header;
40use quiche::h3::NameValue;
41use serde::ser::SerializeStruct;
42use serde::ser::Serializer;
43use serde::Serialize;
44
45use crate::client::connection_summary::MAX_SERIALIZED_BUFFER_LEN;
46use crate::encode_header_block;
47
48pub type BoxError = Box<dyn Error + Send + Sync + 'static>;
49
50/// An internal representation of a QUIC or HTTP/3 frame. This type exists so
51/// that we can extend types defined in Quiche.
52#[derive(Debug, Eq, PartialEq, Clone)]
53pub enum H3iFrame {
54    /// A wrapper around a quiche HTTP/3 frame.
55    QuicheH3(QFrame),
56    /// A wrapper around an [EnrichedHeaders] struct.
57    Headers(EnrichedHeaders),
58    /// A wrapper around a [ResetStream] struct
59    ResetStream(ResetStream),
60}
61
62impl H3iFrame {
63    /// Try to convert this `H3iFrame` to an [EnrichedHeaders].
64    ///
65    /// Returns `Some` if the operation succeeded.
66    pub fn to_enriched_headers(&self) -> Option<EnrichedHeaders> {
67        if let H3iFrame::Headers(header) = self {
68            Some(header.clone())
69        } else {
70            None
71        }
72    }
73}
74
75impl Serialize for H3iFrame {
76    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
77    where
78        S: Serializer,
79    {
80        match self {
81            H3iFrame::QuicheH3(frame) => {
82                let mut state = s.serialize_struct("frame", 1)?;
83                let name = frame_name(frame);
84                state.serialize_field(name, &SerializableQFrame(frame))?;
85                state.end()
86            },
87            H3iFrame::Headers(headers) => {
88                let mut state = s.serialize_struct("enriched_headers", 1)?;
89                state.serialize_field("enriched_headers", headers)?;
90                state.end()
91            },
92            H3iFrame::ResetStream(reset) => {
93                let mut state = s.serialize_struct("reset_stream", 1)?;
94                state.serialize_field("reset_stream", reset)?;
95                state.end()
96            },
97        }
98    }
99}
100
101impl From<QFrame> for H3iFrame {
102    fn from(value: QFrame) -> Self {
103        Self::QuicheH3(value)
104    }
105}
106
107impl From<Vec<Header>> for H3iFrame {
108    fn from(value: Vec<Header>) -> Self {
109        Self::Headers(EnrichedHeaders::from(value))
110    }
111}
112
113pub type HeaderMap = MultiMap<Vec<u8>, Vec<u8>>;
114
115/// An HTTP/3 HEADERS frame with decoded headers and a [HeaderMap].
116#[derive(Clone, PartialEq, Eq)]
117pub struct EnrichedHeaders {
118    header_block: Vec<u8>,
119    headers: Vec<Header>,
120    /// A multi-map of raw header names to values, similar to http's HeaderMap.
121    header_map: HeaderMap,
122}
123
124/// A wrapper to help serialize an quiche HTTP header.
125pub struct SerializableHeader<'a>(&'a Header);
126
127impl Serialize for SerializableHeader<'_> {
128    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
129    where
130        S: Serializer,
131    {
132        let mut state = s.serialize_struct("header", 2)?;
133        state.serialize_field("name", &String::from_utf8_lossy(self.0.name()))?;
134        state
135            .serialize_field("value", &String::from_utf8_lossy(self.0.value()))?;
136        state.end()
137    }
138}
139
140impl EnrichedHeaders {
141    /// Return the array of headers in this frame.
142    ///
143    /// # Examples
144    /// ```
145    /// use h3i::frame::EnrichedHeaders;
146    /// use quiche::h3::Header;
147    ///
148    /// let raw = vec![
149    ///     Header::new(b"new jersey", b"devils"),
150    ///     Header::new(b"new york", b"jets"),
151    /// ];
152    /// let headers = EnrichedHeaders::from(raw.clone());
153    /// assert_eq!(headers.headers(), raw);
154    /// ```
155    pub fn headers(&self) -> &[Header] {
156        &self.headers
157    }
158
159    /// Returns a multi-map of header keys to values.
160    ///
161    /// If a single key contains multiple values, the values in the entry will
162    /// be returned in the same order as they appear in the array of headers
163    /// which backs the [`EnrichedHeaders`].
164    ///
165    /// # Examples
166    /// ```
167    /// use h3i::frame::EnrichedHeaders;
168    /// use h3i::frame::H3iFrame;
169    /// use multimap::MultiMap;
170    /// use quiche::h3::Header;
171    /// use std::iter::FromIterator;
172    ///
173    /// let header_frame = vec![
174    ///     Header::new(b":status", b"200"),
175    ///     Header::new(b"hello", b"world"),
176    ///     Header::new(b"hello", b"super-earth"),
177    /// ];
178    ///
179    /// let enriched = H3iFrame::Headers(header_frame.into())
180    ///     .to_enriched_headers()
181    ///     .unwrap();
182    ///
183    /// let expected = MultiMap::from_iter([
184    ///     (b":status".to_vec(), vec![b"200".to_vec()]),
185    ///     (b"hello".to_vec(), vec![
186    ///         b"world".to_vec(),
187    ///         b"super-earth".to_vec(),
188    ///     ]),
189    /// ]);
190    ///
191    /// assert_eq!(*enriched.header_map(), expected);
192    /// ```
193    pub fn header_map(&self) -> &HeaderMap {
194        &self.header_map
195    }
196
197    /// Fetches the value of the `:status` pseudo-header.
198    ///
199    /// # Examples
200    /// ```
201    /// use h3i::frame::EnrichedHeaders;
202    /// use quiche::h3::Header;
203    ///
204    /// let headers = EnrichedHeaders::from(vec![Header::new(b"hello", b"world")]);
205    /// assert!(headers.status_code().is_none());
206    ///
207    /// let headers = EnrichedHeaders::from(vec![Header::new(b":status", b"200")]);
208    /// assert_eq!(headers.status_code().expect("status code is Some"), b"200");
209    /// ```
210    pub fn status_code(&self) -> Option<&Vec<u8>> {
211        self.header_map.get(b":status".as_slice())
212    }
213}
214
215impl Serialize for EnrichedHeaders {
216    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
217    where
218        S: Serializer,
219    {
220        let mut state = s.serialize_struct("enriched_headers", 2)?;
221        state.serialize_field("header_block_len", &self.header_block.len())?;
222        let x: Vec<SerializableHeader> =
223            self.headers.iter().map(SerializableHeader).collect();
224        state.serialize_field("headers", &x)?;
225        state.end()
226    }
227}
228
229impl From<Vec<Header>> for EnrichedHeaders {
230    fn from(headers: Vec<Header>) -> Self {
231        let header_block = encode_header_block(&headers).unwrap();
232
233        let mut header_map: HeaderMap = MultiMap::with_capacity(headers.len());
234        for header in headers.iter() {
235            header_map.insert(header.name().to_vec(), header.value().to_vec());
236        }
237
238        Self {
239            header_block,
240            headers,
241            header_map,
242        }
243    }
244}
245
246impl TryFrom<QFrame> for EnrichedHeaders {
247    type Error = BoxError;
248
249    fn try_from(value: QFrame) -> Result<Self, Self::Error> {
250        match value {
251            QFrame::Headers { header_block } => {
252                let mut qpack_decoder = quiche::h3::qpack::Decoder::new();
253                let headers = qpack_decoder.decode(&header_block, u64::MAX)?;
254
255                Ok(EnrichedHeaders::from(headers))
256            },
257            _ => Err("Cannot convert non-Headers frame into HeadersFrame".into()),
258        }
259    }
260}
261
262impl Debug for EnrichedHeaders {
263    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264        write!(f, "{:?}", self.headers)
265    }
266}
267
268/// A `RESET_STREAM` frame.
269///
270/// See [RFC 9000](https://datatracker.ietf.org/doc/html/rfc9000#name-reset_stream-frames) for
271/// more.
272#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
273pub struct ResetStream {
274    /// The stream ID over which the RESET_STREAM frame was sent.
275    pub stream_id: u64,
276    /// The error code sent from the peer.
277    pub error_code: u64,
278}
279
280fn frame_name(frame: &QFrame) -> &'static str {
281    match frame {
282        QFrame::Data { .. } => "DATA",
283        QFrame::Headers { .. } => "HEADERS",
284        QFrame::CancelPush { .. } => "CANCEL_PUSH",
285        QFrame::Settings { .. } => "SETTINGS",
286        QFrame::PushPromise { .. } => "PUSH_PROMISE",
287        QFrame::GoAway { .. } => "GO_AWAY",
288        QFrame::MaxPushId { .. } => "MAX_PUSH_ID",
289        QFrame::PriorityUpdateRequest { .. } => "PRIORITY_UPDATE(REQUEST)",
290        QFrame::PriorityUpdatePush { .. } => "PRIORITY_UPDATE(PUSH)",
291        QFrame::Unknown { .. } => "UNKNOWN",
292    }
293}
294
295/// A wrapper to help serialize a quiche HTTP/3 frame.
296pub struct SerializableQFrame<'a>(&'a QFrame);
297
298impl Serialize for SerializableQFrame<'_> {
299    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
300    where
301        S: Serializer,
302    {
303        let name = frame_name(self.0);
304        match self.0 {
305            QFrame::Data { payload } => {
306                let mut state = s.serialize_struct(name, 2)?;
307                let max = cmp::min(payload.len(), MAX_SERIALIZED_BUFFER_LEN);
308                state.serialize_field("payload_len", &payload.len())?;
309                state.serialize_field(
310                    "payload",
311                    &qlog::HexSlice::maybe_string(Some(&payload[..max])),
312                )?;
313                state.end()
314            },
315
316            QFrame::Headers { header_block } => {
317                let mut state = s.serialize_struct(name, 1)?;
318                state.serialize_field("header_block_len", &header_block.len())?;
319                state.end()
320            },
321
322            QFrame::CancelPush { push_id } => {
323                let mut state = s.serialize_struct(name, 1)?;
324                state.serialize_field("push_id", &push_id)?;
325                state.end()
326            },
327
328            QFrame::Settings {
329                max_field_section_size,
330                qpack_max_table_capacity,
331                qpack_blocked_streams,
332                connect_protocol_enabled,
333                h3_datagram,
334                grease: _,
335                additional_settings,
336                raw: _,
337            } => {
338                let mut state = s.serialize_struct(name, 6)?;
339                state.serialize_field(
340                    "max_field_section_size",
341                    &max_field_section_size,
342                )?;
343                state.serialize_field(
344                    "qpack_max_table_capacity",
345                    &qpack_max_table_capacity,
346                )?;
347                state.serialize_field(
348                    "qpack_blocked_streams",
349                    &qpack_blocked_streams,
350                )?;
351                state.serialize_field(
352                    "connect_protocol_enabled",
353                    &connect_protocol_enabled,
354                )?;
355                state.serialize_field("h3_datagram", &h3_datagram)?;
356                state.serialize_field(
357                    "additional_settings",
358                    &additional_settings,
359                )?;
360                state.end()
361            },
362
363            QFrame::PushPromise {
364                push_id,
365                header_block,
366            } => {
367                let mut state = s.serialize_struct(name, 2)?;
368                state.serialize_field("push_id", &push_id)?;
369                state.serialize_field("header_block_len", &header_block.len())?;
370                state.end()
371            },
372
373            QFrame::GoAway { id } => {
374                let mut state = s.serialize_struct(name, 1)?;
375                state.serialize_field("id", &id)?;
376                state.end()
377            },
378
379            QFrame::MaxPushId { push_id } => {
380                let mut state = s.serialize_struct(name, 1)?;
381                state.serialize_field("push_id", &push_id)?;
382                state.end()
383            },
384
385            QFrame::PriorityUpdateRequest {
386                prioritized_element_id,
387                priority_field_value,
388            } => {
389                let mut state = s.serialize_struct(name, 3)?;
390                state.serialize_field(
391                    "prioritized_element_id",
392                    &prioritized_element_id,
393                )?;
394
395                let max = cmp::min(
396                    priority_field_value.len(),
397                    MAX_SERIALIZED_BUFFER_LEN,
398                );
399                state.serialize_field(
400                    "priority_field_value_len",
401                    &priority_field_value.len(),
402                )?;
403                state.serialize_field(
404                    "priority_field_value",
405                    &String::from_utf8_lossy(&priority_field_value[..max]),
406                )?;
407                state.end()
408            },
409
410            QFrame::PriorityUpdatePush {
411                prioritized_element_id,
412                priority_field_value,
413            } => {
414                let mut state = s.serialize_struct(name, 3)?;
415                state.serialize_field(
416                    "prioritized_element_id",
417                    &prioritized_element_id,
418                )?;
419                let max = cmp::min(
420                    priority_field_value.len(),
421                    MAX_SERIALIZED_BUFFER_LEN,
422                );
423                state.serialize_field(
424                    "priority_field_value_len",
425                    &priority_field_value.len(),
426                )?;
427                state.serialize_field(
428                    "priority_field_value",
429                    &String::from_utf8_lossy(&priority_field_value[..max]),
430                )?;
431                state.end()
432            },
433
434            QFrame::Unknown { raw_type, payload } => {
435                let mut state = s.serialize_struct(name, 3)?;
436                state.serialize_field("raw_type", &raw_type)?;
437                let max = cmp::min(payload.len(), MAX_SERIALIZED_BUFFER_LEN);
438                state.serialize_field("payload_len", &payload.len())?;
439                state.serialize_field(
440                    "payload",
441                    &qlog::HexSlice::maybe_string(Some(&payload[..max])),
442                )?;
443                state.end()
444            },
445        }
446    }
447}
448
449type CustomEquivalenceHandler =
450    Box<dyn for<'f> Fn(&'f H3iFrame) -> bool + Send + Sync + 'static>;
451
452#[derive(Clone)]
453enum Comparator {
454    Frame(H3iFrame),
455    /// Specifies how to compare an incoming [`H3iFrame`] with this
456    /// [`CloseTriggerFrame`]. Typically, the validation attempts to fuzzy-match
457    /// the [`CloseTriggerFrame`] against the incoming [`H3iFrame`], but there
458    /// are times where other behavior is desired (for example, checking
459    /// deserialized JSON payloads in a headers frame, or ensuring a random
460    /// value matches a regex).
461    ///
462    /// See [`CloseTriggerFrame::is_equivalent`] for more on how frames are
463    /// compared.
464    Fn(Arc<CustomEquivalenceHandler>),
465}
466
467impl Serialize for Comparator {
468    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
469    where
470        S: Serializer,
471    {
472        match self {
473            Self::Fn(_) => serializer.serialize_str("<comparator_fn>"),
474            Self::Frame(f) => {
475                let mut frame_ser = serializer.serialize_struct("frame", 1)?;
476                frame_ser.serialize_field("frame", f)?;
477                frame_ser.end()
478            },
479        }
480    }
481}
482
483/// Instructs h3i to watch for certain incoming [`H3iFrame`]s. The incoming
484/// frames can either be supplied directly via [`CloseTriggerFrame::new`], or
485/// via a verification callback  passed to
486/// [`CloseTriggerFrame::new_with_comparator`].
487#[derive(Serialize, Clone)]
488pub struct CloseTriggerFrame {
489    stream_id: u64,
490    comparator: Comparator,
491}
492
493impl CloseTriggerFrame {
494    /// Create a new [`CloseTriggerFrame`] which should watch for the provided
495    /// [`H3iFrame`].
496    ///
497    /// # Note
498    ///
499    /// For [QuicheH3] and [ResetStream] variants, equivalence is the same as
500    /// equality.
501    ///
502    /// For Headers variants, this [`CloseTriggerFrame`] is equivalent to the
503    /// incoming [`H3iFrame`] if the [`H3iFrame`] contains all [`Header`]s
504    /// in _this_ frame. In other words, `this` can be considered equivalent
505    /// to `other` if `other` contains a superset of `this`'s [`Header`]s.
506    ///
507    /// This allows users for fuzzy-matching on header frames without needing to
508    /// supply every individual header on the frame.
509    ///
510    /// [ResetStream]: H3iFrame::ResetStream
511    /// [QuicheH3]: H3iFrame::QuicheH3
512    pub fn new(stream_id: u64, frame: impl Into<H3iFrame>) -> Self {
513        Self {
514            stream_id,
515            comparator: Comparator::Frame(frame.into()),
516        }
517    }
518
519    /// Create a new [`CloseTriggerFrame`] which will match incoming
520    /// [`H3iFrame`]s according to the passed `comparator_fn`.
521    ///
522    /// The `comparator_fn` will be called with every incoming [`H3iFrame`]. It
523    /// should return `true` if the incoming frame is expected, and `false`
524    /// if it is not.
525    pub fn new_with_comparator<F>(stream_id: u64, comparator_fn: F) -> Self
526    where
527        F: Fn(&H3iFrame) -> bool + Send + Sync + 'static,
528    {
529        Self {
530            stream_id,
531            comparator: Comparator::Fn(Arc::new(Box::new(comparator_fn))),
532        }
533    }
534
535    pub(crate) fn stream_id(&self) -> u64 {
536        self.stream_id
537    }
538
539    pub(crate) fn is_equivalent(&self, other: &H3iFrame) -> bool {
540        let frame = match &self.comparator {
541            Comparator::Fn(compare) => return compare(other),
542            Comparator::Frame(frame) => frame,
543        };
544
545        match frame {
546            H3iFrame::Headers(me) => {
547                let H3iFrame::Headers(other) = other else {
548                    return false;
549                };
550
551                // TODO(evanrittenhouse): we could theoretically hand-roll a
552                // MultiMap which uses a HashSet as the
553                // multi-value collection, but in practice we don't expect very
554                // many headers on an CloseTriggerFrame
555                //
556                // ref: https://docs.rs/multimap/latest/src/multimap/lib.rs.html#89
557                me.headers().iter().all(|m| other.headers().contains(m))
558            },
559            H3iFrame::QuicheH3(me) => match other {
560                H3iFrame::QuicheH3(other) => me == other,
561                _ => false,
562            },
563            H3iFrame::ResetStream(me) => match other {
564                H3iFrame::ResetStream(rs) => me == rs,
565                _ => false,
566            },
567        }
568    }
569}
570
571impl Debug for CloseTriggerFrame {
572    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
573        let repr = match &self.comparator {
574            Comparator::Frame(frame) => format!("{frame:?}"),
575            Comparator::Fn(_) => "closure".to_string(),
576        };
577
578        write!(
579            f,
580            "CloseTriggerFrame {{ stream_id: {}, comparator: {repr} }}",
581            self.stream_id
582        )
583    }
584}
585
586impl PartialEq for CloseTriggerFrame {
587    fn eq(&self, other: &Self) -> bool {
588        match (&self.comparator, &other.comparator) {
589            (Comparator::Frame(this_frame), Comparator::Frame(other_frame)) =>
590                self.stream_id == other.stream_id && this_frame == other_frame,
591            _ => false,
592        }
593    }
594}
595
596#[cfg(test)]
597mod tests {
598    use super::*;
599    use quiche::h3::frame::Frame;
600
601    #[test]
602    fn enriched_headers_decode_error() {
603        // Field section that references the QPACK dynamic table, which is not
604        // supported.
605        let frame = QFrame::Headers {
606            header_block: vec![0, 0, 0x80],
607        };
608
609        assert!(EnrichedHeaders::try_from(frame).is_err());
610    }
611
612    #[test]
613    fn test_header_equivalence() {
614        let this = CloseTriggerFrame::new(0, vec![
615            Header::new(b"hello", b"world"),
616            Header::new(b"go", b"jets"),
617        ]);
618        let other: H3iFrame = vec![
619            Header::new(b"hello", b"world"),
620            Header::new(b"go", b"jets"),
621            Header::new(b"go", b"devils"),
622        ]
623        .into();
624
625        assert!(this.is_equivalent(&other));
626    }
627
628    #[test]
629    fn test_header_non_equivalence() {
630        let this = CloseTriggerFrame::new(0, vec![
631            Header::new(b"hello", b"world"),
632            Header::new(b"go", b"jets"),
633            Header::new(b"go", b"devils"),
634        ]);
635        let other: H3iFrame =
636            vec![Header::new(b"hello", b"world"), Header::new(b"go", b"jets")]
637                .into();
638
639        // `other` does not contain the `go: devils` header, so it's not
640        // equivalent to `this.
641        assert!(!this.is_equivalent(&other));
642    }
643
644    #[test]
645    fn test_rst_stream_equivalence() {
646        let mut rs = ResetStream {
647            stream_id: 0,
648            error_code: 57,
649        };
650
651        let this = CloseTriggerFrame::new(0, H3iFrame::ResetStream(rs.clone()));
652        let incoming = H3iFrame::ResetStream(rs.clone());
653        assert!(this.is_equivalent(&incoming));
654
655        rs.stream_id = 57;
656        let incoming = H3iFrame::ResetStream(rs);
657        assert!(!this.is_equivalent(&incoming));
658    }
659
660    #[test]
661    fn test_frame_equivalence() {
662        let mut d = Frame::Data {
663            payload: b"57".to_vec(),
664        };
665
666        let this = CloseTriggerFrame::new(0, H3iFrame::QuicheH3(d.clone()));
667        let incoming = H3iFrame::QuicheH3(d.clone());
668        assert!(this.is_equivalent(&incoming));
669
670        d = Frame::Data {
671            payload: b"go jets".to_vec(),
672        };
673        let incoming = H3iFrame::QuicheH3(d.clone());
674        assert!(!this.is_equivalent(&incoming));
675    }
676
677    #[test]
678    fn test_comparator() {
679        let this = CloseTriggerFrame::new_with_comparator(0, |frame| {
680            if let H3iFrame::Headers(..) = frame {
681                frame
682                    .to_enriched_headers()
683                    .unwrap()
684                    .header_map()
685                    .get(&b"cookie".to_vec())
686                    .is_some_and(|v| {
687                        std::str::from_utf8(v)
688                            .map(|s| s.to_lowercase())
689                            .unwrap()
690                            .contains("cookie")
691                    })
692            } else {
693                false
694            }
695        });
696
697        let incoming: H3iFrame =
698            vec![Header::new(b"cookie", b"SomeRandomCookie1234")].into();
699
700        assert!(this.is_equivalent(&incoming));
701    }
702}