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 crate::quiche;
30use qlog::events::h3::H3FrameCreated;
31use qlog::events::h3::H3Owner;
32use qlog::events::h3::H3StreamTypeSet;
33use qlog::events::h3::Http3Frame;
34use qlog::events::h3::HttpHeader;
35use qlog::events::quic::ErrorSpace;
36use qlog::events::quic::PacketSent;
37use qlog::events::quic::QuicFrame;
38use qlog::events::Event;
39use qlog::events::EventData;
40use qlog::events::ExData;
41use qlog::events::JsonEvent;
42use qlog::events::RawInfo;
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::SendDatagram { payload } => {
218                let len = payload.len() as u64;
219                let ev = fake_packet_sent(Some(smallvec![QuicFrame::Datagram {
220                    length: len,
221                    raw: String::from_utf8(payload.clone()).ok()
222                }]));
223
224                vec![QlogEvent::Event {
225                    data: Box::new(ev),
226                    ex_data: BTreeMap::new(),
227                }]
228            },
229
230            Action::ResetStream {
231                stream_id,
232                error_code,
233            } => {
234                let ev =
235                    fake_packet_sent(Some(smallvec![QuicFrame::ResetStream {
236                        stream_id: *stream_id,
237                        error_code: *error_code,
238                        final_size: 0,
239                        length: None,
240                        payload_length: None
241                    }]));
242                vec![QlogEvent::Event {
243                    data: Box::new(ev),
244                    ex_data: BTreeMap::new(),
245                }]
246            },
247
248            Action::StopSending {
249                stream_id,
250                error_code,
251            } => {
252                let ev =
253                    fake_packet_sent(Some(smallvec![QuicFrame::StopSending {
254                        stream_id: *stream_id,
255                        error_code: *error_code,
256                        length: None,
257                        payload_length: None
258                    }]));
259                vec![QlogEvent::Event {
260                    data: Box::new(ev),
261                    ex_data: BTreeMap::new(),
262                }]
263            },
264
265            Action::Wait { wait_type } => {
266                let name = "h3i:wait".into();
267
268                let data = match wait_type {
269                    d @ WaitType::WaitDuration(_) =>
270                        serde_json::to_value(d).unwrap(),
271                    WaitType::StreamEvent(event) =>
272                        serde_json::to_value(event).unwrap(),
273                };
274
275                vec![QlogEvent::JsonEvent(qlog::events::JsonEvent {
276                    time: 0.0,
277                    importance: qlog::events::EventImportance::Core,
278                    name,
279                    data,
280                })]
281            },
282
283            Action::ConnectionClose { error } => {
284                let error_space = if error.is_app {
285                    ErrorSpace::ApplicationError
286                } else {
287                    ErrorSpace::TransportError
288                };
289
290                let reason = if error.reason.is_empty() {
291                    None
292                } else {
293                    Some(String::from_utf8(error.reason.clone()).unwrap())
294                };
295
296                let ev = fake_packet_sent(Some(smallvec![
297                    QuicFrame::ConnectionClose {
298                        error_space: Some(error_space),
299                        error_code: Some(error.error_code),
300                        // https://github.com/cloudflare/quiche/issues/1731
301                        error_code_value: None,
302                        reason,
303                        trigger_frame_type: None
304                    }
305                ]));
306
307                vec![QlogEvent::Event {
308                    data: Box::new(ev),
309                    ex_data: BTreeMap::new(),
310                }]
311            },
312
313            Action::FlushPackets => {
314                vec![]
315            },
316        }
317    }
318}
319
320pub fn actions_from_qlog(event: Event, host_override: Option<&str>) -> H3Actions {
321    let mut actions = vec![];
322    match &event.data {
323        EventData::PacketSent(ps) => {
324            let packet_actions: H3Actions = ps.into();
325            actions.extend(packet_actions.0);
326        },
327
328        EventData::H3FrameCreated(fc) => {
329            let mut frame_created = H3FrameCreatedEx {
330                frame_created: fc.clone(),
331                ex_data: event.ex_data.clone(),
332            };
333
334            // Insert custom data so that conversion of frames to Actions can
335            // use it.
336            if let Some(host) = host_override {
337                frame_created
338                    .ex_data
339                    .insert("host_override".into(), host.into());
340            }
341
342            actions.push(frame_created.into());
343        },
344
345        EventData::H3StreamTypeSet(st) => {
346            let stream_actions = from_qlog_stream_type_set(st, &event.ex_data);
347            actions.extend(stream_actions);
348        },
349
350        _ => (),
351    }
352
353    H3Actions(actions)
354}
355
356impl From<JsonEvent> for H3Actions {
357    fn from(event: JsonEvent) -> Self {
358        let mut actions = vec![];
359        match event.name.as_ref() {
360            "h3i:wait" => {
361                let wait_type =
362                    serde_json::from_value::<WaitType>(event.clone().data);
363
364                if let Ok(wt) = wait_type {
365                    actions.push(Action::Wait { wait_type: wt });
366                } else {
367                    log::debug!("couldn't create action from event: {event:?}");
368                }
369            },
370            _ => unimplemented!(),
371        }
372
373        Self(actions)
374    }
375}
376
377impl From<&PacketSent> for H3Actions {
378    fn from(ps: &PacketSent) -> Self {
379        let mut actions = vec![];
380        if let Some(frames) = &ps.frames {
381            for frame in frames {
382                match &frame {
383                    // TODO add these
384                    QuicFrame::ResetStream {
385                        stream_id,
386                        error_code,
387                        ..
388                    } => actions.push(Action::ResetStream {
389                        stream_id: *stream_id,
390                        error_code: *error_code,
391                    }),
392
393                    QuicFrame::StopSending {
394                        stream_id,
395                        error_code,
396                        ..
397                    } => actions.push(Action::StopSending {
398                        stream_id: *stream_id,
399                        error_code: *error_code,
400                    }),
401
402                    QuicFrame::ConnectionClose {
403                        error_space,
404                        error_code,
405                        reason,
406                        ..
407                    } => {
408                        let is_app = matches!(
409                            error_space.as_ref().expect(
410                                "invalid CC frame in qlog input, no error space"
411                            ),
412                            ErrorSpace::ApplicationError
413                        );
414
415                        actions.push(Action::ConnectionClose {
416                            error: quiche::ConnectionError {
417                                is_app,
418                                // TODO: remove unwrap when https://github.com/cloudflare/quiche/issues/1731
419                                // is done
420                                error_code: error_code.expect("invalid CC frame in qlog input, no error code"),
421                                reason: reason
422                                    .as_ref()
423                                    .map(|s| s.as_bytes().to_vec())
424                                    .unwrap_or_default(),
425                            },
426                        })
427                    },
428
429                    QuicFrame::Stream { stream_id, fin, .. } => {
430                        let fin = fin.unwrap_or_default();
431
432                        if fin {
433                            actions.push(Action::StreamBytes {
434                                stream_id: *stream_id,
435                                fin_stream: true,
436                                bytes: vec![],
437                            });
438                        }
439                    },
440
441                    QuicFrame::Datagram { raw, .. } => {
442                        actions.push(Action::SendDatagram {
443                            payload: raw.clone().unwrap_or_default().into(),
444                        });
445                    },
446                    _ => (),
447                }
448            }
449        }
450
451        Self(actions)
452    }
453}
454
455fn map_header(
456    hdr: &HttpHeader, host_override: Option<&str>,
457) -> quiche::h3::Header {
458    if hdr.name.eq_ignore_ascii_case(":authority") ||
459        hdr.name.eq_ignore_ascii_case("host")
460    {
461        if let Some(host) = host_override {
462            return quiche::h3::Header::new(hdr.name.as_bytes(), host.as_bytes());
463        }
464    }
465
466    quiche::h3::Header::new(hdr.name.as_bytes(), hdr.value.as_bytes())
467}
468
469impl From<H3FrameCreatedEx> for Action {
470    fn from(value: H3FrameCreatedEx) -> Self {
471        let stream_id = value.frame_created.stream_id;
472        let fin_stream = value
473            .ex_data
474            .get("fin_stream")
475            .unwrap_or(&serde_json::Value::Null)
476            .as_bool()
477            .unwrap_or_default();
478        let host_override = value
479            .ex_data
480            .get("host_override")
481            .unwrap_or(&serde_json::Value::Null)
482            .as_str();
483
484        let ret = match &value.frame_created.frame {
485            Http3Frame::Settings { settings } => {
486                let mut raw_settings = vec![];
487                let mut additional_settings = vec![];
488                // This is ugly but it reflects ambiguity in the qlog
489                // specs.
490                for s in settings {
491                    match s.name.as_str() {
492                        "MAX_FIELD_SECTION_SIZE" =>
493                            raw_settings.push((0x6, s.value)),
494                        "QPACK_MAX_TABLE_CAPACITY" =>
495                            raw_settings.push((0x1, s.value)),
496                        "QPACK_BLOCKED_STREAMS" =>
497                            raw_settings.push((0x7, s.value)),
498                        "SETTINGS_ENABLE_CONNECT_PROTOCOL" =>
499                            raw_settings.push((0x8, s.value)),
500                        "H3_DATAGRAM" => raw_settings.push((0x33, s.value)),
501
502                        _ =>
503                            if let Ok(ty) = s.name.parse::<u64>() {
504                                raw_settings.push((ty, s.value));
505                                additional_settings.push((ty, s.value));
506                            },
507                    }
508                }
509
510                Action::SendFrame {
511                    stream_id,
512                    fin_stream,
513                    frame: Frame::Settings {
514                        max_field_section_size: None,
515                        qpack_max_table_capacity: None,
516                        qpack_blocked_streams: None,
517                        connect_protocol_enabled: None,
518                        h3_datagram: None,
519                        grease: None,
520                        raw: Some(raw_settings),
521                        additional_settings: Some(additional_settings),
522                    },
523                }
524            },
525
526            Http3Frame::Headers { headers } => {
527                let hdrs: Vec<quiche::h3::Header> = headers
528                    .iter()
529                    .map(|h| map_header(h, host_override))
530                    .collect();
531
532                let literal_headers = value
533                    .ex_data
534                    .get("literal_headers")
535                    .unwrap_or(&serde_json::Value::Null)
536                    .as_bool()
537                    .unwrap_or_default();
538
539                let header_block = if literal_headers {
540                    encode_header_block_literal(&hdrs).unwrap()
541                } else {
542                    encode_header_block(&hdrs).unwrap()
543                };
544
545                Action::SendHeadersFrame {
546                    stream_id,
547                    fin_stream,
548                    literal_headers,
549                    headers: hdrs,
550                    frame: Frame::Headers { header_block },
551                }
552            },
553
554            Http3Frame::Data { raw } => {
555                let mut payload = vec![];
556                if let Some(r) = raw {
557                    payload = r
558                        .data
559                        .clone()
560                        .unwrap_or("".to_string())
561                        .as_bytes()
562                        .to_vec();
563                }
564
565                Action::SendFrame {
566                    stream_id,
567                    fin_stream,
568                    frame: Frame::Data { payload },
569                }
570            },
571
572            Http3Frame::Goaway { id } => Action::SendFrame {
573                stream_id,
574                fin_stream,
575                frame: Frame::GoAway { id: *id },
576            },
577
578            _ => unimplemented!(),
579        };
580
581        ret
582    }
583}
584
585fn from_qlog_stream_type_set(
586    st: &H3StreamTypeSet, ex_data: &ExData,
587) -> Vec<Action> {
588    let mut actions = vec![];
589    let fin_stream = parse_ex_data(ex_data);
590    let stream_type = match st.stream_type {
591        qlog::events::h3::H3StreamType::Control => Some(0x0),
592        qlog::events::h3::H3StreamType::Push => Some(0x1),
593        qlog::events::h3::H3StreamType::QpackEncode => Some(0x2),
594        qlog::events::h3::H3StreamType::QpackDecode => Some(0x3),
595        qlog::events::h3::H3StreamType::Reserved |
596        qlog::events::h3::H3StreamType::Unknown => st.stream_type_value,
597        _ => None,
598    };
599
600    if let Some(ty) = stream_type {
601        actions.push(Action::OpenUniStream {
602            stream_id: st.stream_id,
603            fin_stream,
604            stream_type: ty,
605        })
606    }
607
608    actions
609}
610
611fn parse_ex_data(ex_data: &ExData) -> bool {
612    ex_data
613        .get("fin_stream")
614        .unwrap_or(&serde_json::Value::Null)
615        .as_bool()
616        .unwrap_or_default()
617}
618
619#[cfg(test)]
620mod tests {
621    use crate::actions::h3::StreamEvent;
622    use crate::actions::h3::StreamEventType;
623    use crate::encode_header_block_literal;
624    use std::time::Duration;
625
626    use super::*;
627    use quiche::h3::Header;
628    use serde_json;
629
630    const NOW: f32 = 123.0;
631    const H3I_WAIT: &str = "h3i:wait";
632
633    #[test]
634    fn ser_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        let serialized = serde_json::to_string(&ev);
645
646        let expected =
647            r#"{"time":123.0,"name":"h3i:wait","data":{"duration":12345.0}}"#;
648        assert_eq!(&serialized.unwrap(), expected);
649    }
650
651    #[test]
652    fn deser_duration_wait() {
653        let ev = JsonEvent {
654            time: NOW,
655            importance: qlog::events::EventImportance::Core,
656            name: H3I_WAIT.to_string(),
657            data: serde_json::to_value(WaitType::WaitDuration(
658                Duration::from_millis(12345),
659            ))
660            .unwrap(),
661        };
662
663        let expected =
664            r#"{"time":123.0,"name":"h3i:wait","data":{"duration":12345.0}}"#;
665        let deser = serde_json::from_str::<JsonEvent>(expected).unwrap();
666        assert_eq!(deser.data, ev.data);
667    }
668
669    #[test]
670    fn ser_stream_wait() {
671        let expected = r#"{"time":123.0,"name":"h3i:wait","data":{"stream_id":0,"type":"data"}}"#;
672        let ev = JsonEvent {
673            time: NOW,
674            importance: qlog::events::EventImportance::Core,
675            name: H3I_WAIT.to_string(),
676            data: serde_json::to_value(StreamEvent {
677                stream_id: 0,
678                event_type: StreamEventType::Data,
679            })
680            .unwrap(),
681        };
682
683        let serialized = serde_json::to_string(&ev);
684        assert_eq!(&serialized.unwrap(), expected);
685    }
686
687    #[test]
688    fn deser_stream_wait() {
689        let ev = JsonEvent {
690            time: NOW,
691            importance: qlog::events::EventImportance::Core,
692            name: H3I_WAIT.to_string(),
693            data: serde_json::to_value(StreamEvent {
694                stream_id: 0,
695                event_type: StreamEventType::Data,
696            })
697            .unwrap(),
698        };
699
700        let expected = r#"{"time":123.0,"name":"h3i:wait","data":{"stream_id":0,"type":"data"}}"#;
701        let deser = serde_json::from_str::<JsonEvent>(expected).unwrap();
702        assert_eq!(deser.data, ev.data);
703    }
704
705    #[test]
706    fn deser_http_headers_to_action() {
707        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}"#;
708        let deserialized = serde_json::from_str::<Event>(serialized).unwrap();
709        let actions = actions_from_qlog(deserialized, None);
710        assert!(actions.0.len() == 1);
711
712        let headers = vec![
713            Header::new(b":method", b"GET"),
714            Header::new(b":authority", b"example.net"),
715            Header::new(b":path", b"/"),
716            Header::new(b":scheme", b"https"),
717        ];
718        let header_block = encode_header_block(&headers).unwrap();
719        let frame = Frame::Headers { header_block };
720        let expected = Action::SendHeadersFrame {
721            stream_id: 0,
722            fin_stream: true,
723            literal_headers: false,
724            headers,
725            frame,
726        };
727
728        assert_eq!(actions.0[0], expected);
729    }
730
731    #[test]
732    fn deser_http_headers_host_overrid_to_action() {
733        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}"#;
734        let deserialized = serde_json::from_str::<Event>(serialized).unwrap();
735        let actions = actions_from_qlog(deserialized, Some("example.org"));
736        assert!(actions.0.len() == 1);
737
738        let headers = vec![
739            Header::new(b":method", b"GET"),
740            Header::new(b":authority", b"example.org"),
741            Header::new(b":path", b"/"),
742            Header::new(b":scheme", b"https"),
743        ];
744        let header_block = encode_header_block(&headers).unwrap();
745        let frame = Frame::Headers { header_block };
746        let expected = Action::SendHeadersFrame {
747            stream_id: 0,
748            fin_stream: true,
749            literal_headers: false,
750            headers,
751            frame,
752        };
753
754        assert_eq!(actions.0[0], expected);
755    }
756
757    #[test]
758    fn deser_http_headers_literal_to_action() {
759        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}"#;
760        let deserialized = serde_json::from_str::<Event>(serialized).unwrap();
761        let actions = actions_from_qlog(deserialized, None);
762        assert!(actions.0.len() == 1);
763
764        let headers = vec![
765            Header::new(b":method", b"GET"),
766            Header::new(b":authority", b"bla.com"),
767            Header::new(b":path", b"/"),
768            Header::new(b":scheme", b"https"),
769            Header::new(b"Foo", b"bar"),
770        ];
771        let header_block = encode_header_block_literal(&headers).unwrap();
772        let frame = Frame::Headers { header_block };
773        let expected = Action::SendHeadersFrame {
774            stream_id: 0,
775            fin_stream: true,
776            literal_headers: true,
777            headers,
778            frame,
779        };
780
781        assert_eq!(actions.0[0], expected);
782    }
783}