internal/http3: QPACK encoding and decoding Basic support for encoding/decoding QPACK headers. QPACK supports three forms of header compression: Huffman-encoding of literal strings, a static table of well-known header values, and a dynamic table of header values negotiated between encoder and decoder at runtime. Right now, we support Huffman compression and the static table, but not the dynamic table. This is a supported mode for a QPACK encoder or decoder, so we can leave dynamic table support for after the rest of HTTP/3 is working. For golang/go#70914 Change-Id: Ib694199b99c752a220d43f3a309169b16020b474 Reviewed-on: https://go-review.googlesource.com/c/net/+/642599 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Auto-Submit: Damien Neil <dneil@google.com> Reviewed-by: Jonathan Amsterdam <jba@google.com> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/internal/http3/http3_test.go b/internal/http3/http3_test.go index c632c89..281c0cd 100644 --- a/internal/http3/http3_test.go +++ b/internal/http3/http3_test.go
@@ -7,8 +7,10 @@ package http3 import ( + "encoding/hex" "os" "slices" + "strings" "testing" "testing/synctest" ) @@ -57,3 +59,17 @@ f() } } + +func unhex(s string) []byte { + b, err := hex.DecodeString(strings.Map(func(c rune) rune { + switch c { + case ' ', '\t', '\n': + return -1 // ignore + } + return c + }, s)) + if err != nil { + panic(err) + } + return b +}
diff --git a/internal/http3/qpack.go b/internal/http3/qpack.go index 5dcdf6a..66f4e29 100644 --- a/internal/http3/qpack.go +++ b/internal/http3/qpack.go
@@ -7,11 +7,190 @@ package http3 import ( + "errors" "io" "golang.org/x/net/http2/hpack" ) +// QPACK (RFC 9204) header compression wire encoding. +// https://www.rfc-editor.org/rfc/rfc9204.html + +// tableType is the static or dynamic table. +// +// The T bit in QPACK instructions indicates whether a table index refers to +// the dynamic (T=0) or static (T=1) table. tableTypeForTBit and tableType.tbit +// convert a T bit from the wire encoding to/from a tableType. +type tableType byte + +const ( + dynamicTable = 0x00 // T=0, dynamic table + staticTable = 0xff // T=1, static table +) + +// tableTypeForTbit returns the table type corresponding to a T bit value. +// The input parameter contains a byte masked to contain only the T bit. +func tableTypeForTbit(bit byte) tableType { + if bit == 0 { + return dynamicTable + } + return staticTable +} + +// tbit produces the T bit corresponding to the table type. +// The input parameter contains a byte with the T bit set to 1, +// and the return is either the input or 0 depending on the table type. +func (t tableType) tbit(bit byte) byte { + return bit & byte(t) +} + +// indexType indicates a literal's indexing status. +// +// The N bit in QPACK instructions indicates whether a literal is "never-indexed". +// A never-indexed literal (N=1) must not be encoded as an indexed literal if it +// forwarded on another connection. +// +// (See https://www.rfc-editor.org/rfc/rfc9204.html#section-7.1 for details on the +// security reasons for never-indexed literals.) +type indexType byte + +const ( + mayIndex = 0x00 // N=0, not a never-indexed literal + neverIndex = 0xff // N=1, never-indexed literal +) + +// indexTypeForNBit returns the index type corresponding to a N bit value. +// The input parameter contains a byte masked to contain only the N bit. +func indexTypeForNBit(bit byte) indexType { + if bit == 0 { + return mayIndex + } + return neverIndex +} + +// nbit produces the N bit corresponding to the table type. +// The input parameter contains a byte with the N bit set to 1, +// and the return is either the input or 0 depending on the table type. +func (t indexType) nbit(bit byte) byte { + return bit & byte(t) +} + +// Indexed Field Line: +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | 1 | T | Index (6+) | +// +---+---+-----------------------+ +// +// https://www.rfc-editor.org/rfc/rfc9204.html#section-4.5.2 + +func appendIndexedFieldLine(b []byte, ttype tableType, index int) []byte { + const tbit = 0b_01000000 + return appendPrefixedInt(b, 0b_1000_0000|ttype.tbit(tbit), 6, int64(index)) +} + +func (st *stream) decodeIndexedFieldLine(b byte) (itype indexType, name, value string, err error) { + index, err := st.readPrefixedIntWithByte(b, 6) + if err != nil { + return 0, "", "", err + } + const tbit = 0b_0100_0000 + if tableTypeForTbit(b&tbit) == staticTable { + ent, err := staticTableEntry(index) + if err != nil { + return 0, "", "", err + } + return mayIndex, ent.name, ent.value, nil + } else { + return 0, "", "", errors.New("dynamic table is not supported yet") + } +} + +// Literal Field Line With Name Reference: +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | 0 | 1 | N | T |Name Index (4+)| +// +---+---+---+---+---------------+ +// | H | Value Length (7+) | +// +---+---------------------------+ +// | Value String (Length bytes) | +// +-------------------------------+ +// +// https://www.rfc-editor.org/rfc/rfc9204.html#section-4.5.4 + +func appendLiteralFieldLineWithNameReference(b []byte, ttype tableType, itype indexType, nameIndex int, value string) []byte { + const tbit = 0b_0001_0000 + const nbit = 0b_0010_0000 + b = appendPrefixedInt(b, 0b_0100_0000|itype.nbit(nbit)|ttype.tbit(tbit), 4, int64(nameIndex)) + b = appendPrefixedString(b, 0, 7, value) + return b +} + +func (st *stream) decodeLiteralFieldLineWithNameReference(b byte) (itype indexType, name, value string, err error) { + nameIndex, err := st.readPrefixedIntWithByte(b, 4) + if err != nil { + return 0, "", "", err + } + + const tbit = 0b_0001_0000 + if tableTypeForTbit(b&tbit) == staticTable { + ent, err := staticTableEntry(nameIndex) + if err != nil { + return 0, "", "", err + } + name = ent.name + } else { + return 0, "", "", errors.New("dynamic table is not supported yet") + } + + _, value, err = st.readPrefixedString(7) + if err != nil { + return 0, "", "", err + } + + const nbit = 0b_0010_0000 + itype = indexTypeForNBit(b & nbit) + + return itype, name, value, nil +} + +// Literal Field Line with Literal Name: +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | 0 | 0 | 1 | N | H |NameLen(3+)| +// +---+---+---+---+---+-----------+ +// | Name String (Length bytes) | +// +---+---------------------------+ +// | H | Value Length (7+) | +// +---+---------------------------+ +// | Value String (Length bytes) | +// +-------------------------------+ +// +// https://www.rfc-editor.org/rfc/rfc9204.html#section-4.5.6 + +func appendLiteralFieldLineWithLiteralName(b []byte, itype indexType, name, value string) []byte { + const nbit = 0b_0001_0000 + b = appendPrefixedString(b, 0b_0010_0000|itype.nbit(nbit), 3, name) + b = appendPrefixedString(b, 0, 7, value) + return b +} + +func (st *stream) decodeLiteralFieldLineWithLiteralName(b byte) (itype indexType, name, value string, err error) { + name, err = st.readPrefixedStringWithByte(b, 3) + if err != nil { + return 0, "", "", err + } + _, value, err = st.readPrefixedString(7) + if err != nil { + return 0, "", "", err + } + const nbit = 0b_0001_0000 + itype = indexTypeForNBit(b & nbit) + return itype, name, value, nil +} + // Prefixed-integer encoding from RFC 7541, section 5.1 // // Prefixed integers consist of some number of bits of data, @@ -135,7 +314,9 @@ return string(data), nil } -// appendPrefixedString appends an RFC 7541 string to st. +// appendPrefixedString appends an RFC 7541 string to st, +// applying Huffman encoding and setting the H bit (indicating Huffman encoding) +// when appropriate. // // The firstByte parameter includes the non-integer bits of the first byte. // The other bits must be zero.
diff --git a/internal/http3/qpack_decode.go b/internal/http3/qpack_decode.go new file mode 100644 index 0000000..018867a --- /dev/null +++ b/internal/http3/qpack_decode.go
@@ -0,0 +1,83 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.24 + +package http3 + +import ( + "errors" + "math/bits" +) + +type qpackDecoder struct { + // The decoder has no state for now, + // but that'll change once we add dynamic table support. + // + // TODO: dynamic table support. +} + +func (qd *qpackDecoder) decode(st *stream, f func(itype indexType, name, value string) error) error { + // Encoded Field Section prefix. + + // We set SETTINGS_QPACK_MAX_TABLE_CAPACITY to 0, + // so the Required Insert Count must be 0. + _, requiredInsertCount, err := st.readPrefixedInt(8) + if err != nil { + return err + } + if requiredInsertCount != 0 { + return errQPACKDecompressionFailed + } + + // Delta Base. We don't use the dynamic table yet, so this may be ignored. + _, _, err = st.readPrefixedInt(7) + if err != nil { + return err + } + + sawNonPseudo := false + for st.lim > 0 { + firstByte, err := st.ReadByte() + if err != nil { + return err + } + var name, value string + var itype indexType + switch bits.LeadingZeros8(firstByte) { + case 0: + // Indexed Field Line + itype, name, value, err = st.decodeIndexedFieldLine(firstByte) + case 1: + // Literal Field Line With Name Reference + itype, name, value, err = st.decodeLiteralFieldLineWithNameReference(firstByte) + case 2: + // Literal Field Line with Literal Name + itype, name, value, err = st.decodeLiteralFieldLineWithLiteralName(firstByte) + case 3: + // Indexed Field Line With Post-Base Index + err = errors.New("dynamic table is not supported yet") + case 4: + // Indexed Field Line With Post-Base Name Reference + err = errors.New("dynamic table is not supported yet") + } + if err != nil { + return err + } + if len(name) == 0 { + return errH3MessageError + } + if name[0] == ':' { + if sawNonPseudo { + return errH3MessageError + } + } else { + sawNonPseudo = true + } + if err := f(itype, name, value); err != nil { + return err + } + } + return nil +}
diff --git a/internal/http3/qpack_decode_test.go b/internal/http3/qpack_decode_test.go new file mode 100644 index 0000000..1b779aa --- /dev/null +++ b/internal/http3/qpack_decode_test.go
@@ -0,0 +1,196 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.24 + +package http3 + +import ( + "reflect" + "strings" + "testing" +) + +func TestQPACKDecode(t *testing.T) { + type header struct { + itype indexType + name, value string + } + // Many test cases here taken from Google QUICHE, + // quiche/quic/core/qpack/qpack_encoder_test.cc. + for _, test := range []struct { + name string + enc []byte + want []header + }{{ + name: "empty", + enc: unhex("0000"), + want: []header{}, + }, { + name: "literal entry empty value", + enc: unhex("000023666f6f00"), + want: []header{ + {mayIndex, "foo", ""}, + }, + }, { + name: "simple literal entry", + enc: unhex("000023666f6f03626172"), + want: []header{ + {mayIndex, "foo", "bar"}, + }, + }, { + name: "multiple literal entries", + enc: unhex("0000" + // prefix + // foo: bar + "23666f6f03626172" + + // 7 octet long header name, the smallest number + // that does not fit on a 3-bit prefix. + "2700666f6f62616172" + + // 127 octet long header value, the smallest number + // that does not fit on a 7-bit prefix. + "7f00616161616161616161616161616161616161616161616161616161616161616161" + + "6161616161616161616161616161616161616161616161616161616161616161616161" + + "6161616161616161616161616161616161616161616161616161616161616161616161" + + "616161616161616161616161616161616161616161616161", + ), + want: []header{ + {mayIndex, "foo", "bar"}, + {mayIndex, "foobaar", strings.Repeat("a", 127)}, + }, + }, { + name: "line feed in value", + enc: unhex("000023666f6f0462610a72"), + want: []header{ + {mayIndex, "foo", "ba\nr"}, + }, + }, { + name: "huffman simple", + enc: unhex("00002f0125a849e95ba97d7f8925a849e95bb8e8b4bf"), + want: []header{ + {mayIndex, "custom-key", "custom-value"}, + }, + }, { + name: "alternating huffman nonhuffman", + enc: unhex("0000" + // Prefix. + "2f0125a849e95ba97d7f" + // Huffman-encoded name. + "8925a849e95bb8e8b4bf" + // Huffman-encoded value. + "2703637573746f6d2d6b6579" + // Non-Huffman encoded name. + "0c637573746f6d2d76616c7565" + // Non-Huffman encoded value. + "2f0125a849e95ba97d7f" + // Huffman-encoded name. + "0c637573746f6d2d76616c7565" + // Non-Huffman encoded value. + "2703637573746f6d2d6b6579" + // Non-Huffman encoded name. + "8925a849e95bb8e8b4bf", // Huffman-encoded value. + ), + want: []header{ + {mayIndex, "custom-key", "custom-value"}, + {mayIndex, "custom-key", "custom-value"}, + {mayIndex, "custom-key", "custom-value"}, + {mayIndex, "custom-key", "custom-value"}, + }, + }, { + name: "static table", + enc: unhex("0000d1d45f00055452414345dfcc5f108621e9aec2a11f5c8294e75f1000"), + want: []header{ + {mayIndex, ":method", "GET"}, + {mayIndex, ":method", "POST"}, + {mayIndex, ":method", "TRACE"}, + {mayIndex, "accept-encoding", "gzip, deflate, br"}, + {mayIndex, "location", ""}, + {mayIndex, "accept-encoding", "compress"}, + {mayIndex, "location", "foo"}, + {mayIndex, "accept-encoding", ""}, + }, + }} { + runSynctestSubtest(t, test.name, func(t testing.TB) { + st1, st2 := newStreamPair(t) + st1.Write(test.enc) + st1.Flush() + + st2.lim = int64(len(test.enc)) + + var dec qpackDecoder + got := []header{} + err := dec.decode(st2, func(itype indexType, name, value string) error { + got = append(got, header{itype, name, value}) + return nil + }) + if err != nil { + t.Fatalf("decode: %v", err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("encoded: %x", test.enc) + t.Errorf("got headers:") + for _, h := range got { + t.Errorf(" %v: %q", h.name, h.value) + } + t.Errorf("want headers:") + for _, h := range test.want { + t.Errorf(" %v: %q", h.name, h.value) + } + } + }) + } +} + +func TestQPACKDecodeErrors(t *testing.T) { + // Many test cases here taken from Google QUICHE, + // quiche/quic/core/qpack/qpack_encoder_test.cc. + for _, test := range []struct { + name string + enc []byte + }{{ + name: "literal entry empty name", + enc: unhex("00002003666f6f"), + }, { + name: "literal entry empty name and value", + enc: unhex("00002000"), + }, { + name: "name length too large for varint", + enc: unhex("000027ffffffffffffffffffff"), + }, { + name: "string literal too long", + enc: unhex("000027ffff7f"), + }, { + name: "value length too large for varint", + enc: unhex("000023666f6f7fffffffffffffffffffff"), + }, { + name: "value length too long", + enc: unhex("000023666f6f7fffff7f"), + }, { + name: "incomplete header block", + enc: unhex("00002366"), + }, { + name: "huffman name does not have eos prefix", + enc: unhex("00002f0125a849e95ba97d7e8925a849e95bb8e8b4bf"), + }, { + name: "huffman value does not have eos prefix", + enc: unhex("00002f0125a849e95ba97d7f8925a849e95bb8e8b4be"), + }, { + name: "huffman name eos prefix too long", + enc: unhex("00002f0225a849e95ba97d7fff8925a849e95bb8e8b4bf"), + }, { + name: "huffman value eos prefix too long", + enc: unhex("00002f0125a849e95ba97d7f8a25a849e95bb8e8b4bfff"), + }, { + name: "too high static table index", + enc: unhex("0000ff23ff24"), + }} { + runSynctestSubtest(t, test.name, func(t testing.TB) { + st1, st2 := newStreamPair(t) + st1.Write(test.enc) + st1.Flush() + + st2.lim = int64(len(test.enc)) + + var dec qpackDecoder + err := dec.decode(st2, func(itype indexType, name, value string) error { + return nil + }) + if err == nil { + t.Errorf("encoded: %x", test.enc) + t.Fatalf("decode succeeded; want error") + } + }) + } +}
diff --git a/internal/http3/qpack_encode.go b/internal/http3/qpack_encode.go new file mode 100644 index 0000000..0f35e0c --- /dev/null +++ b/internal/http3/qpack_encode.go
@@ -0,0 +1,47 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.24 + +package http3 + +type qpackEncoder struct { + // The encoder has no state for now, + // but that'll change once we add dynamic table support. + // + // TODO: dynamic table support. +} + +func (qe *qpackEncoder) init() { + staticTableOnce.Do(initStaticTableMaps) +} + +// encode encodes a list of headers into a QPACK encoded field section. +// +// The headers func must produce the same headers on repeated calls, +// although the order may vary. +func (qe *qpackEncoder) encode(headers func(func(itype indexType, name, value string))) []byte { + // Encoded Field Section prefix. + // + // We don't yet use the dynamic table, so both values here are zero. + var b []byte + b = appendPrefixedInt(b, 0, 8, 0) // Required Insert Count + b = appendPrefixedInt(b, 0, 7, 0) // Delta Base + + headers(func(itype indexType, name, value string) { + if itype == mayIndex { + if i, ok := staticTableByNameValue[tableEntry{name, value}]; ok { + b = appendIndexedFieldLine(b, staticTable, i) + return + } + } + if i, ok := staticTableByName[name]; ok { + b = appendLiteralFieldLineWithNameReference(b, staticTable, itype, i, value) + } else { + b = appendLiteralFieldLineWithLiteralName(b, itype, name, value) + } + }) + + return b +}
diff --git a/internal/http3/qpack_encode_test.go b/internal/http3/qpack_encode_test.go new file mode 100644 index 0000000..f426d77 --- /dev/null +++ b/internal/http3/qpack_encode_test.go
@@ -0,0 +1,126 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.24 + +package http3 + +import ( + "bytes" + "strings" + "testing" +) + +func TestQPACKEncode(t *testing.T) { + type header struct { + itype indexType + name, value string + } + // Many test cases here taken from Google QUICHE, + // quiche/quic/core/qpack/qpack_encoder_test.cc. + for _, test := range []struct { + name string + headers []header + want []byte + }{{ + name: "empty", + headers: []header{}, + want: unhex("0000"), + }, { + name: "empty name", + headers: []header{ + {mayIndex, "", "foo"}, + }, + want: unhex("0000208294e7"), + }, { + name: "empty value", + headers: []header{ + {mayIndex, "foo", ""}, + }, + want: unhex("00002a94e700"), + }, { + name: "empty name and value", + headers: []header{ + {mayIndex, "", ""}, + }, + want: unhex("00002000"), + }, { + name: "simple", + headers: []header{ + {mayIndex, "foo", "bar"}, + }, + want: unhex("00002a94e703626172"), + }, { + name: "multiple", + headers: []header{ + {mayIndex, "foo", "bar"}, + {mayIndex, "ZZZZZZZ", strings.Repeat("Z", 127)}, + }, + want: unhex("0000" + // prefix + // foo: bar + "2a94e703626172" + + // 7 octet long header name, the smallest number + // that does not fit on a 3-bit prefix. + "27005a5a5a5a5a5a5a" + + // 127 octet long header value, the smallest + // number that does not fit on a 7-bit prefix. + "7f005a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a"), + }, { + name: "static table 1", + headers: []header{ + {mayIndex, ":method", "GET"}, + {mayIndex, "accept-encoding", "gzip, deflate, br"}, + {mayIndex, "location", ""}, + }, + want: unhex("0000d1dfcc"), + }, { + name: "static table 2", + headers: []header{ + {mayIndex, ":method", "POST"}, + {mayIndex, "accept-encoding", "compress"}, + {mayIndex, "location", "foo"}, + }, + want: unhex("0000d45f108621e9aec2a11f5c8294e7"), + }, { + name: "static table 3", + headers: []header{ + {mayIndex, ":method", "TRACE"}, + {mayIndex, "accept-encoding", ""}, + }, + want: unhex("00005f000554524143455f1000"), + }, { + name: "never indexed literal field line with name reference", + headers: []header{ + {neverIndex, ":method", ""}, + }, + want: unhex("00007f0000"), + }, { + name: "never indexed literal field line with literal name", + headers: []header{ + {neverIndex, "a", "b"}, + }, + want: unhex("000031610162"), + }} { + t.Run(test.name, func(t *testing.T) { + var enc qpackEncoder + enc.init() + + got := enc.encode(func(f func(itype indexType, name, value string)) { + for _, h := range test.headers { + f(h.itype, h.name, h.value) + } + }) + if !bytes.Equal(got, test.want) { + for _, h := range test.headers { + t.Logf("header %v: %q", h.name, h.value) + } + t.Errorf("got: %x", got) + t.Errorf("want: %x", test.want) + } + }) + } +}
diff --git a/internal/http3/qpack_static.go b/internal/http3/qpack_static.go new file mode 100644 index 0000000..cb0884e --- /dev/null +++ b/internal/http3/qpack_static.go
@@ -0,0 +1,144 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.24 + +package http3 + +import "sync" + +type tableEntry struct { + name string + value string +} + +// staticTableEntry returns the static table entry with the given index. +func staticTableEntry(index int64) (tableEntry, error) { + if index >= int64(len(staticTableEntries)) { + return tableEntry{}, errQPACKDecompressionFailed + } + return staticTableEntries[index], nil +} + +func initStaticTableMaps() { + staticTableByName = make(map[string]int) + staticTableByNameValue = make(map[tableEntry]int) + for i, ent := range staticTableEntries { + if _, ok := staticTableByName[ent.name]; !ok { + staticTableByName[ent.name] = i + } + staticTableByNameValue[ent] = i + } +} + +var ( + staticTableOnce sync.Once + staticTableByName map[string]int + staticTableByNameValue map[tableEntry]int +) + +// https://www.rfc-editor.org/rfc/rfc9204.html#appendix-A +// +// Note that this is different from the HTTP/2 static table. +var staticTableEntries = [...]tableEntry{ + 0: {":authority", ""}, + 1: {":path", "/"}, + 2: {"age", "0"}, + 3: {"content-disposition", ""}, + 4: {"content-length", "0"}, + 5: {"cookie", ""}, + 6: {"date", ""}, + 7: {"etag", ""}, + 8: {"if-modified-since", ""}, + 9: {"if-none-match", ""}, + 10: {"last-modified", ""}, + 11: {"link", ""}, + 12: {"location", ""}, + 13: {"referer", ""}, + 14: {"set-cookie", ""}, + 15: {":method", "CONNECT"}, + 16: {":method", "DELETE"}, + 17: {":method", "GET"}, + 18: {":method", "HEAD"}, + 19: {":method", "OPTIONS"}, + 20: {":method", "POST"}, + 21: {":method", "PUT"}, + 22: {":scheme", "http"}, + 23: {":scheme", "https"}, + 24: {":status", "103"}, + 25: {":status", "200"}, + 26: {":status", "304"}, + 27: {":status", "404"}, + 28: {":status", "503"}, + 29: {"accept", "*/*"}, + 30: {"accept", "application/dns-message"}, + 31: {"accept-encoding", "gzip, deflate, br"}, + 32: {"accept-ranges", "bytes"}, + 33: {"access-control-allow-headers", "cache-control"}, + 34: {"access-control-allow-headers", "content-type"}, + 35: {"access-control-allow-origin", "*"}, + 36: {"cache-control", "max-age=0"}, + 37: {"cache-control", "max-age=2592000"}, + 38: {"cache-control", "max-age=604800"}, + 39: {"cache-control", "no-cache"}, + 40: {"cache-control", "no-store"}, + 41: {"cache-control", "public, max-age=31536000"}, + 42: {"content-encoding", "br"}, + 43: {"content-encoding", "gzip"}, + 44: {"content-type", "application/dns-message"}, + 45: {"content-type", "application/javascript"}, + 46: {"content-type", "application/json"}, + 47: {"content-type", "application/x-www-form-urlencoded"}, + 48: {"content-type", "image/gif"}, + 49: {"content-type", "image/jpeg"}, + 50: {"content-type", "image/png"}, + 51: {"content-type", "text/css"}, + 52: {"content-type", "text/html; charset=utf-8"}, + 53: {"content-type", "text/plain"}, + 54: {"content-type", "text/plain;charset=utf-8"}, + 55: {"range", "bytes=0-"}, + 56: {"strict-transport-security", "max-age=31536000"}, + 57: {"strict-transport-security", "max-age=31536000; includesubdomains"}, + 58: {"strict-transport-security", "max-age=31536000; includesubdomains; preload"}, + 59: {"vary", "accept-encoding"}, + 60: {"vary", "origin"}, + 61: {"x-content-type-options", "nosniff"}, + 62: {"x-xss-protection", "1; mode=block"}, + 63: {":status", "100"}, + 64: {":status", "204"}, + 65: {":status", "206"}, + 66: {":status", "302"}, + 67: {":status", "400"}, + 68: {":status", "403"}, + 69: {":status", "421"}, + 70: {":status", "425"}, + 71: {":status", "500"}, + 72: {"accept-language", ""}, + 73: {"access-control-allow-credentials", "FALSE"}, + 74: {"access-control-allow-credentials", "TRUE"}, + 75: {"access-control-allow-headers", "*"}, + 76: {"access-control-allow-methods", "get"}, + 77: {"access-control-allow-methods", "get, post, options"}, + 78: {"access-control-allow-methods", "options"}, + 79: {"access-control-expose-headers", "content-length"}, + 80: {"access-control-request-headers", "content-type"}, + 81: {"access-control-request-method", "get"}, + 82: {"access-control-request-method", "post"}, + 83: {"alt-svc", "clear"}, + 84: {"authorization", ""}, + 85: {"content-security-policy", "script-src 'none'; object-src 'none'; base-uri 'none'"}, + 86: {"early-data", "1"}, + 87: {"expect-ct", ""}, + 88: {"forwarded", ""}, + 89: {"if-range", ""}, + 90: {"origin", ""}, + 91: {"purpose", "prefetch"}, + 92: {"server", ""}, + 93: {"timing-allow-origin", "*"}, + 94: {"upgrade-insecure-requests", "1"}, + 95: {"user-agent", ""}, + 96: {"x-forwarded-for", ""}, + 97: {"x-frame-options", "deny"}, + 98: {"x-frame-options", "sameorigin"}, +}
diff --git a/internal/http3/stream_test.go b/internal/http3/stream_test.go index d76b890..62140d0 100644 --- a/internal/http3/stream_test.go +++ b/internal/http3/stream_test.go
@@ -263,7 +263,7 @@ } } -func newStreamPair(t *testing.T) (s1, s2 *stream) { +func newStreamPair(t testing.TB) (s1, s2 *stream) { t.Helper() q1, q2 := newQUICStreamPair(t) return newStream(q1), newStream(q2)