h3i/recordreplay/
qlog.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
27use std::collections::BTreeMap;
28
29use qlog::events::h3::H3FrameCreated;
30use qlog::events::h3::H3Owner;
31use qlog::events::h3::H3StreamTypeSet;
32use qlog::events::h3::Http3Frame;
33use qlog::events::h3::HttpHeader;
34use qlog::events::quic::ErrorSpace;
35use qlog::events::quic::PacketSent;
36use qlog::events::quic::QuicFrame;
37use qlog::events::Event;
38use qlog::events::EventData;
39use qlog::events::ExData;
40use qlog::events::JsonEvent;
41use qlog::events::RawInfo;
42use quiche;
43use quiche::h3::frame::Frame;
44use quiche::h3::NameValue;
45
46use serde_json::json;
47
48use smallvec::smallvec;
49
50use crate::actions::h3::Action;
51use crate::actions::h3::WaitType;
52use crate::encode_header_block;
53use crate::encode_header_block_literal;
54use crate::fake_packet_sent;
55use crate::HTTP3_CONTROL_STREAM_TYPE_ID;
56use crate::HTTP3_PUSH_STREAM_TYPE_ID;
57use crate::QPACK_DECODER_STREAM_TYPE_ID;
58use crate::QPACK_ENCODER_STREAM_TYPE_ID;
59
60/// A qlog event representation using either the official RFC format or the
61/// catch-al JSON event.
62pub enum QlogEvent {
63    Event {
64        data: Box<EventData>,
65        ex_data: ExData,
66    },
67    JsonEvent(JsonEvent),
68}
69
70/// A collection of [QlogEvent]s.
71pub type QlogEvents = Vec<QlogEvent>;
72
73/// A collection of [Action]s.
74pub struct H3Actions(pub Vec<Action>);
75
76/// A qlog [H3FrameCreated] event, with [ExData].
77pub struct H3FrameCreatedEx {
78    frame_created: H3FrameCreated,
79    ex_data: ExData,
80}
81
82impl From<&Action> for QlogEvents {
83    fn from(action: &Action) -> Self {
84        match action {
85            Action::SendFrame {
86                stream_id,
87                fin_stream,
88                frame,
89            } => {
90                let frame_ev = EventData::H3FrameCreated(H3FrameCreated {
91                    stream_id: *stream_id,
92                    frame: frame.to_qlog(),
93                    ..Default::default()
94                });
95
96                let mut ex = BTreeMap::new();
97
98                if *fin_stream {
99                    ex.insert("fin_stream".to_string(), json!(true));
100                }
101
102                vec![QlogEvent::Event {
103                    data: Box::new(frame_ev),
104                    ex_data: ex,
105                }]
106            },
107
108            Action::SendHeadersFrame {
109                stream_id,
110                fin_stream,
111                headers,
112                literal_headers,
113                ..
114            } => {
115                let qlog_headers = headers
116                    .iter()
117                    .map(|h| qlog::events::h3::HttpHeader {
118                        name: String::from_utf8_lossy(h.name()).into_owned(),
119                        value: String::from_utf8_lossy(h.value()).into_owned(),
120                    })
121                    .collect();
122
123                let frame = Http3Frame::Headers {
124                    headers: qlog_headers,
125                };
126
127                let frame_ev = EventData::H3FrameCreated(H3FrameCreated {
128                    stream_id: *stream_id,
129                    frame,
130                    ..Default::default()
131                });
132
133                let mut ex = BTreeMap::new();
134
135                if *fin_stream {
136                    ex.insert("fin_stream".to_string(), json!(true));
137                }
138
139                if *literal_headers {
140                    ex.insert("literal_headers".to_string(), json!(true));
141                }
142
143                vec![QlogEvent::Event {
144                    data: Box::new(frame_ev),
145                    ex_data: ex,
146                }]
147            },
148
149            Action::OpenUniStream {
150                stream_id,
151                fin_stream,
152                stream_type,
153            } => {
154                let ty = match *stream_type {
155                    HTTP3_CONTROL_STREAM_TYPE_ID =>
156                        qlog::events::h3::H3StreamType::Control,
157                    HTTP3_PUSH_STREAM_TYPE_ID =>
158                        qlog::events::h3::H3StreamType::Push,
159                    QPACK_ENCODER_STREAM_TYPE_ID =>
160                        qlog::events::h3::H3StreamType::QpackEncode,
161                    QPACK_DECODER_STREAM_TYPE_ID =>
162                        qlog::events::h3::H3StreamType::QpackDecode,
163
164                    _ => qlog::events::h3::H3StreamType::Unknown,
165                };
166                let ty_val =
167                    if matches!(ty, qlog::events::h3::H3StreamType::Unknown) {
168                        Some(*stream_type)
169                    } else {
170                        None
171                    };
172
173                let stream_ev = EventData::H3StreamTypeSet(H3StreamTypeSet {
174                    owner: Some(H3Owner::Local),
175                    stream_id: *stream_id,
176                    stream_type: ty,
177                    stream_type_value: ty_val,
178                    ..Default::default()
179                });
180                let mut ex = BTreeMap::new();
181
182                if *fin_stream {
183                    ex.insert("fin_stream".to_string(), json!(true));
184                }
185
186                vec![QlogEvent::Event {
187                    data: Box::new(stream_ev),
188                    ex_data: ex,
189                }]
190            },
191
192            Action::StreamBytes {
193                stream_id,
194                fin_stream,
195                bytes,
196            } => {
197                let len = bytes.len() as u64;
198                let ev = fake_packet_sent(Some(smallvec![QuicFrame::Stream {
199                    stream_id: *stream_id,
200                    fin: Some(*fin_stream),
201                    // ignore offset
202                    offset: 0,
203                    length: len,
204                    raw: Some(RawInfo {
205                        length: Some(len),
206                        payload_length: Some(len),
207                        data: String::from_utf8(bytes.clone()).ok()
208                    })
209                }]));
210
211                vec![QlogEvent::Event {
212                    data: Box::new(ev),
213                    ex_data: BTreeMap::new(),
214                }]
215            },
216
217            Action::ResetStream {
218                stream_id,
219                error_code,
220            } => {
221                let ev =
222                    fake_packet_sent(Some(smallvec![QuicFrame::ResetStream {
223                        stream_id: *stream_id,
224                        error_code: *error_code,
225                        final_size: 0,
226                        length: None,
227                        payload_length: None
228                    }]));
229                vec![QlogEvent::Event {
230                    data: Box::new(ev),
231                    ex_data: BTreeMap::new(),
232                }]
233            },
234
235            Action::StopSending {
236                stream_id,
237                error_code,
238            } => {
239                let ev =
240                    fake_packet_sent(Some(smallvec![QuicFrame::StopSending {
241                        stream_id: *stream_id,
242                        error_code: *error_code,
243                        length: None,
244                        payload_length: None
245                    }]));
246                vec![QlogEvent::Event {
247                    data: Box::new(ev),
248                    ex_data: BTreeMap::new(),
249                }]
250            },
251
252            Action::Wait { wait_type } => {
253                let name = "h3i:wait".into();
254
255                let data = match wait_type {
256                    d @ WaitType::WaitDuration(_) =>
257                        serde_json::to_value(d).unwrap(),
258                    WaitType::StreamEvent(event) =>
259                        serde_json::to_value(event).unwrap(),
260                };
261
262                vec![QlogEvent::JsonEvent(qlog::events::JsonEvent {
263                    time: 0.0,
264                    importance: qlog::events::EventImportance::Core,
265                    name,
266                    data,
267                })]
268            },
269
270            Action::ConnectionClose { error } => {
271                let error_space = if error.is_app {
272                    ErrorSpace::ApplicationError
273                } else {
274                    ErrorSpace::TransportError
275                };
276
277                let reason = if error.reason.is_empty() {
278                    None
279                } else {
280                    Some(String::from_utf8(error.reason.clone()).unwrap())
281                };
282
283                let ev = fake_packet_sent(Some(smallvec![
284                    QuicFrame::ConnectionClose {
285                        error_space: Some(error_space),
286                        error_code: Some(error.error_code),
287                        // https://github.com/cloudflare/quiche/issues/1731
288                        error_code_value: None,
289                        reason,
290                        trigger_frame_type: None
291                    }
292                ]));
293
294                vec![QlogEvent::Event {
295                    data: Box::new(ev),
296                    ex_data: BTreeMap::new(),
297                }]
298            },
299
300            Action::FlushPackets => {
301                vec![]
302            },
303        }
304    }
305}
306
307pub fn actions_from_qlog(event: Event, host_override: Option<&str>) -> H3Actions {
308    let mut actions = vec![];
309    match &event.data {
310        EventData::PacketSent(ps) => {
311            let packet_actions: H3Actions = ps.into();
312            actions.extend(packet_actions.0);
313        },
314
315        EventData::H3FrameCreated(fc) => {
316            let mut frame_created = H3FrameCreatedEx {
317                frame_created: fc.clone(),
318                ex_data: event.ex_data.clone(),
319            };
320
321            // Insert custom data so that conversion of frames to Actions can
322            // use it.
323            if let Some(host) = host_override {
324                frame_created
325                    .ex_data
326                    .insert("host_override".into(), host.into());
327            }
328
329            actions.push(frame_created.into());
330        },
331
332        EventData::H3StreamTypeSet(st) => {
333            let stream_actions = from_qlog_stream_type_set(st, &event.ex_data);
334            actions.extend(stream_actions);
335        },
336
337        _ => (),
338    }
339
340    H3Actions(actions)
341}
342
343impl From<JsonEvent> for H3Actions {
344    fn from(event: JsonEvent) -> Self {
345        let mut actions = vec![];
346        match event.name.as_ref() {
347            "h3i:wait" => {
348                let wait_type =
349                    serde_json::from_value::<WaitType>(event.clone().data);
350
351                if let Ok(wt) = wait_type {
352                    actions.push(Action::Wait { wait_type: wt });
353                } else {
354                    log::debug!("couldn't create action from event: {:?}", event);
355                }
356            },
357            _ => unimplemented!(),
358        }
359
360        Self(actions)
361    }
362}
363
364impl From<&PacketSent> for H3Actions {
365    fn from(ps: &PacketSent) -> Self {
366        let mut actions = vec![];
367        if let Some(frames) = &ps.frames {
368            for frame in frames {
369                match &frame {
370                    // TODO add these
371                    QuicFrame::ResetStream {
372                        stream_id,
373                        error_code,
374                        ..
375                    } => actions.push(Action::ResetStream {
376                        stream_id: *stream_id,
377                        error_code: *error_code,
378                    }),
379
380                    QuicFrame::StopSending {
381                        stream_id,
382                        error_code,
383                        ..
384                    } => actions.push(Action::StopSending {
385                        stream_id: *stream_id,
386                        error_code: *error_code,
387                    }),
388
389                    QuicFrame::ConnectionClose {
390                        error_space,
391                        error_code,
392                        reason,
393                        ..
394                    } => {
395                        let is_app = matches!(
396                            error_space.as_ref().expect(
397                                "invalid CC frame in qlog input, no error space"
398                            ),
399                            ErrorSpace::ApplicationError
400                        );
401
402                        actions.push(Action::ConnectionClose {
403                            error: quiche::ConnectionError {
404                                is_app,
405                                // TODO: remove unwrap when https://github.com/cloudflare/quiche/issues/1731
406                                // is done
407                                error_code: error_code.expect("invalid CC frame in qlog input, no error code"),
408                                reason: reason
409                                    .as_ref()
410                                    .map(|s| s.as_bytes().to_vec())
411                                    .unwrap_or_default(),
412                            },
413                        })
414                    },
415
416                    QuicFrame::Stream { stream_id, fin, .. } => {
417                        let fin = fin.unwrap_or_default();
418
419                        if fin {
420                            actions.push(Action::StreamBytes {
421                                stream_id: *stream_id,
422                                fin_stream: true,
423                                bytes: vec![],
424                            });
425                        }
426                    },
427
428                    _ => (),
429                }
430            }
431        }
432
433        Self(actions)
434    }
435}
436
437fn map_header(
438    hdr: &HttpHeader, host_override: Option<&str>,
439) -> quiche::h3::Header {
440    if hdr.name.eq_ignore_ascii_case(":authority") ||
441        hdr.name.eq_ignore_ascii_case("host")
442    {
443        if let Some(host) = host_override {
444            return quiche::h3::Header::new(hdr.name.as_bytes(), host.as_bytes());
445        }
446    }
447
448    quiche::h3::Header::new(hdr.name.as_bytes(), hdr.value.as_bytes())
449}
450
451impl From<H3FrameCreatedEx> for Action {
452    fn from(value: H3FrameCreatedEx) -> Self {
453        let stream_id = value.frame_created.stream_id;
454        let fin_stream = value
455            .ex_data
456            .get("fin_stream")
457            .unwrap_or(&serde_json::Value::Null)
458            .as_bool()
459            .unwrap_or_default();
460        let host_override = value
461            .ex_data
462            .get("host_override")
463            .unwrap_or(&serde_json::Value::Null)
464            .as_str();
465
466        let ret = match &value.frame_created.frame {
467            Http3Frame::Settings { settings } => {
468                let mut raw_settings = vec![];
469                let mut additional_settings = vec![];
470                // This is ugly but it reflects ambiguity in the qlog
471                // specs.
472                for s in settings {
473                    match s.name.as_str() {
474                        "MAX_FIELD_SECTION_SIZE" =>
475                            raw_settings.push((0x6, s.value)),
476                        "QPACK_MAX_TABLE_CAPACITY" =>
477                            raw_settings.push((0x1, s.value)),
478                        "QPACK_BLOCKED_STREAMS" =>
479                            raw_settings.push((0x7, s.value)),
480                        "SETTINGS_ENABLE_CONNECT_PROTOCOL" =>
481                            raw_settings.push((0x8, s.value)),
482                        "H3_DATAGRAM" => raw_settings.push((0x33, s.value)),
483
484                        _ =>
485                            if let Ok(ty) = s.name.parse::<u64>() {
486                                raw_settings.push((ty, s.value));
487                                additional_settings.push((ty, s.value));
488                            },
489                    }
490                }
491
492                Action::SendFrame {
493                    stream_id,
494                    fin_stream,
495                    frame: Frame::Settings {
496                        max_field_section_size: None,
497                        qpack_max_table_capacity: None,
498                        qpack_blocked_streams: None,
499                        connect_protocol_enabled: None,
500                        h3_datagram: None,
501                        grease: None,
502                        raw: Some(raw_settings),
503                        additional_settings: Some(additional_settings),
504                    },
505                }
506            },
507
508            Http3Frame::Headers { headers } => {
509                let hdrs: Vec<quiche::h3::Header> = headers
510                    .iter()
511                    .map(|h| map_header(h, host_override))
512                    .collect();
513
514                let literal_headers = value
515                    .ex_data
516                    .get("literal_headers")
517                    .unwrap_or(&serde_json::Value::Null)
518                    .as_bool()
519                    .unwrap_or_default();
520
521                let header_block = if literal_headers {
522                    encode_header_block_literal(&hdrs).unwrap()
523                } else {
524                    encode_header_block(&hdrs).unwrap()
525                };
526
527                Action::SendHeadersFrame {
528                    stream_id,
529                    fin_stream,
530                    literal_headers,
531                    headers: hdrs,
532                    frame: Frame::Headers { header_block },
533                }
534            },
535
536            Http3Frame::Data { raw } => {
537                let mut payload = vec![];
538                if let Some(r) = raw {
539                    payload = r
540                        .data
541                        .clone()
542                        .unwrap_or("".to_string())
543                        .as_bytes()
544                        .to_vec();
545                }
546
547                Action::SendFrame {
548                    stream_id,
549                    fin_stream,
550                    frame: Frame::Data { payload },
551                }
552            },
553
554            Http3Frame::Goaway { id } => Action::SendFrame {
555                stream_id,
556                fin_stream,
557                frame: Frame::GoAway { id: *id },
558            },
559
560            _ => unimplemented!(),
561        };
562
563        ret
564    }
565}
566
567fn from_qlog_stream_type_set(
568    st: &H3StreamTypeSet, ex_data: &ExData,
569) -> Vec<Action> {
570    let mut actions = vec![];
571    let fin_stream = parse_ex_data(ex_data);
572    let stream_type = match st.stream_type {
573        qlog::events::h3::H3StreamType::Control => Some(0x0),
574        qlog::events::h3::H3StreamType::Push => Some(0x1),
575        qlog::events::h3::H3StreamType::QpackEncode => Some(0x2),
576        qlog::events::h3::H3StreamType::QpackDecode => Some(0x3),
577        qlog::events::h3::H3StreamType::Reserved |
578        qlog::events::h3::H3StreamType::Unknown => st.stream_type_value,
579        _ => None,
580    };
581
582    if let Some(ty) = stream_type {
583        actions.push(Action::OpenUniStream {
584            stream_id: st.stream_id,
585            fin_stream,
586            stream_type: ty,
587        })
588    }
589
590    actions
591}
592
593fn parse_ex_data(ex_data: &ExData) -> bool {
594    ex_data
595        .get("fin_stream")
596        .unwrap_or(&serde_json::Value::Null)
597        .as_bool()
598        .unwrap_or_default()
599}
600
601#[cfg(test)]
602mod tests {
603    use crate::actions::h3::StreamEvent;
604    use crate::actions::h3::StreamEventType;
605    use crate::encode_header_block_literal;
606    use std::time::Duration;
607
608    use super::*;
609    use quiche::h3::Header;
610    use serde_json;
611
612    const NOW: f32 = 123.0;
613    const H3I_WAIT: &str = "h3i:wait";
614
615    #[test]
616    fn ser_duration_wait() {
617        let ev = JsonEvent {
618            time: NOW,
619            importance: qlog::events::EventImportance::Core,
620            name: H3I_WAIT.to_string(),
621            data: serde_json::to_value(WaitType::WaitDuration(
622                Duration::from_millis(12345),
623            ))
624            .unwrap(),
625        };
626        let serialized = serde_json::to_string(&ev);
627
628        let expected =
629            r#"{"time":123.0,"name":"h3i:wait","data":{"duration":12345.0}}"#;
630        assert_eq!(&serialized.unwrap(), expected);
631    }
632
633    #[test]
634    fn deser_duration_wait() {
635        let ev = JsonEvent {
636            time: NOW,
637            importance: qlog::events::EventImportance::Core,
638            name: H3I_WAIT.to_string(),
639            data: serde_json::to_value(WaitType::WaitDuration(
640                Duration::from_millis(12345),
641            ))
642            .unwrap(),
643        };
644
645        let expected =
646            r#"{"time":123.0,"name":"h3i:wait","data":{"duration":12345.0}}"#;
647        let deser = serde_json::from_str::<JsonEvent>(expected).unwrap();
648        assert_eq!(deser.data, ev.data);
649    }
650
651    #[test]
652    fn ser_stream_wait() {
653        let expected = r#"{"time":123.0,"name":"h3i:wait","data":{"stream_id":0,"type":"data"}}"#;
654        let ev = JsonEvent {
655            time: NOW,
656            importance: qlog::events::EventImportance::Core,
657            name: H3I_WAIT.to_string(),
658            data: serde_json::to_value(StreamEvent {
659                stream_id: 0,
660                event_type: StreamEventType::Data,
661            })
662            .unwrap(),
663        };
664
665        let serialized = serde_json::to_string(&ev);
666        assert_eq!(&serialized.unwrap(), expected);
667    }
668
669    #[test]
670    fn deser_stream_wait() {
671        let ev = JsonEvent {
672            time: NOW,
673            importance: qlog::events::EventImportance::Core,
674            name: H3I_WAIT.to_string(),
675            data: serde_json::to_value(StreamEvent {
676                stream_id: 0,
677                event_type: StreamEventType::Data,
678            })
679            .unwrap(),
680        };
681
682        let expected = r#"{"time":123.0,"name":"h3i:wait","data":{"stream_id":0,"type":"data"}}"#;
683        let deser = serde_json::from_str::<JsonEvent>(expected).unwrap();
684        assert_eq!(deser.data, ev.data);
685    }
686
687    #[test]
688    fn deser_http_headers_to_action() {
689        let serialized = r#"{"time":0.074725,"name":"http:frame_created","data":{"stream_id":0,"frame":{"frame_type":"headers","headers":[{"name":":method","value":"GET"},{"name":":authority","value":"example.net"},{"name":":path","value":"/"},{"name":":scheme","value":"https"}]}},"fin_stream":true}"#;
690        let deserialized = serde_json::from_str::<Event>(serialized).unwrap();
691        let actions = actions_from_qlog(deserialized, None);
692        assert!(actions.0.len() == 1);
693
694        let headers = vec![
695            Header::new(b":method", b"GET"),
696            Header::new(b":authority", b"example.net"),
697            Header::new(b":path", b"/"),
698            Header::new(b":scheme", b"https"),
699        ];
700        let header_block = encode_header_block(&headers).unwrap();
701        let frame = Frame::Headers { header_block };
702        let expected = Action::SendHeadersFrame {
703            stream_id: 0,
704            fin_stream: true,
705            literal_headers: false,
706            headers,
707            frame,
708        };
709
710        assert_eq!(actions.0[0], expected);
711    }
712
713    #[test]
714    fn deser_http_headers_host_overrid_to_action() {
715        let serialized = r#"{"time":0.074725,"name":"http:frame_created","data":{"stream_id":0,"frame":{"frame_type":"headers","headers":[{"name":":method","value":"GET"},{"name":":authority","value":"bla.com"},{"name":":path","value":"/"},{"name":":scheme","value":"https"}]}},"fin_stream":true}"#;
716        let deserialized = serde_json::from_str::<Event>(serialized).unwrap();
717        let actions = actions_from_qlog(deserialized, Some("example.org"));
718        assert!(actions.0.len() == 1);
719
720        let headers = vec![
721            Header::new(b":method", b"GET"),
722            Header::new(b":authority", b"example.org"),
723            Header::new(b":path", b"/"),
724            Header::new(b":scheme", b"https"),
725        ];
726        let header_block = encode_header_block(&headers).unwrap();
727        let frame = Frame::Headers { header_block };
728        let expected = Action::SendHeadersFrame {
729            stream_id: 0,
730            fin_stream: true,
731            literal_headers: false,
732            headers,
733            frame,
734        };
735
736        assert_eq!(actions.0[0], expected);
737    }
738
739    #[test]
740    fn deser_http_headers_literal_to_action() {
741        let serialized = r#"{"time":0.074725,"name":"http:frame_created","data":{"stream_id":0,"frame":{"frame_type":"headers","headers":[{"name":":method","value":"GET"},{"name":":authority","value":"bla.com"},{"name":":path","value":"/"},{"name":":scheme","value":"https"},{"name":"Foo","value":"bar"}]}},"fin_stream":true,"literal_headers":true}"#;
742        let deserialized = serde_json::from_str::<Event>(serialized).unwrap();
743        let actions = actions_from_qlog(deserialized, None);
744        assert!(actions.0.len() == 1);
745
746        let headers = vec![
747            Header::new(b":method", b"GET"),
748            Header::new(b":authority", b"bla.com"),
749            Header::new(b":path", b"/"),
750            Header::new(b":scheme", b"https"),
751            Header::new(b"Foo", b"bar"),
752        ];
753        let header_block = encode_header_block_literal(&headers).unwrap();
754        let frame = Frame::Headers { header_block };
755        let expected = Action::SendHeadersFrame {
756            stream_id: 0,
757            fin_stream: true,
758            literal_headers: true,
759            headers,
760            frame,
761        };
762
763        assert_eq!(actions.0[0], expected);
764    }
765}