From f8fdf06ff8054589918afd371c125dd465789265 Mon Sep 17 00:00:00 2001 From: gngpp Date: Tue, 9 Jun 2026 19:51:13 +0800 Subject: [PATCH 01/22] feat(qpack): implement qpack decoder --- .duvet/config.toml | 3 + .duvet/exceptions/rfc9204/4.2.toml | 24 + .duvet/snapshot.txt | 179 ++ .../www.rfc-editor.org/rfc/rfc9204.txt | 2133 +++++++++++++++++ examples/client.rs | 13 +- h3-quinn/Cargo.toml | 4 +- h3/src/client/builder.rs | 2 + h3/src/client/connection.rs | 14 +- h3/src/client/stream.rs | 2 +- h3/src/connection.rs | 344 ++- h3/src/qpack/decoder.rs | 10 + h3/src/qpack/mod.rs | 2 +- h3/src/server/connection.rs | 4 +- h3/src/server/request.rs | 28 +- 14 files changed, 2692 insertions(+), 70 deletions(-) create mode 100644 .duvet/exceptions/rfc9204/4.2.toml create mode 100644 .duvet/specifications/www.rfc-editor.org/rfc/rfc9204.txt diff --git a/.duvet/config.toml b/.duvet/config.toml index 8482911..283d388 100644 --- a/.duvet/config.toml +++ b/.duvet/config.toml @@ -6,6 +6,9 @@ pattern = "h3/**/*.rs" [[specification]] source = "https://www.rfc-editor.org/rfc/rfc9114" +[[specification]] +source = "https://www.rfc-editor.org/rfc/rfc9204.html" + [report.html] enabled = true issue-link = "https://github.com/hyperium/h3/issues" diff --git a/.duvet/exceptions/rfc9204/4.2.toml b/.duvet/exceptions/rfc9204/4.2.toml new file mode 100644 index 0000000..9e31fe9 --- /dev/null +++ b/.duvet/exceptions/rfc9204/4.2.toml @@ -0,0 +1,24 @@ +[[exception]] +target = "https://www.rfc-editor.org/rfc/rfc9204.html#section-4.2" +quote = ''' +An endpoint MAY avoid creating an encoder stream if it will not be +used (for example, if its encoder does not wish to use the dynamic +table or if the maximum size of the dynamic table permitted by the +peer is zero). +''' +reason = ''' +H3 creates the streams even if they are not used. +See https://github.com/hyperium/h3/pull/242 +''' + + +[[exception]] +target = "https://www.rfc-editor.org/rfc/rfc9204.html#section-4.2" +quote = ''' +An endpoint MAY avoid creating a decoder stream if its decoder sets +the maximum capacity of the dynamic table to zero. +''' +reason = ''' +H3 creates the streams even if they are not used. +See https://github.com/hyperium/h3/pull/242 +''' diff --git a/.duvet/snapshot.txt b/.duvet/snapshot.txt index a11d810..f5e1ff1 100644 --- a/.duvet/snapshot.txt +++ b/.duvet/snapshot.txt @@ -865,3 +865,182 @@ SPECIFICATION: https://www.rfc-editor.org/rfc/rfc9114 TEXT[!MUST,exception]: values of N (that is, 0x21, 0x40, ..., through 0x3ffffffffffffffe) TEXT[!MUST,exception]: MUST NOT be assigned by IANA and MUST NOT appear in the listing of TEXT[!MUST,exception]: assigned values. + +SPECIFICATION: https://www.rfc-editor.org/rfc/rfc9204.html + SECTION: [Encoder](#section-2.1) + TEXT[!MAY]: An encoder MAY insert any entry in the dynamic table it chooses; it + TEXT[!MAY]: is not limited to field lines it is compressing. + TEXT[!MUST]: An encoder MUST emit field representations in the order + TEXT[!MUST]: they appear in the input field section. + + SECTION: [Limits on Dynamic Table Insertions](#section-2.1.1) + TEXT[!MUST]: If the dynamic table does not contain enough room for a new entry + TEXT[!MUST]: without evicting other entries, and the entries that would be evicted + TEXT[!MUST]: are not evictable, the encoder MUST NOT insert that entry into the + TEXT[!MUST]: dynamic table (including duplicates of existing entries). + + SECTION: [Blocked Streams](#section-2.1.2) + TEXT[!MUST]: An encoder MUST limit the number of streams that could + TEXT[!MUST]: become blocked to the value of SETTINGS_QPACK_BLOCKED_STREAMS at all + TEXT[!MUST]: times. + TEXT[!MUST]: If a decoder encounters more blocked streams than it promised + TEXT[!MUST]: to support, it MUST treat this as a connection error of type + TEXT[!MUST]: QPACK_DECOMPRESSION_FAILED. + + SECTION: [Avoiding Flow-Control Deadlocks](#section-2.1.3) + TEXT[!SHOULD]: To avoid these deadlocks, an encoder SHOULD NOT write an instruction + TEXT[!SHOULD]: unless sufficient stream and connection flow-control credit is + TEXT[!SHOULD]: available for the entire instruction. + + SECTION: [Decoder](#section-2.2) + TEXT[!MUST]: The decoder MUST emit field lines in the order their representations + TEXT[!MUST]: appear in the encoded field section. + + SECTION: [Blocked Decoding](#section-2.2.1) + TEXT[!SHOULD]: While blocked, encoded field section data SHOULD remain in the + TEXT[!SHOULD]: blocked stream's flow-control window. + TEXT[!MUST]: If it encounters a Required Insert + TEXT[!MUST]: Count smaller than expected, it MUST treat this as a connection error + TEXT[!MUST]: of type QPACK_DECOMPRESSION_FAILED; see Section 2.2.3. + TEXT[!MAY]: If it + TEXT[!MAY]: encounters a Required Insert Count larger than expected, it MAY treat + TEXT[!MAY]: this as a connection error of type QPACK_DECOMPRESSION_FAILED. + + SECTION: [Completed Processing of a Field Section](#section-2.2.2.1) + TEXT[!MUST]: After the decoder finishes decoding a field section encoded using + TEXT[!MUST]: representations containing dynamic table references, it MUST emit a + TEXT[!MUST]: Section Acknowledgment instruction (Section 4.4.1). + + SECTION: [Abandonment of a Stream](#section-2.2.2.2) + TEXT[!MAY]: A decoder with a maximum dynamic table + TEXT[!MAY]: capacity (Section 3.2.3) equal to zero MAY omit sending Stream + TEXT[!MAY]: Cancellations, because the encoder cannot have any dynamic table + TEXT[!MAY]: references. + + SECTION: [Invalid References](#section-2.2.3) + TEXT[!MUST]: If the decoder encounters a reference in a field line representation + TEXT[!MUST]: to a dynamic table entry that has already been evicted or that has an + TEXT[!MUST]: absolute index greater than or equal to the declared Required Insert + TEXT[!MUST]: Count (Section 4.5.1), it MUST treat this as a connection error of + TEXT[!MUST]: type QPACK_DECOMPRESSION_FAILED. + TEXT[!MUST]: If the decoder encounters a reference in an encoder instruction to a + TEXT[!MUST]: dynamic table entry that has already been evicted, it MUST treat this + TEXT[!MUST]: as a connection error of type QPACK_ENCODER_STREAM_ERROR. + + SECTION: [Static Table](#section-3.1) + TEXT[!MUST]: When the decoder encounters an invalid static table index in a field + TEXT[!MUST]: line representation, it MUST treat this as a connection error of type + TEXT[!MUST]: QPACK_DECOMPRESSION_FAILED. + TEXT[!MUST]: If this index is received on the encoder + TEXT[!MUST]: stream, this MUST be treated as a connection error of type + TEXT[!MUST]: QPACK_ENCODER_STREAM_ERROR. + + SECTION: [Dynamic Table](#section-3.2) + TEXT[!MUST]: Therefore, duplicate entries MUST NOT + TEXT[!MUST]: be treated as an error by the decoder. + + SECTION: [Dynamic Table Capacity and Eviction](#section-3.2.2) + TEXT[!MUST]: The + TEXT[!MUST]: encoder MUST NOT cause a dynamic table entry to be evicted unless + TEXT[!MUST]: that entry is evictable; see Section 2.1.1. + TEXT[!MUST]: It is an error if the encoder attempts to add an + TEXT[!MUST]: entry that is larger than the dynamic table capacity; the decoder + TEXT[!MUST]: MUST treat this as a connection error of type + TEXT[!MUST]: QPACK_ENCODER_STREAM_ERROR. + + SECTION: [Maximum Dynamic Table Capacity](#section-3.2.3) + TEXT[!MUST]: The encoder MUST NOT set a dynamic table capacity that exceeds this + TEXT[!MUST]: maximum, but it can choose to use a lower dynamic table capacity; see + TEXT[!MUST]: Section 4.3.1. + TEXT[!MAY]: When the client's 0-RTT value of the + TEXT[!MAY]: SETTING is zero, the server MAY set it to a non-zero value in its + TEXT[!MAY]: SETTINGS frame. + TEXT[!MUST]: If the remembered value is non-zero, the server MUST + TEXT[!MUST]: send the same non-zero value in its SETTINGS frame. + TEXT[!MUST]: When the maximum table capacity is zero, the encoder MUST NOT insert + TEXT[!MUST]: entries into the dynamic table and MUST NOT send any encoder + TEXT[!MUST]: instructions on the encoder stream. + + SECTION: [Prefixed Integers](#section-4.1.1) + TEXT[!MUST,implementation,test]: QPACK implementations MUST be able to decode integers up to and + TEXT[!MUST,implementation,test]: including 62 bits long. + + SECTION: [Encoder and Decoder Streams](#section-4.2) + TEXT[!MUST,implementation]: Each endpoint + TEXT[!MUST,implementation]: MUST initiate, at most, one encoder stream and, at most, one decoder + TEXT[!MUST,implementation]: stream. + TEXT[!MUST,implementation]: Receipt of a second instance of either stream type MUST be + TEXT[!MUST,implementation]: treated as a connection error of type H3_STREAM_CREATION_ERROR. + TEXT[!MUST]: The sender MUST NOT close either of these streams, and the receiver + TEXT[!MUST]: MUST NOT request that the sender close either of these streams. + TEXT[!MUST]: Closure of either unidirectional stream type MUST be treated as a + TEXT[!MUST]: connection error of type H3_CLOSED_CRITICAL_STREAM. + TEXT[!MAY,exception]: An endpoint MAY avoid creating an encoder stream if it will not be + TEXT[!MAY,exception]: used (for example, if its encoder does not wish to use the dynamic + TEXT[!MAY,exception]: table or if the maximum size of the dynamic table permitted by the + TEXT[!MAY,exception]: peer is zero). + TEXT[!MAY,exception]: An endpoint MAY avoid creating a decoder stream if its decoder sets + TEXT[!MAY,exception]: the maximum capacity of the dynamic table to zero. + TEXT[!MUST,implementation]: An endpoint MUST allow its peer to create an encoder stream and a + TEXT[!MUST,implementation]: decoder stream even if the connection's settings prevent their use. + + SECTION: [Set Dynamic Table Capacity](#section-4.3.1) + TEXT[!MUST]: The new capacity MUST be lower than or equal to the limit described + TEXT[!MUST]: in Section 3.2.3. + TEXT[!MUST]: The decoder MUST treat a new dynamic table capacity + TEXT[!MUST]: value that exceeds this limit as a connection error of type + TEXT[!MUST]: QPACK_ENCODER_STREAM_ERROR. + TEXT[!MUST]: This MUST NOT cause the eviction of entries that + TEXT[!MUST]: are not evictable; see Section 2.1.1. + + SECTION: [Section Acknowledgment](#section-4.4.1) + TEXT[!MUST]: If an encoder receives a Section Acknowledgment instruction referring + TEXT[!MUST]: to a stream on which every encoded field section with a non-zero + TEXT[!MUST]: Required Insert Count has already been acknowledged, this MUST be + TEXT[!MUST]: treated as a connection error of type QPACK_DECODER_STREAM_ERROR. + + SECTION: [Insert Count Increment](#section-4.4.3) + TEXT[!MUST]: An encoder that receives an Increment field equal to zero, or one + TEXT[!MUST]: that increases the Known Received Count beyond what the encoder has + TEXT[!MUST]: sent, MUST treat this as a connection error of type + TEXT[!MUST]: QPACK_DECODER_STREAM_ERROR. + + SECTION: [Required Insert Count](#section-4.5.1.1) + TEXT[!MUST]: If the decoder encounters a value + TEXT[!MUST]: of EncodedInsertCount that could not have been produced by a + TEXT[!MUST]: conformant encoder, it MUST treat this as a connection error of type + TEXT[!MUST]: QPACK_DECOMPRESSION_FAILED. + + SECTION: [Base](#section-4.5.1.2) + TEXT[!MUST]: The value of Base MUST NOT be negative. + TEXT[!MUST]: An endpoint MUST treat a field block + TEXT[!MUST]: with a Sign bit of 1 as invalid if the value of Required Insert Count + TEXT[!MUST]: is less than or equal to the value of Delta Base. + + SECTION: [Literal Field Line with Name Reference](#section-4.5.4) + TEXT[!MUST]: When + TEXT[!MUST]: the 'N' bit is set, the encoded field line MUST always be encoded + TEXT[!MUST]: with a literal representation. + TEXT[!MUST]: In particular, when a peer sends a + TEXT[!MUST]: field line that it received represented as a literal field line with + TEXT[!MUST]: the 'N' bit set, it MUST use a literal representation to forward this + TEXT[!MUST]: field line. + + SECTION: [Never-Indexed Literals](#section-7.1.3) + TEXT[!MUST]: An intermediary MUST NOT re-encode a value that uses a literal + TEXT[!MUST]: representation with the 'N' bit set with another representation that + TEXT[!MUST]: would index it. + TEXT[!MUST]: If QPACK is used for re-encoding, a literal + TEXT[!MUST]: representation with the 'N' bit set MUST be used. + TEXT[!MUST]: If HPACK is used + TEXT[!MUST]: for re-encoding, the never-indexed literal representation (see + TEXT[!MUST]: Section 6.2.3 of [RFC7541]) MUST be used. + + SECTION: [Implementation Limits](#section-7.4) + TEXT[!SHOULD]: These limits SHOULD be large + TEXT[!SHOULD]: enough to process the largest individual field the HTTP + TEXT[!SHOULD]: implementation can be configured to accept. + TEXT[!MUST]: If an implementation encounters a value larger than it is able to + TEXT[!MUST]: decode, this MUST be treated as a stream error of type + TEXT[!MUST]: QPACK_DECOMPRESSION_FAILED if on a request stream or a connection + TEXT[!MUST]: error of the appropriate type if on the encoder or decoder stream. diff --git a/.duvet/specifications/www.rfc-editor.org/rfc/rfc9204.txt b/.duvet/specifications/www.rfc-editor.org/rfc/rfc9204.txt new file mode 100644 index 0000000..d9aa0f6 --- /dev/null +++ b/.duvet/specifications/www.rfc-editor.org/rfc/rfc9204.txt @@ -0,0 +1,2133 @@ + + + + +Internet Engineering Task Force (IETF) C. Krasic +Request for Comments: 9204 +Category: Standards Track M. Bishop +ISSN: 2070-1721 Akamai Technologies + A. Frindell, Ed. + Facebook + June 2022 + + + QPACK: Field Compression for HTTP/3 + +Abstract + + This specification defines QPACK: a compression format for + efficiently representing HTTP fields that is to be used in HTTP/3. + This is a variation of HPACK compression that seeks to reduce head- + of-line blocking. + +Status of This Memo + + This is an Internet Standards Track document. + + This document is a product of the Internet Engineering Task Force + (IETF). It represents the consensus of the IETF community. It has + received public review and has been approved for publication by the + Internet Engineering Steering Group (IESG). Further information on + Internet Standards is available in Section 2 of RFC 7841. + + Information about the current status of this document, any errata, + and how to provide feedback on it may be obtained at + https://www.rfc-editor.org/info/rfc9204. + +Copyright Notice + + Copyright (c) 2022 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (https://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Revised BSD License text as described in Section 4.e of the + Trust Legal Provisions and are provided without warranty as described + in the Revised BSD License. + +Table of Contents + + 1. Introduction + 1.1. Conventions and Definitions + 1.2. Notational Conventions + 2. Compression Process Overview + 2.1. Encoder + 2.1.1. Limits on Dynamic Table Insertions + 2.1.2. Blocked Streams + 2.1.3. Avoiding Flow-Control Deadlocks + 2.1.4. Known Received Count + 2.2. Decoder + 2.2.1. Blocked Decoding + 2.2.2. State Synchronization + 2.2.3. Invalid References + 3. Reference Tables + 3.1. Static Table + 3.2. Dynamic Table + 3.2.1. Dynamic Table Size + 3.2.2. Dynamic Table Capacity and Eviction + 3.2.3. Maximum Dynamic Table Capacity + 3.2.4. Absolute Indexing + 3.2.5. Relative Indexing + 3.2.6. Post-Base Indexing + 4. Wire Format + 4.1. Primitives + 4.1.1. Prefixed Integers + 4.1.2. String Literals + 4.2. Encoder and Decoder Streams + 4.3. Encoder Instructions + 4.3.1. Set Dynamic Table Capacity + 4.3.2. Insert with Name Reference + 4.3.3. Insert with Literal Name + 4.3.4. Duplicate + 4.4. Decoder Instructions + 4.4.1. Section Acknowledgment + 4.4.2. Stream Cancellation + 4.4.3. Insert Count Increment + 4.5. Field Line Representations + 4.5.1. Encoded Field Section Prefix + 4.5.2. Indexed Field Line + 4.5.3. Indexed Field Line with Post-Base Index + 4.5.4. Literal Field Line with Name Reference + 4.5.5. Literal Field Line with Post-Base Name Reference + 4.5.6. Literal Field Line with Literal Name + 5. Configuration + 6. Error Handling + 7. Security Considerations + 7.1. Probing Dynamic Table State + 7.1.1. Applicability to QPACK and HTTP + 7.1.2. Mitigation + 7.1.3. Never-Indexed Literals + 7.2. Static Huffman Encoding + 7.3. Memory Consumption + 7.4. Implementation Limits + 8. IANA Considerations + 8.1. Settings Registration + 8.2. Stream Type Registration + 8.3. Error Code Registration + 9. References + 9.1. Normative References + 9.2. Informative References + Appendix A. Static Table + Appendix B. Encoding and Decoding Examples + B.1. Literal Field Line with Name Reference + B.2. Dynamic Table + B.3. Speculative Insert + B.4. Duplicate Instruction, Stream Cancellation + B.5. Dynamic Table Insert, Eviction + Appendix C. Sample Single-Pass Encoding Algorithm + Acknowledgments + Authors' Addresses + +1. Introduction + + The QUIC transport protocol ([QUIC-TRANSPORT]) is designed to support + HTTP semantics, and its design subsumes many of the features of + HTTP/2 ([HTTP/2]). HTTP/2 uses HPACK ([RFC7541]) for compression of + the header and trailer sections. If HPACK were used for HTTP/3 + ([HTTP/3]), it would induce head-of-line blocking for field sections + due to built-in assumptions of a total ordering across frames on all + streams. + + QPACK reuses core concepts from HPACK, but is redesigned to allow + correctness in the presence of out-of-order delivery, with + flexibility for implementations to balance between resilience against + head-of-line blocking and optimal compression ratio. The design + goals are to closely approach the compression ratio of HPACK with + substantially less head-of-line blocking under the same loss + conditions. + +1.1. Conventions and Definitions + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all + capitals, as shown here. + + The following terms are used in this document: + + HTTP fields: Metadata sent as part of an HTTP message. The term + encompasses both header and trailer fields. Colloquially, the + term "headers" has often been used to refer to HTTP header fields + and trailer fields; this document uses "fields" for generality. + + HTTP field line: A name-value pair sent as part of an HTTP field + section. See Sections 6.3 and 6.5 of [HTTP]. + + HTTP field value: Data associated with a field name, composed from + all field line values with that field name in that section, + concatenated together with comma separators. + + Field section: An ordered collection of HTTP field lines associated + with an HTTP message. A field section can contain multiple field + lines with the same name. It can also contain duplicate field + lines. An HTTP message can include both header and trailer + sections. + + Representation: An instruction that represents a field line, + possibly by reference to the dynamic and static tables. + + Encoder: An implementation that encodes field sections. + + Decoder: An implementation that decodes encoded field sections. + + Absolute Index: A unique index for each entry in the dynamic table. + + Base: A reference point for relative and post-Base indices. + Representations that reference dynamic table entries are relative + to a Base. + + Insert Count: The total number of entries inserted in the dynamic + table. + + Note that QPACK is a name, not an abbreviation. + +1.2. Notational Conventions + + Diagrams in this document use the format described in Section 3.1 of + [RFC2360], with the following additional conventions: + + x (A) Indicates that x is A bits long. + + x (A+) Indicates that x uses the prefixed integer encoding defined + in Section 4.1.1, beginning with an A-bit prefix. + + x ... Indicates that x is variable length and extends to the end of + the region. + +2. Compression Process Overview + + Like HPACK, QPACK uses two tables for associating field lines + ("headers") to indices. The static table (Section 3.1) is predefined + and contains common header field lines (some of them with an empty + value). The dynamic table (Section 3.2) is built up over the course + of the connection and can be used by the encoder to index both header + and trailer field lines in the encoded field sections. + + QPACK defines unidirectional streams for sending instructions from + encoder to decoder and vice versa. + +2.1. Encoder + + An encoder converts a header or trailer section into a series of + representations by emitting either an indexed or a literal + representation for each field line in the list; see Section 4.5. + Indexed representations achieve high compression by replacing the + literal name and possibly the value with an index to either the + static or dynamic table. References to the static table and literal + representations do not require any dynamic state and never risk head- + of-line blocking. References to the dynamic table risk head-of-line + blocking if the encoder has not received an acknowledgment indicating + the entry is available at the decoder. + + An encoder MAY insert any entry in the dynamic table it chooses; it + is not limited to field lines it is compressing. + + QPACK preserves the ordering of field lines within each field + section. An encoder MUST emit field representations in the order + they appear in the input field section. + + QPACK is designed to place the burden of optional state tracking on + the encoder, resulting in relatively simple decoders. + +2.1.1. Limits on Dynamic Table Insertions + + Inserting entries into the dynamic table might not be possible if the + table contains entries that cannot be evicted. + + A dynamic table entry cannot be evicted immediately after insertion, + even if it has never been referenced. Once the insertion of a + dynamic table entry has been acknowledged and there are no + outstanding references to the entry in unacknowledged + representations, the entry becomes evictable. Note that references + on the encoder stream never preclude the eviction of an entry, + because those references are guaranteed to be processed before the + instruction evicting the entry. + + If the dynamic table does not contain enough room for a new entry + without evicting other entries, and the entries that would be evicted + are not evictable, the encoder MUST NOT insert that entry into the + dynamic table (including duplicates of existing entries). In order + to avoid this, an encoder that uses the dynamic table has to keep + track of each dynamic table entry referenced by each field section + until those representations are acknowledged by the decoder; see + Section 4.4.1. + +2.1.1.1. Avoiding Prohibited Insertions + + To ensure that the encoder is not prevented from adding new entries, + the encoder can avoid referencing entries that are close to eviction. + Rather than reference such an entry, the encoder can emit a Duplicate + instruction (Section 4.3.4) and reference the duplicate instead. + + Determining which entries are too close to eviction to reference is + an encoder preference. One heuristic is to target a fixed amount of + available space in the dynamic table: either unused space or space + that can be reclaimed by evicting non-blocking entries. To achieve + this, the encoder can maintain a draining index, which is the + smallest absolute index (Section 3.2.4) in the dynamic table that it + will emit a reference for. As new entries are inserted, the encoder + increases the draining index to maintain the section of the table + that it will not reference. If the encoder does not create new + references to entries with an absolute index lower than the draining + index, the number of unacknowledged references to those entries will + eventually become zero, allowing them to be evicted. + + <-- Newer Entries Older Entries --> + (Larger Indices) (Smaller Indices) + +--------+---------------------------------+----------+ + | Unused | Referenceable | Draining | + | Space | Entries | Entries | + +--------+---------------------------------+----------+ + ^ ^ ^ + | | | + Insertion Point Draining Index Dropping + Point + + Figure 1: Draining Dynamic Table Entries + +2.1.2. Blocked Streams + + Because QUIC does not guarantee order between data on different + streams, a decoder might encounter a representation that references a + dynamic table entry that it has not yet received. + + Each encoded field section contains a Required Insert Count + (Section 4.5.1), the lowest possible value for the Insert Count with + which the field section can be decoded. For a field section encoded + using references to the dynamic table, the Required Insert Count is + one larger than the largest absolute index of all referenced dynamic + table entries. For a field section encoded with no references to the + dynamic table, the Required Insert Count is zero. + + When the decoder receives an encoded field section with a Required + Insert Count greater than its own Insert Count, the stream cannot be + processed immediately and is considered "blocked"; see Section 2.2.1. + + The decoder specifies an upper bound on the number of streams that + can be blocked using the SETTINGS_QPACK_BLOCKED_STREAMS setting; see + Section 5. An encoder MUST limit the number of streams that could + become blocked to the value of SETTINGS_QPACK_BLOCKED_STREAMS at all + times. If a decoder encounters more blocked streams than it promised + to support, it MUST treat this as a connection error of type + QPACK_DECOMPRESSION_FAILED. + + Note that the decoder might not become blocked on every stream that + risks becoming blocked. + + An encoder can decide whether to risk having a stream become blocked. + If permitted by the value of SETTINGS_QPACK_BLOCKED_STREAMS, + compression efficiency can often be improved by referencing dynamic + table entries that are still in transit, but if there is loss or + reordering, the stream can become blocked at the decoder. An encoder + can avoid the risk of blocking by only referencing dynamic table + entries that have been acknowledged, but this could mean using + literals. Since literals make the encoded field section larger, this + can result in the encoder becoming blocked on congestion or flow- + control limits. + +2.1.3. Avoiding Flow-Control Deadlocks + + Writing instructions on streams that are limited by flow control can + produce deadlocks. + + A decoder might stop issuing flow-control credit on the stream that + carries an encoded field section until the necessary updates are + received on the encoder stream. If the granting of flow-control + credit on the encoder stream (or the connection as a whole) depends + on the consumption and release of data on the stream carrying the + encoded field section, a deadlock might result. + + More generally, a stream containing a large instruction can become + deadlocked if the decoder withholds flow-control credit until the + instruction is completely received. + + To avoid these deadlocks, an encoder SHOULD NOT write an instruction + unless sufficient stream and connection flow-control credit is + available for the entire instruction. + +2.1.4. Known Received Count + + The Known Received Count is the total number of dynamic table + insertions and duplications acknowledged by the decoder. The encoder + tracks the Known Received Count in order to identify which dynamic + table entries can be referenced without potentially blocking a + stream. The decoder tracks the Known Received Count in order to be + able to send Insert Count Increment instructions. + + A Section Acknowledgment instruction (Section 4.4.1) implies that the + decoder has received all dynamic table state necessary to decode the + field section. If the Required Insert Count of the acknowledged + field section is greater than the current Known Received Count, the + Known Received Count is updated to that Required Insert Count value. + + An Insert Count Increment instruction (Section 4.4.3) increases the + Known Received Count by its Increment parameter. See Section 2.2.2.3 + for guidance. + +2.2. Decoder + + As in HPACK, the decoder processes a series of representations and + emits the corresponding field sections. It also processes + instructions received on the encoder stream that modify the dynamic + table. Note that encoded field sections and encoder stream + instructions arrive on separate streams. This is unlike HPACK, where + encoded field sections (header blocks) can contain instructions that + modify the dynamic table, and there is no dedicated stream of HPACK + instructions. + + The decoder MUST emit field lines in the order their representations + appear in the encoded field section. + +2.2.1. Blocked Decoding + + Upon receipt of an encoded field section, the decoder examines the + Required Insert Count. When the Required Insert Count is less than + or equal to the decoder's Insert Count, the field section can be + processed immediately. Otherwise, the stream on which the field + section was received becomes blocked. + + While blocked, encoded field section data SHOULD remain in the + blocked stream's flow-control window. This data is unusable until + the stream becomes unblocked, and releasing the flow control + prematurely makes the decoder vulnerable to memory exhaustion + attacks. A stream becomes unblocked when the Insert Count becomes + greater than or equal to the Required Insert Count for all encoded + field sections the decoder has started reading from the stream. + + When processing encoded field sections, the decoder expects the + Required Insert Count to equal the lowest possible value for the + Insert Count with which the field section can be decoded, as + prescribed in Section 2.1.2. If it encounters a Required Insert + Count smaller than expected, it MUST treat this as a connection error + of type QPACK_DECOMPRESSION_FAILED; see Section 2.2.3. If it + encounters a Required Insert Count larger than expected, it MAY treat + this as a connection error of type QPACK_DECOMPRESSION_FAILED. + +2.2.2. State Synchronization + + The decoder signals the following events by emitting decoder + instructions (Section 4.4) on the decoder stream. + +2.2.2.1. Completed Processing of a Field Section + + After the decoder finishes decoding a field section encoded using + representations containing dynamic table references, it MUST emit a + Section Acknowledgment instruction (Section 4.4.1). A stream may + carry multiple field sections in the case of intermediate responses, + trailers, and pushed requests. The encoder interprets each + Section Acknowledgment instruction as acknowledging the earliest + unacknowledged field section containing dynamic table references sent + on the given stream. + +2.2.2.2. Abandonment of a Stream + + When an endpoint receives a stream reset before the end of a stream + or before all encoded field sections are processed on that stream, or + when it abandons reading of a stream, it generates a Stream + Cancellation instruction; see Section 4.4.2. This signals to the + encoder that all references to the dynamic table on that stream are + no longer outstanding. A decoder with a maximum dynamic table + capacity (Section 3.2.3) equal to zero MAY omit sending Stream + Cancellations, because the encoder cannot have any dynamic table + references. An encoder cannot infer from this instruction that any + updates to the dynamic table have been received. + + The Section Acknowledgment and Stream Cancellation instructions + permit the encoder to remove references to entries in the dynamic + table. When an entry with an absolute index lower than the Known + Received Count has zero references, then it is considered evictable; + see Section 2.1.1. + +2.2.2.3. New Table Entries + + After receiving new table entries on the encoder stream, the decoder + chooses when to emit Insert Count Increment instructions; see + Section 4.4.3. Emitting this instruction after adding each new + dynamic table entry will provide the timeliest feedback to the + encoder, but could be redundant with other decoder feedback. By + delaying an Insert Count Increment instruction, the decoder might be + able to coalesce multiple Insert Count Increment instructions or + replace them entirely with Section Acknowledgments; see + Section 4.4.1. However, delaying too long may lead to compression + inefficiencies if the encoder waits for an entry to be acknowledged + before using it. + +2.2.3. Invalid References + + If the decoder encounters a reference in a field line representation + to a dynamic table entry that has already been evicted or that has an + absolute index greater than or equal to the declared Required Insert + Count (Section 4.5.1), it MUST treat this as a connection error of + type QPACK_DECOMPRESSION_FAILED. + + If the decoder encounters a reference in an encoder instruction to a + dynamic table entry that has already been evicted, it MUST treat this + as a connection error of type QPACK_ENCODER_STREAM_ERROR. + +3. Reference Tables + + Unlike in HPACK, entries in the QPACK static and dynamic tables are + addressed separately. The following sections describe how entries in + each table are addressed. + +3.1. Static Table + + The static table consists of a predefined list of field lines, each + of which has a fixed index over time. Its entries are defined in + Appendix A. + + All entries in the static table have a name and a value. However, + values can be empty (that is, have a length of 0). Each entry is + identified by a unique index. + + Note that the QPACK static table is indexed from 0, whereas the HPACK + static table is indexed from 1. + + When the decoder encounters an invalid static table index in a field + line representation, it MUST treat this as a connection error of type + QPACK_DECOMPRESSION_FAILED. If this index is received on the encoder + stream, this MUST be treated as a connection error of type + QPACK_ENCODER_STREAM_ERROR. + +3.2. Dynamic Table + + The dynamic table consists of a list of field lines maintained in + first-in, first-out order. A QPACK encoder and decoder share a + dynamic table that is initially empty. The encoder adds entries to + the dynamic table and sends them to the decoder via instructions on + the encoder stream; see Section 4.3. + + The dynamic table can contain duplicate entries (i.e., entries with + the same name and same value). Therefore, duplicate entries MUST NOT + be treated as an error by the decoder. + + Dynamic table entries can have empty values. + +3.2.1. Dynamic Table Size + + The size of the dynamic table is the sum of the size of its entries. + + The size of an entry is the sum of its name's length in bytes, its + value's length in bytes, and 32 additional bytes. The size of an + entry is calculated using the length of its name and value without + Huffman encoding applied. + +3.2.2. Dynamic Table Capacity and Eviction + + The encoder sets the capacity of the dynamic table, which serves as + the upper limit on its size. The initial capacity of the dynamic + table is zero. The encoder sends a Set Dynamic Table Capacity + instruction (Section 4.3.1) with a non-zero capacity to begin using + the dynamic table. + + Before a new entry is added to the dynamic table, entries are evicted + from the end of the dynamic table until the size of the dynamic table + is less than or equal to (table capacity - size of new entry). The + encoder MUST NOT cause a dynamic table entry to be evicted unless + that entry is evictable; see Section 2.1.1. The new entry is then + added to the table. It is an error if the encoder attempts to add an + entry that is larger than the dynamic table capacity; the decoder + MUST treat this as a connection error of type + QPACK_ENCODER_STREAM_ERROR. + + A new entry can reference an entry in the dynamic table that will be + evicted when adding this new entry into the dynamic table. + Implementations are cautioned to avoid deleting the referenced name + or value if the referenced entry is evicted from the dynamic table + prior to inserting the new entry. + + Whenever the dynamic table capacity is reduced by the encoder + (Section 4.3.1), entries are evicted from the end of the dynamic + table until the size of the dynamic table is less than or equal to + the new table capacity. This mechanism can be used to completely + clear entries from the dynamic table by setting a capacity of 0, + which can subsequently be restored. + +3.2.3. Maximum Dynamic Table Capacity + + To bound the memory requirements of the decoder, the decoder limits + the maximum value the encoder is permitted to set for the dynamic + table capacity. In HTTP/3, this limit is determined by the value of + SETTINGS_QPACK_MAX_TABLE_CAPACITY sent by the decoder; see Section 5. + The encoder MUST NOT set a dynamic table capacity that exceeds this + maximum, but it can choose to use a lower dynamic table capacity; see + Section 4.3.1. + + For clients using 0-RTT data in HTTP/3, the server's maximum table + capacity is the remembered value of the setting or zero if the value + was not previously sent. When the client's 0-RTT value of the + SETTING is zero, the server MAY set it to a non-zero value in its + SETTINGS frame. If the remembered value is non-zero, the server MUST + send the same non-zero value in its SETTINGS frame. If it specifies + any other value, or omits SETTINGS_QPACK_MAX_TABLE_CAPACITY from + SETTINGS, the encoder must treat this as a connection error of type + QPACK_DECODER_STREAM_ERROR. + + For clients not using 0-RTT data (whether 0-RTT is not attempted or + is rejected) and for all HTTP/3 servers, the maximum table capacity + is 0 until the encoder processes a SETTINGS frame with a non-zero + value of SETTINGS_QPACK_MAX_TABLE_CAPACITY. + + When the maximum table capacity is zero, the encoder MUST NOT insert + entries into the dynamic table and MUST NOT send any encoder + instructions on the encoder stream. + +3.2.4. Absolute Indexing + + Each entry possesses an absolute index that is fixed for the lifetime + of that entry. The first entry inserted has an absolute index of 0; + indices increase by one with each insertion. + +3.2.5. Relative Indexing + + Relative indices begin at zero and increase in the opposite direction + from the absolute index. Determining which entry has a relative + index of 0 depends on the context of the reference. + + In encoder instructions (Section 4.3), a relative index of 0 refers + to the most recently inserted value in the dynamic table. Note that + this means the entry referenced by a given relative index will change + while interpreting instructions on the encoder stream. + + +-----+---------------+-------+ + | n-1 | ... | d | Absolute Index + + - - +---------------+ - - - + + | 0 | ... | n-d-1 | Relative Index + +-----+---------------+-------+ + ^ | + | V + Insertion Point Dropping Point + + n = count of entries inserted + d = count of entries dropped + + Figure 2: Example Dynamic Table Indexing - Encoder Stream + + Unlike in encoder instructions, relative indices in field line + representations are relative to the Base at the beginning of the + encoded field section; see Section 4.5.1. This ensures that + references are stable even if encoded field sections and dynamic + table updates are processed out of order. + + In a field line representation, a relative index of 0 refers to the + entry with absolute index equal to Base - 1. + + Base + | + V + +-----+-----+-----+-----+-------+ + | n-1 | n-2 | n-3 | ... | d | Absolute Index + +-----+-----+ - +-----+ - + + | 0 | ... | n-d-3 | Relative Index + +-----+-----+-------+ + + n = count of entries inserted + d = count of entries dropped + In this example, Base = n - 2 + + Figure 3: Example Dynamic Table Indexing - Relative Index in + Representation + +3.2.6. Post-Base Indexing + + Post-Base indices are used in field line representations for entries + with absolute indices greater than or equal to Base, starting at 0 + for the entry with absolute index equal to Base and increasing in the + same direction as the absolute index. + + Post-Base indices allow an encoder to process a field section in a + single pass and include references to entries added while processing + this (or other) field sections. + + Base + | + V + +-----+-----+-----+-----+-----+ + | n-1 | n-2 | n-3 | ... | d | Absolute Index + +-----+-----+-----+-----+-----+ + | 1 | 0 | Post-Base Index + +-----+-----+ + + n = count of entries inserted + d = count of entries dropped + In this example, Base = n - 2 + + Figure 4: Example Dynamic Table Indexing - Post-Base Index in + Representation + +4. Wire Format + +4.1. Primitives + +4.1.1. Prefixed Integers + + The prefixed integer from Section 5.1 of [RFC7541] is used heavily + throughout this document. The format from [RFC7541] is used + unmodified. Note, however, that QPACK uses some prefix sizes not + actually used in HPACK. + + QPACK implementations MUST be able to decode integers up to and + including 62 bits long. + +4.1.2. String Literals + + The string literal defined by Section 5.2 of [RFC7541] is also used + throughout. This string format includes optional Huffman encoding. + + HPACK defines string literals to begin on a byte boundary. They + begin with a single bit flag, denoted as 'H' in this document + (indicating whether the string is Huffman encoded), followed by the + string length encoded as a 7-bit prefix integer, and finally the + indicated number of bytes of data. When Huffman encoding is enabled, + the Huffman table from Appendix B of [RFC7541] is used without + modification and the indicated length is the size of the string after + encoding. + + This document expands the definition of string literals by permitting + them to begin other than on a byte boundary. An "N-bit prefix string + literal" begins mid-byte, with the first (8-N) bits allocated to a + previous field. The string uses one bit for the Huffman flag, + followed by the length of the encoded string as a (N-1)-bit prefix + integer. The prefix size, N, can have a value between 2 and 8, + inclusive. The remainder of the string literal is unmodified. + + A string literal without a prefix length noted is an 8-bit prefix + string literal and follows the definitions in [RFC7541] without + modification. + +4.2. Encoder and Decoder Streams + + QPACK defines two unidirectional stream types: + + * An encoder stream is a unidirectional stream of type 0x02. It + carries an unframed sequence of encoder instructions from encoder + to decoder. + + * A decoder stream is a unidirectional stream of type 0x03. It + carries an unframed sequence of decoder instructions from decoder + to encoder. + + HTTP/3 endpoints contain a QPACK encoder and decoder. Each endpoint + MUST initiate, at most, one encoder stream and, at most, one decoder + stream. Receipt of a second instance of either stream type MUST be + treated as a connection error of type H3_STREAM_CREATION_ERROR. + + The sender MUST NOT close either of these streams, and the receiver + MUST NOT request that the sender close either of these streams. + Closure of either unidirectional stream type MUST be treated as a + connection error of type H3_CLOSED_CRITICAL_STREAM. + + An endpoint MAY avoid creating an encoder stream if it will not be + used (for example, if its encoder does not wish to use the dynamic + table or if the maximum size of the dynamic table permitted by the + peer is zero). + + An endpoint MAY avoid creating a decoder stream if its decoder sets + the maximum capacity of the dynamic table to zero. + + An endpoint MUST allow its peer to create an encoder stream and a + decoder stream even if the connection's settings prevent their use. + +4.3. Encoder Instructions + + An encoder sends encoder instructions on the encoder stream to set + the capacity of the dynamic table and add dynamic table entries. + Instructions adding table entries can use existing entries to avoid + transmitting redundant information. The name can be transmitted as a + reference to an existing entry in the static or the dynamic table or + as a string literal. For entries that already exist in the dynamic + table, the full entry can also be used by reference, creating a + duplicate entry. + +4.3.1. Set Dynamic Table Capacity + + An encoder informs the decoder of a change to the dynamic table + capacity using an instruction that starts with the '001' 3-bit + pattern. This is followed by the new dynamic table capacity + represented as an integer with a 5-bit prefix; see Section 4.1.1. + + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 1 | Capacity (5+) | + +---+---+---+-------------------+ + + Figure 5: Set Dynamic Table Capacity + + The new capacity MUST be lower than or equal to the limit described + in Section 3.2.3. In HTTP/3, this limit is the value of the + SETTINGS_QPACK_MAX_TABLE_CAPACITY parameter (Section 5) received from + the decoder. The decoder MUST treat a new dynamic table capacity + value that exceeds this limit as a connection error of type + QPACK_ENCODER_STREAM_ERROR. + + Reducing the dynamic table capacity can cause entries to be evicted; + see Section 3.2.2. This MUST NOT cause the eviction of entries that + are not evictable; see Section 2.1.1. Changing the capacity of the + dynamic table is not acknowledged as this instruction does not insert + an entry. + +4.3.2. Insert with Name Reference + + An encoder adds an entry to the dynamic table where the field name + matches the field name of an entry stored in the static or the + dynamic table using an instruction that starts with the '1' 1-bit + pattern. The second ('T') bit indicates whether the reference is to + the static or dynamic table. The 6-bit prefix integer + (Section 4.1.1) that follows is used to locate the table entry for + the field name. When T=1, the number represents the static table + index; when T=0, the number is the relative index of the entry in the + dynamic table. + + The field name reference is followed by the field value represented + as a string literal; see Section 4.1.2. + + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 1 | T | Name Index (6+) | + +---+---+-----------------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length bytes) | + +-------------------------------+ + + Figure 6: Insert Field Line -- Indexed Name + +4.3.3. Insert with Literal Name + + An encoder adds an entry to the dynamic table where both the field + name and the field value are represented as string literals using an + instruction that starts with the '01' 2-bit pattern. + + This is followed by the name represented as a 6-bit prefix string + literal and the value represented as an 8-bit prefix string literal; + see Section 4.1.2. + + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 1 | H | Name Length (5+) | + +---+---+---+-------------------+ + | Name String (Length bytes) | + +---+---------------------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length bytes) | + +-------------------------------+ + + Figure 7: Insert Field Line -- New Name + +4.3.4. Duplicate + + An encoder duplicates an existing entry in the dynamic table using an + instruction that starts with the '000' 3-bit pattern. This is + followed by the relative index of the existing entry represented as + an integer with a 5-bit prefix; see Section 4.1.1. + + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 0 | Index (5+) | + +---+---+---+-------------------+ + + Figure 8: Duplicate + + The existing entry is reinserted into the dynamic table without + resending either the name or the value. This is useful to avoid + adding a reference to an older entry, which might block inserting new + entries. + +4.4. Decoder Instructions + + A decoder sends decoder instructions on the decoder stream to inform + the encoder about the processing of field sections and table updates + to ensure consistency of the dynamic table. + +4.4.1. Section Acknowledgment + + After processing an encoded field section whose declared Required + Insert Count is not zero, the decoder emits a Section Acknowledgment + instruction. The instruction starts with the '1' 1-bit pattern, + followed by the field section's associated stream ID encoded as a + 7-bit prefix integer; see Section 4.1.1. + + This instruction is used as described in Sections 2.1.4 and 2.2.2. + + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 1 | Stream ID (7+) | + +---+---------------------------+ + + Figure 9: Section Acknowledgment + + If an encoder receives a Section Acknowledgment instruction referring + to a stream on which every encoded field section with a non-zero + Required Insert Count has already been acknowledged, this MUST be + treated as a connection error of type QPACK_DECODER_STREAM_ERROR. + + The Section Acknowledgment instruction might increase the Known + Received Count; see Section 2.1.4. + +4.4.2. Stream Cancellation + + When a stream is reset or reading is abandoned, the decoder emits a + Stream Cancellation instruction. The instruction starts with the + '01' 2-bit pattern, followed by the stream ID of the affected stream + encoded as a 6-bit prefix integer. + + This instruction is used as described in Section 2.2.2. + + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 1 | Stream ID (6+) | + +---+---+-----------------------+ + + Figure 10: Stream Cancellation + +4.4.3. Insert Count Increment + + The Insert Count Increment instruction starts with the '00' 2-bit + pattern, followed by the Increment encoded as a 6-bit prefix integer. + This instruction increases the Known Received Count (Section 2.1.4) + by the value of the Increment parameter. The decoder should send an + Increment value that increases the Known Received Count to the total + number of dynamic table insertions and duplications processed so far. + + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | Increment (6+) | + +---+---+-----------------------+ + + Figure 11: Insert Count Increment + + An encoder that receives an Increment field equal to zero, or one + that increases the Known Received Count beyond what the encoder has + sent, MUST treat this as a connection error of type + QPACK_DECODER_STREAM_ERROR. + +4.5. Field Line Representations + + An encoded field section consists of a prefix and a possibly empty + sequence of representations defined in this section. Each + representation corresponds to a single field line. These + representations reference the static table or the dynamic table in a + particular state, but they do not modify that state. + + Encoded field sections are carried in frames on streams defined by + the enclosing protocol. + +4.5.1. Encoded Field Section Prefix + + Each encoded field section is prefixed with two integers. The + Required Insert Count is encoded as an integer with an 8-bit prefix + using the encoding described in Section 4.5.1.1. The Base is encoded + as a Sign bit ('S') and a Delta Base value with a 7-bit prefix; see + Section 4.5.1.2. + + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | Required Insert Count (8+) | + +---+---------------------------+ + | S | Delta Base (7+) | + +---+---------------------------+ + | Encoded Field Lines ... + +-------------------------------+ + + Figure 12: Encoded Field Section + +4.5.1.1. Required Insert Count + + Required Insert Count identifies the state of the dynamic table + needed to process the encoded field section. Blocking decoders use + the Required Insert Count to determine when it is safe to process the + rest of the field section. + + The encoder transforms the Required Insert Count as follows before + encoding: + + if ReqInsertCount == 0: + EncInsertCount = 0 + else: + EncInsertCount = (ReqInsertCount mod (2 * MaxEntries)) + 1 + + Here MaxEntries is the maximum number of entries that the dynamic + table can have. The smallest entry has empty name and value strings + and has the size of 32. Hence, MaxEntries is calculated as: + + MaxEntries = floor( MaxTableCapacity / 32 ) + + MaxTableCapacity is the maximum capacity of the dynamic table as + specified by the decoder; see Section 3.2.3. + + This encoding limits the length of the prefix on long-lived + connections. + + The decoder can reconstruct the Required Insert Count using an + algorithm such as the following. If the decoder encounters a value + of EncodedInsertCount that could not have been produced by a + conformant encoder, it MUST treat this as a connection error of type + QPACK_DECOMPRESSION_FAILED. + + TotalNumberOfInserts is the total number of inserts into the + decoder's dynamic table. + + FullRange = 2 * MaxEntries + if EncodedInsertCount == 0: + ReqInsertCount = 0 + else: + if EncodedInsertCount > FullRange: + Error + MaxValue = TotalNumberOfInserts + MaxEntries + + # MaxWrapped is the largest possible value of + # ReqInsertCount that is 0 mod 2 * MaxEntries + MaxWrapped = floor(MaxValue / FullRange) * FullRange + ReqInsertCount = MaxWrapped + EncodedInsertCount - 1 + + # If ReqInsertCount exceeds MaxValue, the Encoder's value + # must have wrapped one fewer time + if ReqInsertCount > MaxValue: + if ReqInsertCount <= FullRange: + Error + ReqInsertCount -= FullRange + + # Value of 0 must be encoded as 0. + if ReqInsertCount == 0: + Error + + For example, if the dynamic table is 100 bytes, then the Required + Insert Count will be encoded modulo 6. If a decoder has received 10 + inserts, then an encoded value of 4 indicates that the Required + Insert Count is 9 for the field section. + +4.5.1.2. Base + + The Base is used to resolve references in the dynamic table as + described in Section 3.2.5. + + To save space, the Base is encoded relative to the Required Insert + Count using a one-bit Sign ('S' in Figure 12) and the Delta Base + value. A Sign bit of 0 indicates that the Base is greater than or + equal to the value of the Required Insert Count; the decoder adds the + value of Delta Base to the Required Insert Count to determine the + value of the Base. A Sign bit of 1 indicates that the Base is less + than the Required Insert Count; the decoder subtracts the value of + Delta Base from the Required Insert Count and also subtracts one to + determine the value of the Base. That is: + + if Sign == 0: + Base = ReqInsertCount + DeltaBase + else: + Base = ReqInsertCount - DeltaBase - 1 + + A single-pass encoder determines the Base before encoding a field + section. If the encoder inserted entries in the dynamic table while + encoding the field section and is referencing them, Required Insert + Count will be greater than the Base, so the encoded difference is + negative and the Sign bit is set to 1. If the field section was not + encoded using representations that reference the most recent entry in + the table and did not insert any new entries, the Base will be + greater than the Required Insert Count, so the encoded difference + will be positive and the Sign bit is set to 0. + + The value of Base MUST NOT be negative. Though the protocol might + operate correctly with a negative Base using post-Base indexing, it + is unnecessary and inefficient. An endpoint MUST treat a field block + with a Sign bit of 1 as invalid if the value of Required Insert Count + is less than or equal to the value of Delta Base. + + An encoder that produces table updates before encoding a field + section might set Base to the value of Required Insert Count. In + such a case, both the Sign bit and the Delta Base will be set to + zero. + + A field section that was encoded without references to the dynamic + table can use any value for the Base; setting Delta Base to zero is + one of the most efficient encodings. + + For example, with a Required Insert Count of 9, a decoder receives a + Sign bit of 1 and a Delta Base of 2. This sets the Base to 6 and + enables post-Base indexing for three entries. In this example, a + relative index of 1 refers to the fifth entry that was added to the + table; a post-Base index of 1 refers to the eighth entry. + +4.5.2. Indexed Field Line + + An indexed field line representation identifies an entry in the + static table or an entry in the dynamic table with an absolute index + less than the value of the Base. + + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 1 | T | Index (6+) | + +---+---+-----------------------+ + + Figure 13: Indexed Field Line + + This representation starts with the '1' 1-bit pattern, followed by + the 'T' bit, indicating whether the reference is into the static or + dynamic table. The 6-bit prefix integer (Section 4.1.1) that follows + is used to locate the table entry for the field line. When T=1, the + number represents the static table index; when T=0, the number is the + relative index of the entry in the dynamic table. + +4.5.3. Indexed Field Line with Post-Base Index + + An indexed field line with post-Base index representation identifies + an entry in the dynamic table with an absolute index greater than or + equal to the value of the Base. + + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 0 | 1 | Index (4+) | + +---+---+---+---+---------------+ + + Figure 14: Indexed Field Line with Post-Base Index + + This representation starts with the '0001' 4-bit pattern. This is + followed by the post-Base index (Section 3.2.6) of the matching field + line, represented as an integer with a 4-bit prefix; see + Section 4.1.1. + +4.5.4. Literal Field Line with Name Reference + + A literal field line with name reference representation encodes a + field line where the field name matches the field name of an entry in + the static table or the field name of an entry in the dynamic table + with an absolute index less than the value of the Base. + + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 1 | N | T |Name Index (4+)| + +---+---+---+---+---------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length bytes) | + +-------------------------------+ + + Figure 15: Literal Field Line with Name Reference + + This representation starts with the '01' 2-bit pattern. The + following bit, 'N', indicates whether an intermediary is permitted to + add this field line to the dynamic table on subsequent hops. When + the 'N' bit is set, the encoded field line MUST always be encoded + with a literal representation. In particular, when a peer sends a + field line that it received represented as a literal field line with + the 'N' bit set, it MUST use a literal representation to forward this + field line. This bit is intended for protecting field values that + are not to be put at risk by compressing them; see Section 7.1 for + more details. + + The fourth ('T') bit indicates whether the reference is to the static + or dynamic table. The 4-bit prefix integer (Section 4.1.1) that + follows is used to locate the table entry for the field name. When + T=1, the number represents the static table index; when T=0, the + number is the relative index of the entry in the dynamic table. + + Only the field name is taken from the dynamic table entry; the field + value is encoded as an 8-bit prefix string literal; see + Section 4.1.2. + +4.5.5. Literal Field Line with Post-Base Name Reference + + A literal field line with post-Base name reference representation + encodes a field line where the field name matches the field name of a + dynamic table entry with an absolute index greater than or equal to + the value of the Base. + + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 0 | 0 | N |NameIdx(3+)| + +---+---+---+---+---+-----------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length bytes) | + +-------------------------------+ + + Figure 16: Literal Field Line with Post-Base Name Reference + + This representation starts with the '0000' 4-bit pattern. The fifth + bit is the 'N' bit as described in Section 4.5.4. This is followed + by a post-Base index of the dynamic table entry (Section 3.2.6) + encoded as an integer with a 3-bit prefix; see Section 4.1.1. + + Only the field name is taken from the dynamic table entry; the field + value is encoded as an 8-bit prefix string literal; see + Section 4.1.2. + +4.5.6. Literal Field Line with Literal Name + + The literal field line with literal name representation encodes a + field name and a field value as string literals. + + 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) | + +-------------------------------+ + + Figure 17: Literal Field Line with Literal Name + + This representation starts with the '001' 3-bit pattern. The fourth + bit is the 'N' bit as described in Section 4.5.4. The name follows, + represented as a 4-bit prefix string literal, then the value, + represented as an 8-bit prefix string literal; see Section 4.1.2. + +5. Configuration + + QPACK defines two settings for the HTTP/3 SETTINGS frame: + + SETTINGS_QPACK_MAX_TABLE_CAPACITY (0x01): The default value is zero. + See Section 3.2 for usage. This is the equivalent of the + SETTINGS_HEADER_TABLE_SIZE from HTTP/2. + + SETTINGS_QPACK_BLOCKED_STREAMS (0x07): The default value is zero. + See Section 2.1.2. + +6. Error Handling + + The following error codes are defined for HTTP/3 to indicate failures + of QPACK that prevent the stream or connection from continuing: + + QPACK_DECOMPRESSION_FAILED (0x0200): The decoder failed to interpret + an encoded field section and is not able to continue decoding that + field section. + + QPACK_ENCODER_STREAM_ERROR (0x0201): The decoder failed to interpret + an encoder instruction received on the encoder stream. + + QPACK_DECODER_STREAM_ERROR (0x0202): The encoder failed to interpret + a decoder instruction received on the decoder stream. + +7. Security Considerations + + This section describes potential areas of security concern with + QPACK: + + * Use of compression as a length-based oracle for verifying guesses + about secrets that are compressed into a shared compression + context. + + * Denial of service resulting from exhausting processing or memory + capacity at a decoder. + +7.1. Probing Dynamic Table State + + QPACK reduces the encoded size of field sections by exploiting the + redundancy inherent in protocols like HTTP. The ultimate goal of + this is to reduce the amount of data that is required to send HTTP + requests or responses. + + The compression context used to encode header and trailer fields can + be probed by an attacker who can both define fields to be encoded and + transmitted and observe the length of those fields once they are + encoded. When an attacker can do both, they can adaptively modify + requests in order to confirm guesses about the dynamic table state. + If a guess is compressed into a shorter length, the attacker can + observe the encoded length and infer that the guess was correct. + + This is possible even over the Transport Layer Security Protocol + ([TLS]) and the QUIC Transport Protocol ([QUIC-TRANSPORT]), because + while TLS and QUIC provide confidentiality protection for content, + they only provide a limited amount of protection for the length of + that content. + + | Note: Padding schemes only provide limited protection against + | an attacker with these capabilities, potentially only forcing + | an increased number of guesses to learn the length associated + | with a given guess. Padding schemes also work directly against + | compression by increasing the number of bits that are + | transmitted. + + Attacks like CRIME ([CRIME]) demonstrated the existence of these + general attacker capabilities. The specific attack exploited the + fact that DEFLATE ([RFC1951]) removes redundancy based on prefix + matching. This permitted the attacker to confirm guesses a character + at a time, reducing an exponential-time attack into a linear-time + attack. + +7.1.1. Applicability to QPACK and HTTP + + QPACK mitigates, but does not completely prevent, attacks modeled on + CRIME ([CRIME]) by forcing a guess to match an entire field line + rather than individual characters. An attacker can only learn + whether a guess is correct or not, so the attacker is reduced to a + brute-force guess for the field values associated with a given field + name. + + Therefore, the viability of recovering specific field values depends + on the entropy of values. As a result, values with high entropy are + unlikely to be recovered successfully. However, values with low + entropy remain vulnerable. + + Attacks of this nature are possible any time that two mutually + distrustful entities control requests or responses that are placed + onto a single HTTP/3 connection. If the shared QPACK compressor + permits one entity to add entries to the dynamic table, and the other + to refer to those entries while encoding chosen field lines, then the + attacker (the second entity) can learn the state of the table by + observing the length of the encoded output. + + For example, requests or responses from mutually distrustful entities + can occur when an intermediary either: + + * sends requests from multiple clients on a single connection toward + an origin server, or + + * takes responses from multiple origin servers and places them on a + shared connection toward a client. + + Web browsers also need to assume that requests made on the same + connection by different web origins ([RFC6454]) are made by mutually + distrustful entities. Other scenarios involving mutually distrustful + entities are also possible. + +7.1.2. Mitigation + + Users of HTTP that require confidentiality for header or trailer + fields can use values with entropy sufficient to make guessing + infeasible. However, this is impractical as a general solution + because it forces all users of HTTP to take steps to mitigate + attacks. It would impose new constraints on how HTTP is used. + + Rather than impose constraints on users of HTTP, an implementation of + QPACK can instead constrain how compression is applied in order to + limit the potential for dynamic table probing. + + An ideal solution segregates access to the dynamic table based on the + entity that is constructing the message. Field values that are added + to the table are attributed to an entity, and only the entity that + created a particular value can extract that value. + + To improve compression performance of this option, certain entries + might be tagged as being public. For example, a web browser might + make the values of the Accept-Encoding header field available in all + requests. + + An encoder without good knowledge of the provenance of field values + might instead introduce a penalty for many field lines with the same + field name and different values. This penalty could cause a large + number of attempts to guess a field value to result in the field not + being compared to the dynamic table entries in future messages, + effectively preventing further guesses. + + This response might be made inversely proportional to the length of + the field value. Disabling access to the dynamic table for a given + field name might occur for shorter values more quickly or with higher + probability than for longer values. + + This mitigation is most effective between two endpoints. If messages + are re-encoded by an intermediary without knowledge of which entity + constructed a given message, the intermediary could inadvertently + merge compression contexts that the original encoder had specifically + kept separate. + + | Note: Simply removing entries corresponding to the field from + | the dynamic table can be ineffectual if the attacker has a + | reliable way of causing values to be reinstalled. For example, + | a request to load an image in a web browser typically includes + | the Cookie header field (a potentially highly valued target for + | this sort of attack), and websites can easily force an image to + | be loaded, thereby refreshing the entry in the dynamic table. + +7.1.3. Never-Indexed Literals + + Implementations can also choose to protect sensitive fields by not + compressing them and instead encoding their value as literals. + + Refusing to insert a field line into the dynamic table is only + effective if doing so is avoided on all hops. The never-indexed + literal bit (see Section 4.5.4) can be used to signal to + intermediaries that a particular value was intentionally sent as a + literal. + + An intermediary MUST NOT re-encode a value that uses a literal + representation with the 'N' bit set with another representation that + would index it. If QPACK is used for re-encoding, a literal + representation with the 'N' bit set MUST be used. If HPACK is used + for re-encoding, the never-indexed literal representation (see + Section 6.2.3 of [RFC7541]) MUST be used. + + The choice to mark that a field value should never be indexed depends + on several factors. Since QPACK does not protect against guessing an + entire field value, short or low-entropy values are more readily + recovered by an adversary. Therefore, an encoder might choose not to + index values with low entropy. + + An encoder might also choose not to index values for fields that are + considered to be highly valuable or sensitive to recovery, such as + the Cookie or Authorization header fields. + + On the contrary, an encoder might prefer indexing values for fields + that have little or no value if they were exposed. For instance, a + User-Agent header field does not commonly vary between requests and + is sent to any server. In that case, confirmation that a particular + User-Agent value has been used provides little value. + + Note that these criteria for deciding to use a never-indexed literal + representation will evolve over time as new attacks are discovered. + +7.2. Static Huffman Encoding + + There is no currently known attack against a static Huffman encoding. + A study has shown that using a static Huffman encoding table created + an information leakage; however, this same study concluded that an + attacker could not take advantage of this information leakage to + recover any meaningful amount of information (see [PETAL]). + +7.3. Memory Consumption + + An attacker can try to cause an endpoint to exhaust its memory. + QPACK is designed to limit both the peak and stable amounts of memory + allocated by an endpoint. + + QPACK uses the definition of the maximum size of the dynamic table + and the maximum number of blocking streams to limit the amount of + memory the encoder can cause the decoder to consume. In HTTP/3, + these values are controlled by the decoder through the settings + parameters SETTINGS_QPACK_MAX_TABLE_CAPACITY and + SETTINGS_QPACK_BLOCKED_STREAMS, respectively (see Section 3.2.3 and + Section 2.1.2). The limit on the size of the dynamic table takes + into account the size of the data stored in the dynamic table, plus a + small allowance for overhead. The limit on the number of blocked + streams is only a proxy for the maximum amount of memory required by + the decoder. The actual maximum amount of memory will depend on how + much memory the decoder uses to track each blocked stream. + + A decoder can limit the amount of state memory used for the dynamic + table by setting an appropriate value for the maximum size of the + dynamic table. In HTTP/3, this is realized by setting an appropriate + value for the SETTINGS_QPACK_MAX_TABLE_CAPACITY parameter. An + encoder can limit the amount of state memory it uses by choosing a + smaller dynamic table size than the decoder allows and signaling this + to the decoder (see Section 4.3.1). + + A decoder can limit the amount of state memory used for blocked + streams by setting an appropriate value for the maximum number of + blocked streams. In HTTP/3, this is realized by setting an + appropriate value for the SETTINGS_QPACK_BLOCKED_STREAMS parameter. + Streams that risk becoming blocked consume no additional state memory + on the encoder. + + An encoder allocates memory to track all dynamic table references in + unacknowledged field sections. An implementation can directly limit + the amount of state memory by only using as many references to the + dynamic table as it wishes to track; no signaling to the decoder is + required. However, limiting references to the dynamic table will + reduce compression effectiveness. + + The amount of temporary memory consumed by an encoder or decoder can + be limited by processing field lines sequentially. A decoder + implementation does not need to retain a complete list of field lines + while decoding a field section. An encoder implementation does not + need to retain a complete list of field lines while encoding a field + section if it is using a single-pass algorithm. Note that it might + be necessary for an application to retain a complete list of field + lines for other reasons; even if QPACK does not force this to occur, + application constraints might make this necessary. + + While the negotiated limit on the dynamic table size accounts for + much of the memory that can be consumed by a QPACK implementation, + data that cannot be immediately sent due to flow control is not + affected by this limit. Implementations should limit the size of + unsent data, especially on the decoder stream where flexibility to + choose what to send is limited. Possible responses to an excess of + unsent data might include limiting the ability of the peer to open + new streams, reading only from the encoder stream, or closing the + connection. + +7.4. Implementation Limits + + An implementation of QPACK needs to ensure that large values for + integers, long encoding for integers, or long string literals do not + create security weaknesses. + + An implementation has to set a limit for the values it accepts for + integers, as well as for the encoded length; see Section 4.1.1. In + the same way, it has to set a limit to the length it accepts for + string literals; see Section 4.1.2. These limits SHOULD be large + enough to process the largest individual field the HTTP + implementation can be configured to accept. + + If an implementation encounters a value larger than it is able to + decode, this MUST be treated as a stream error of type + QPACK_DECOMPRESSION_FAILED if on a request stream or a connection + error of the appropriate type if on the encoder or decoder stream. + +8. IANA Considerations + + This document makes multiple registrations in the registries defined + by [HTTP/3]. The allocations created by this document are all + assigned permanent status and list a change controller of the IETF + and a contact of the HTTP working group (ietf-http-wg@w3.org). + +8.1. Settings Registration + + This document specifies two settings. The entries in the following + table are registered in the "HTTP/3 Settings" registry established in + [HTTP/3]. + + +==========================+======+===============+=========+ + | Setting Name | Code | Specification | Default | + +==========================+======+===============+=========+ + | QPACK_MAX_TABLE_CAPACITY | 0x01 | Section 5 | 0 | + +--------------------------+------+---------------+---------+ + | QPACK_BLOCKED_STREAMS | 0x07 | Section 5 | 0 | + +--------------------------+------+---------------+---------+ + + Table 1: Additions to the HTTP/3 Settings Registry + + For formatting reasons, the setting names here are abbreviated by + removing the 'SETTINGS_' prefix. + +8.2. Stream Type Registration + + This document specifies two stream types. The entries in the + following table are registered in the "HTTP/3 Stream Types" registry + established in [HTTP/3]. + + +======================+======+===============+========+ + | Stream Type | Code | Specification | Sender | + +======================+======+===============+========+ + | QPACK Encoder Stream | 0x02 | Section 4.2 | Both | + +----------------------+------+---------------+--------+ + | QPACK Decoder Stream | 0x03 | Section 4.2 | Both | + +----------------------+------+---------------+--------+ + + Table 2: Additions to the HTTP/3 Stream Types Registry + +8.3. Error Code Registration + + This document specifies three error codes. The entries in the + following table are registered in the "HTTP/3 Error Codes" registry + established in [HTTP/3]. + + +============================+========+=============+===============+ + | Name | Code |Description | Specification | + +============================+========+=============+===============+ + | QPACK_DECOMPRESSION_FAILED | 0x0200 |Decoding of a| Section 6 | + | | |field section| | + | | |failed | | + +----------------------------+--------+-------------+---------------+ + | QPACK_ENCODER_STREAM_ERROR | 0x0201 |Error on the | Section 6 | + | | |encoder | | + | | |stream | | + +----------------------------+--------+-------------+---------------+ + | QPACK_DECODER_STREAM_ERROR | 0x0202 |Error on the | Section 6 | + | | |decoder | | + | | |stream | | + +----------------------------+--------+-------------+---------------+ + + Table 3: Additions to the HTTP/3 Error Codes Registry + +9. References + +9.1. Normative References + + [HTTP] Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, + Ed., "HTTP Semantics", STD 97, RFC 9110, + DOI 10.17487/RFC9110, June 2022, + . + + [HTTP/3] Bishop, M., Ed., "HTTP/3", RFC 9114, DOI 10.17487/RFC9114, + June 2022, . + + [QUIC-TRANSPORT] + Iyengar, J., Ed. and M. Thomson, Ed., "QUIC: A UDP-Based + Multiplexed and Secure Transport", RFC 9000, + DOI 10.17487/RFC9000, May 2021, + . + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + [RFC2360] Scott, G., "Guide for Internet Standards Writers", BCP 22, + RFC 2360, DOI 10.17487/RFC2360, June 1998, + . + + [RFC7541] Peon, R. and H. Ruellan, "HPACK: Header Compression for + HTTP/2", RFC 7541, DOI 10.17487/RFC7541, May 2015, + . + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + +9.2. Informative References + + [CRIME] Wikipedia, "CRIME", May 2015, . + + [HTTP/2] Thomson, M., Ed. and C. Benfield, Ed., "HTTP/2", RFC 9113, + DOI 10.17487/RFC9113, June 2022, + . + + [PETAL] Tan, J. and J. Nahata, "PETAL: Preset Encoding + Table Information Leakage", April 2013, + . + + [RFC1951] Deutsch, P., "DEFLATE Compressed Data Format Specification + version 1.3", RFC 1951, DOI 10.17487/RFC1951, May 1996, + . + + [RFC6454] Barth, A., "The Web Origin Concept", RFC 6454, + DOI 10.17487/RFC6454, December 2011, + . + + [TLS] Rescorla, E., "The Transport Layer Security (TLS) Protocol + Version 1.3", RFC 8446, DOI 10.17487/RFC8446, August 2018, + . + +Appendix A. Static Table + + This table was generated by analyzing actual Internet traffic in 2018 + and including the most common header fields, after filtering out some + unsupported and non-standard values. Due to this methodology, some + of the entries may be inconsistent or appear multiple times with + similar but not identical values. The order of the entries is + optimized to encode the most common header fields with the smallest + number of bytes. + + +=======+==================================+=======================+ + | Index | Name | Value | + +=======+==================================+=======================+ + | 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 | + +-------+----------------------------------+-----------------------+ + + Table 4: Static Table + + Any line breaks that appear within field names or values are due to + formatting. + +Appendix B. Encoding and Decoding Examples + + The following examples represent a series of exchanges between an + encoder and a decoder. The exchanges are designed to exercise most + QPACK instructions and highlight potentially common patterns and + their impact on dynamic table state. The encoder sends three encoded + field sections containing one field line each, as well as two + speculative inserts that are not referenced. + + The state of the encoder's dynamic table is shown, along with its + current size. Each entry is shown with the Absolute Index of the + entry (Abs), the current number of outstanding encoded field sections + with references to that entry (Ref), along with the name and value. + Entries above the 'acknowledged' line have been acknowledged by the + decoder. + +B.1. Literal Field Line with Name Reference + + The encoder sends an encoded field section containing a literal + representation of a field with a static name reference. + + Data | Interpretation + | Encoder's Dynamic Table + + Stream: 0 + 0000 | Required Insert Count = 0, Base = 0 + 510b 2f69 6e64 6578 | Literal Field Line with Name Reference + 2e68 746d 6c | Static Table, Index=1 + | (:path=/index.html) + + Abs Ref Name Value + ^-- acknowledged --^ + Size=0 + +B.2. Dynamic Table + + The encoder sets the dynamic table capacity, inserts a header with a + dynamic name reference, then sends a potentially blocking, encoded + field section referencing this new entry. The decoder acknowledges + processing the encoded field section, which implicitly acknowledges + all dynamic table insertions up to the Required Insert Count. + + Stream: Encoder + 3fbd01 | Set Dynamic Table Capacity=220 + c00f 7777 772e 6578 | Insert With Name Reference + 616d 706c 652e 636f | Static Table, Index=0 + 6d | (:authority=www.example.com) + c10c 2f73 616d 706c | Insert With Name Reference + 652f 7061 7468 | Static Table, Index=1 + | (:path=/sample/path) + + Abs Ref Name Value + ^-- acknowledged --^ + 0 0 :authority www.example.com + 1 0 :path /sample/path + Size=106 + + Stream: 4 + 0381 | Required Insert Count = 2, Base = 0 + 10 | Indexed Field Line With Post-Base Index + | Absolute Index = Base(0) + Index(0) = 0 + | (:authority=www.example.com) + 11 | Indexed Field Line With Post-Base Index + | Absolute Index = Base(0) + Index(1) = 1 + | (:path=/sample/path) + + Abs Ref Name Value + ^-- acknowledged --^ + 0 1 :authority www.example.com + 1 1 :path /sample/path + Size=106 + + Stream: Decoder + 84 | Section Acknowledgment (stream=4) + + Abs Ref Name Value + 0 0 :authority www.example.com + 1 0 :path /sample/path + ^-- acknowledged --^ + Size=106 + +B.3. Speculative Insert + + The encoder inserts a header into the dynamic table with a literal + name. The decoder acknowledges receipt of the entry. The encoder + does not send any encoded field sections. + + Stream: Encoder + 4a63 7573 746f 6d2d | Insert With Literal Name + 6b65 790c 6375 7374 | (custom-key=custom-value) + 6f6d 2d76 616c 7565 | + + Abs Ref Name Value + 0 0 :authority www.example.com + 1 0 :path /sample/path + ^-- acknowledged --^ + 2 0 custom-key custom-value + Size=160 + + Stream: Decoder + 01 | Insert Count Increment (1) + + Abs Ref Name Value + 0 0 :authority www.example.com + 1 0 :path /sample/path + 2 0 custom-key custom-value + ^-- acknowledged --^ + Size=160 + +B.4. Duplicate Instruction, Stream Cancellation + + The encoder duplicates an existing entry in the dynamic table, then + sends an encoded field section referencing the dynamic table entries + including the duplicated entry. The packet containing the encoder + stream data is delayed. Before the packet arrives, the decoder + cancels the stream and notifies the encoder that the encoded field + section was not processed. + + Stream: Encoder + 02 | Duplicate (Relative Index = 2) + | Absolute Index = + | Insert Count(3) - Index(2) - 1 = 0 + + Abs Ref Name Value + 0 0 :authority www.example.com + 1 0 :path /sample/path + 2 0 custom-key custom-value + ^-- acknowledged --^ + 3 0 :authority www.example.com + Size=217 + + Stream: 8 + 0500 | Required Insert Count = 4, Base = 4 + 80 | Indexed Field Line, Dynamic Table + | Absolute Index = Base(4) - Index(0) - 1 = 3 + | (:authority=www.example.com) + c1 | Indexed Field Line, Static Table Index = 1 + | (:path=/) + 81 | Indexed Field Line, Dynamic Table + | Absolute Index = Base(4) - Index(1) - 1 = 2 + | (custom-key=custom-value) + + Abs Ref Name Value + 0 0 :authority www.example.com + 1 0 :path /sample/path + 2 1 custom-key custom-value + ^-- acknowledged --^ + 3 1 :authority www.example.com + Size=217 + + Stream: Decoder + 48 | Stream Cancellation (Stream=8) + + Abs Ref Name Value + 0 0 :authority www.example.com + 1 0 :path /sample/path + 2 0 custom-key custom-value + ^-- acknowledged --^ + 3 0 :authority www.example.com + Size=217 + +B.5. Dynamic Table Insert, Eviction + + The encoder inserts another header into the dynamic table, which + evicts the oldest entry. The encoder does not send any encoded field + sections. + + Stream: Encoder + 810d 6375 7374 6f6d | Insert With Name Reference + 2d76 616c 7565 32 | Dynamic Table, Relative Index = 1 + | Absolute Index = + | Insert Count(4) - Index(1) - 1 = 2 + | (custom-key=custom-value2) + + Abs Ref Name Value + 1 0 :path /sample/path + 2 0 custom-key custom-value + ^-- acknowledged --^ + 3 0 :authority www.example.com + 4 0 custom-key custom-value2 + Size=215 + +Appendix C. Sample Single-Pass Encoding Algorithm + + Pseudocode for single-pass encoding, excluding handling of + duplicates, non-blocking mode, available encoder stream flow control + and reference tracking. + + # Helper functions: + # ==== + # Encode an integer with the specified prefix and length + encodeInteger(buffer, prefix, value, prefixLength) + + # Encode a dynamic table insert instruction with optional static + # or dynamic name index (but not both) + encodeInsert(buffer, staticNameIndex, dynamicNameIndex, fieldLine) + + # Encode a static index reference + encodeStaticIndexReference(buffer, staticIndex) + + # Encode a dynamic index reference relative to Base + encodeDynamicIndexReference(buffer, dynamicIndex, base) + + # Encode a literal with an optional static name index + encodeLiteral(buffer, staticNameIndex, fieldLine) + + # Encode a literal with a dynamic name index relative to Base + encodeDynamicLiteral(buffer, dynamicNameIndex, base, fieldLine) + + # Encoding Algorithm + # ==== + base = dynamicTable.getInsertCount() + requiredInsertCount = 0 + for line in fieldLines: + staticIndex = staticTable.findIndex(line) + if staticIndex is not None: + encodeStaticIndexReference(streamBuffer, staticIndex) + continue + + dynamicIndex = dynamicTable.findIndex(line) + if dynamicIndex is None: + # No matching entry. Either insert+index or encode literal + staticNameIndex = staticTable.findName(line.name) + if staticNameIndex is None: + dynamicNameIndex = dynamicTable.findName(line.name) + + if shouldIndex(line) and dynamicTable.canIndex(line): + encodeInsert(encoderBuffer, staticNameIndex, + dynamicNameIndex, line) + dynamicIndex = dynamicTable.add(line) + + if dynamicIndex is None: + # Could not index it, literal + if dynamicNameIndex is not None: + # Encode literal with dynamic name, possibly above Base + encodeDynamicLiteral(streamBuffer, dynamicNameIndex, + base, line) + requiredInsertCount = max(requiredInsertCount, + dynamicNameIndex) + else: + # Encodes a literal with a static name or literal name + encodeLiteral(streamBuffer, staticNameIndex, line) + else: + # Dynamic index reference + assert(dynamicIndex is not None) + requiredInsertCount = max(requiredInsertCount, dynamicIndex) + # Encode dynamicIndex, possibly above Base + encodeDynamicIndexReference(streamBuffer, dynamicIndex, base) + + # encode the prefix + if requiredInsertCount == 0: + encodeInteger(prefixBuffer, 0x00, 0, 8) + encodeInteger(prefixBuffer, 0x00, 0, 7) + else: + wireRIC = ( + requiredInsertCount + % (2 * getMaxEntries(maxTableCapacity)) + ) + 1; + encodeInteger(prefixBuffer, 0x00, wireRIC, 8) + if base >= requiredInsertCount: + encodeInteger(prefixBuffer, 0x00, + base - requiredInsertCount, 7) + else: + encodeInteger(prefixBuffer, 0x80, + requiredInsertCount - base - 1, 7) + + return encoderBuffer, prefixBuffer + streamBuffer + +Acknowledgments + + The IETF QUIC Working Group received an enormous amount of support + from many people. + + The compression design team did substantial work exploring the + problem space and influencing the initial draft version of this + document. The contributions of design team members Roberto Peon, + Martin Thomson, and Dmitri Tikhonov are gratefully acknowledged. + + The following people also provided substantial contributions to this + document: + + * Bence Beky + * Alessandro Ghedini + * Ryan Hamilton + * Robin Marx + * Patrick McManus + * 奥 一穂 (Kazuho Oku) + * Lucas Pardue + * Biren Roy + * Ian Swett + + This document draws heavily on the text of [RFC7541]. The indirect + input of those authors is also gratefully acknowledged. + + Buck Krasic's contribution was supported by Google during his + employment there. + + A portion of Mike Bishop's contribution was supported by Microsoft + during his employment there. + +Authors' Addresses + + Charles 'Buck' Krasic + Email: krasic@acm.org + + + Mike Bishop + Akamai Technologies + Email: mbishop@evequefou.be + + + Alan Frindell (editor) + Facebook + Email: afrind@fb.com diff --git a/examples/client.rs b/examples/client.rs index f91a340..87039ce 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -1,5 +1,6 @@ use std::{path::PathBuf, sync::Arc}; +use bytes::Bytes; use futures::future; use h3::error::{ConnectionError, StreamError}; use rustls::pki_types::CertificateDer; @@ -112,7 +113,14 @@ async fn main() -> Result<(), Box> { // h3_quinn implements the trait w/ quinn to make it work with h3. let quinn_conn = h3_quinn::Connection::new(conn); - let (mut driver, mut send_request) = h3::client::new(quinn_conn).await?; + let (mut driver, mut send_request) = h3::client::builder() + .qpack_max_table_capacity(65536u64) + .max_field_section_size(262144u64) + .qpack_blocked_streams(100u64) + .enable_datagram(true) + .send_grease(true) + .build(quinn_conn) + .await?; let drive = async move { return Err::<(), ConnectionError>(future::poll_fn(|cx| driver.poll_close(cx)).await); @@ -131,7 +139,8 @@ async fn main() -> Result<(), Box> { // sending request results in a bidirectional stream, // which is also used for receiving response - let mut stream = send_request.send_request(req).await?; + let mut stream: h3::client::RequestStream, _> = + send_request.send_request(req).await?; // finish on the sending side stream.finish().await?; diff --git a/h3-quinn/Cargo.toml b/h3-quinn/Cargo.toml index e0c0187..1cb191f 100644 --- a/h3-quinn/Cargo.toml +++ b/h3-quinn/Cargo.toml @@ -15,9 +15,7 @@ license = "MIT" [dependencies] h3 = { version = "0.0.8", path = "../h3" } bytes = "1" -quinn = { git = "https://github.com/0x676e67/quinn", default-features = false, features = [ - "futures-io", -] } +quinn = { version = "0.11", default-features = false, features = ["futures-io"] } tokio-util = { version = "0.7.9" } futures-util = { version = "0.3.28", default-features = false } h3-datagram = {version = "0.0.2", path = "../h3-datagram", optional = true } diff --git a/h3/src/client/builder.rs b/h3/src/client/builder.rs index 99c4840..b72416a 100644 --- a/h3/src/client/builder.rs +++ b/h3/src/client/builder.rs @@ -170,9 +170,11 @@ impl Builder { let send_grease_frame = self.config.send_grease; let inner = ConnectionInner::new(quic, conn_state.clone(), self.config.clone()).await?; + let qpack_decoder = inner.qpack_decoder(); let send_request = SendRequest { open, conn_state, + qpack_decoder, max_field_section_size, sender_count: Arc::new(AtomicUsize::new(1)), send_grease_frame, diff --git a/h3/src/client/connection.rs b/h3/src/client/connection.rs index 82b800e..05e91c5 100644 --- a/h3/src/client/connection.rs +++ b/h3/src/client/connection.rs @@ -2,7 +2,7 @@ use std::{ marker::PhantomData, - sync::{atomic::AtomicUsize, Arc}, + sync::{atomic::AtomicUsize, Arc, Mutex}, task::{Context, Poll}, }; @@ -113,6 +113,7 @@ where { pub(super) open: T, pub(super) conn_state: Arc, + pub(super) qpack_decoder: Arc>, pub(super) max_field_section_size: u64, // maximum size for a header we receive // counts instances of SendRequest to close the connection when the last is dropped. pub(super) sender_count: Arc, @@ -220,6 +221,7 @@ where self.max_field_section_size, self.conn_state.clone(), self.send_grease_frame, + Some(self.qpack_decoder.clone()), ), }; // send the grease frame only once @@ -239,6 +241,7 @@ where Self { conn_state: self.conn_state.clone(), + qpack_decoder: self.qpack_decoder.clone(), open: self.open.clone(), max_field_section_size: self.max_field_section_size, sender_count: self.sender_count.clone(), @@ -377,6 +380,7 @@ where impl Connection where C: quic::Connection, + C::SendStream: quic::SendStreamUnframed, B: Buf, { /// Initiate a graceful shutdown, accepting `max_push` potentially in-flight server pushes @@ -395,6 +399,14 @@ where /// Maintain the connection state until it is closed #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))] pub fn poll_close(&mut self, cx: &mut Context<'_>) -> Poll { + if let Err(err) = self.inner.poll_accept_recv(cx) { + return Poll::Ready(err); + } + + if let Poll::Ready(Err(err)) = self.inner.poll_qpack_encoder(cx) { + return Poll::Ready(err); + } + while let Poll::Ready(result) = self.inner.poll_control(cx) { match result { //= https://www.rfc-editor.org/rfc/rfc9114#section-7.2.4.2 diff --git a/h3/src/client/stream.rs b/h3/src/client/stream.rs index bba0e2b..8f3b360 100644 --- a/h3/src/client/stream.rs +++ b/h3/src/client/stream.rs @@ -124,7 +124,7 @@ where //# H3_GENERAL_PROTOCOL_ERROR. let decoded = if let Frame::Headers(ref mut encoded) = frame { - match qpack::decode_stateless(encoded, self.inner.max_field_section_size) { + match self.inner.decode_header_block(encoded) { //= https://www.rfc-editor.org/rfc/rfc9114#section-4.2.2 //# An HTTP/3 implementation MAY impose a limit on the maximum size of //# the message header it will accept on an individual HTTP message. diff --git a/h3/src/connection.rs b/h3/src/connection.rs index 02b9689..b38f06c 100644 --- a/h3/src/connection.rs +++ b/h3/src/connection.rs @@ -1,7 +1,7 @@ use std::{ convert::TryFrom, marker::PhantomData, - sync::Arc, + sync::{Arc, Mutex}, task::{Context, Poll}, }; @@ -30,7 +30,7 @@ use crate::{ varint::VarInt, }, qpack, - quic::{self, RecvStream, SendStream, StreamErrorIncoming}, + quic::{self, RecvStream, SendStream, SendStreamUnframed, StreamErrorIncoming}, shared_state::{ConnectionState, SharedState}, stream::{self, AcceptRecvStream, AcceptedRecvStream, BufRecvStream, UniStreamHeader}, webtransport::SessionId, @@ -64,6 +64,7 @@ where B: Buf, { decoder_send: Option, + decoder_send_buf: BytesMut, decoder_recv: Option>, encoder_send: Option, encoder_recv: Option>, @@ -81,6 +82,10 @@ where control_send: C::SendStream, control_recv: Option>, qpack_streams: QpackStreams, + + /// QPACK decoder state for field sections that reference the dynamic table. + decoder: Arc>, + /// Buffers incoming uni/recv streams which have yet to be claimed. /// /// This is opposed to discarding them by returning in `poll_accept_recv`, which may cause them to be missed by something else polling. @@ -193,6 +198,11 @@ where let mut decoder_send = Option::take(&mut self.qpack_streams.decoder_send); let mut encoder_send = Option::take(&mut self.qpack_streams.encoder_send); + //= https://www.rfc-editor.org/rfc/rfc9204.html#section-4.2 + //# Each endpoint + //# MUST initiate, at most, one encoder stream and, at most, one decoder + //# stream. + let (control, ..) = future::join3( stream::write( &mut self.control_send, @@ -291,6 +301,7 @@ where let qpack_streams = QpackStreams { decoder_send: qpack_decoder.ok(), + decoder_send_buf: BytesMut::new(), decoder_recv: None, encoder_send: qpack_encoder.ok(), encoder_recv: None, @@ -301,12 +312,26 @@ where //# The //# sender MUST NOT close the control stream, and the receiver MUST NOT //# request that the sender close the control stream. + let decoder = Arc::new(Mutex::new( + qpack::Decoder::new( + config.settings.qpack_max_table_capacity, + config.settings.qpack_blocked_streams, + ) + .map_err(|err| { + conn.close_raw_connection_with_h3_error(InternalConnectionError::new( + Code::QPACK_DECOMPRESSION_FAILED, + format!("invalid QPACK decoder configuration: {}", err), + )) + })?, + )); + let mut conn_inner = Self { shared, conn, control_send, control_recv: None, qpack_streams, + decoder, handled_connection_error: None, pending_recv_streams: Vec::with_capacity(3), got_peer_settings: false, @@ -323,6 +348,11 @@ where Ok(conn_inner) } + #[allow(missing_docs)] + pub(crate) fn qpack_decoder(&self) -> Arc> { + self.decoder.clone() + } + /// Send GOAWAY with specified max_id, iff max_id is smaller than the previous one. #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))] pub async fn shutdown( @@ -399,13 +429,20 @@ where let _ = self.poll_connection_error(cx)?; // Get all currently pending streams - while let Poll::Ready(stream) = self - .conn - .poll_accept_recv(cx) - .map_err(|e| self.handle_connection_error(e))? - { - self.pending_recv_streams - .push(Some(AcceptRecvStream::new(stream))) + loop { + match self + .conn + //= https://www.rfc-editor.org/rfc/rfc9204.html#section-4.2 + //# An endpoint MUST allow its peer to create an encoder stream and a + //# decoder stream even if the connection's settings prevent their use. + .poll_accept_recv(cx) + .map_err(|e| self.handle_connection_error(e))? + { + Poll::Ready(stream) => self + .pending_recv_streams + .push(Some(AcceptRecvStream::new(stream))), + Poll::Pending => break, + } } for stream in self.pending_recv_streams.iter_mut().filter(|s| s.is_some()) { @@ -446,6 +483,14 @@ where } enc @ AcceptedRecvStream::Encoder(_) => { if let Some(_prev) = self.qpack_streams.encoder_recv.replace(enc) { + //= https://www.rfc-editor.org/rfc/rfc9204.html#section-4.2 + //# Receipt of a second instance of either stream type MUST be + //# treated as a connection error of type H3_STREAM_CREATION_ERROR. + + //= https://www.rfc-editor.org/rfc/rfc9204.html#section-4.2 + //# An endpoint MUST allow its peer to create an encoder stream and a + //# decoder stream even if the connection's settings prevent their use. + return Err(self.handle_connection_error(InternalConnectionError::new( Code::H3_STREAM_CREATION_ERROR, "got two encoder streams".to_string(), @@ -454,6 +499,14 @@ where } dec @ AcceptedRecvStream::Decoder(_) => { if let Some(_prev) = self.qpack_streams.decoder_recv.replace(dec) { + //= https://www.rfc-editor.org/rfc/rfc9204.html#section-4.2 + //# Receipt of a second instance of either stream type MUST be + //# treated as a connection error of type H3_STREAM_CREATION_ERROR. + + //= https://www.rfc-editor.org/rfc/rfc9204.html#section-4.2 + //# An endpoint MUST allow its peer to create an encoder stream and a + //# decoder stream even if the connection's settings prevent their use. + return Err(self.handle_connection_error(InternalConnectionError::new( Code::H3_STREAM_CREATION_ERROR, "got two decoder streams".to_string(), @@ -500,6 +553,161 @@ where Ok(()) } + #[allow(missing_docs)] + #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))] + pub fn poll_qpack_encoder(&mut self, cx: &mut Context<'_>) -> Poll> + where + C::SendStream: quic::SendStreamUnframed, + { + let _ = self.poll_connection_error(cx)?; + + match self.poll_flush_qpack_decoder(cx) { + Poll::Ready(Ok(())) => {} + Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), + Poll::Pending => return Poll::Pending, + } + + let Some(accepted) = self.qpack_streams.encoder_recv.take() else { + return Poll::Pending; + }; + + let mut encoder_recv = match accepted { + AcceptedRecvStream::Encoder(stream) => stream, + other => { + self.qpack_streams.encoder_recv = Some(other); + return Poll::Pending; + } + }; + + loop { + while encoder_recv.has_remaining() { + let before = encoder_recv.buf().remaining(); + let decoder = self.decoder.clone(); + let decode_result = match decoder.lock() { + Ok(mut decoder) => decoder.on_encoder_recv( + encoder_recv.buf_mut(), + &mut self.qpack_streams.decoder_send_buf, + ), + Err(_) => { + return Poll::Ready(Err(self.handle_connection_error( + InternalConnectionError::new( + Code::QPACK_ENCODER_STREAM_ERROR, + "QPACK decoder state lock poisoned".to_string(), + ), + ))) + } + }; + if let Err(err) = decode_result { + return Poll::Ready(Err(self.handle_connection_error( + InternalConnectionError::new( + Code::QPACK_ENCODER_STREAM_ERROR, + format!("invalid QPACK encoder stream instruction: {}", err), + ), + ))); + } + + if encoder_recv.buf().remaining() == before { + break; + } + } + + match self.poll_flush_qpack_decoder(cx) { + Poll::Ready(Ok(())) => {} + Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), + Poll::Pending => { + self.qpack_streams.encoder_recv = + Some(AcceptedRecvStream::Encoder(encoder_recv)); + return Poll::Pending; + } + } + + match encoder_recv.poll_read(cx) { + Poll::Ready(Ok(false)) => continue, + Poll::Ready(Ok(true)) => { + return Poll::Ready(Err(self.handle_connection_error( + InternalConnectionError::new( + Code::H3_CLOSED_CRITICAL_STREAM, + "QPACK encoder stream closed".to_string(), + ), + ))); + } + Poll::Ready(Err(StreamErrorIncoming::ConnectionErrorIncoming { + connection_error, + })) => return Poll::Ready(Err(self.handle_connection_error(connection_error))), + Poll::Ready(Err(StreamErrorIncoming::StreamTerminated { error_code })) => { + return Poll::Ready(Err(self.handle_connection_error( + InternalConnectionError::new( + Code::H3_CLOSED_CRITICAL_STREAM, + format!("QPACK encoder stream reset with error code {}", error_code), + ), + ))); + } + Poll::Ready(Err(StreamErrorIncoming::Unknown(error))) => { + return Poll::Ready(Err(self.handle_connection_error( + InternalConnectionError::new( + Code::H3_CLOSED_CRITICAL_STREAM, + format!("QPACK encoder stream error: {}", error), + ), + ))); + } + Poll::Pending => { + self.qpack_streams.encoder_recv = + Some(AcceptedRecvStream::Encoder(encoder_recv)); + return Poll::Pending; + } + } + } + } + + fn poll_flush_qpack_decoder( + &mut self, + cx: &mut Context<'_>, + ) -> Poll> + where + C::SendStream: quic::SendStreamUnframed, + { + if !self.qpack_streams.decoder_send_buf.has_remaining() { + return Poll::Ready(Ok(())); + } + + let Some(decoder_send) = self.qpack_streams.decoder_send.as_mut() else { + return Poll::Ready(Err(self.handle_connection_error( + InternalConnectionError::new( + Code::QPACK_DECODER_STREAM_ERROR, + "QPACK decoder stream is unavailable".to_string(), + ), + ))); + }; + + while self.qpack_streams.decoder_send_buf.has_remaining() { + match decoder_send.poll_send(cx, &mut self.qpack_streams.decoder_send_buf) { + Poll::Ready(Ok(0)) | Poll::Pending => return Poll::Pending, + Poll::Ready(Ok(_)) => {} + Poll::Ready(Err(StreamErrorIncoming::ConnectionErrorIncoming { + connection_error, + })) => return Poll::Ready(Err(self.handle_connection_error(connection_error))), + Poll::Ready(Err(StreamErrorIncoming::StreamTerminated { error_code })) => { + return Poll::Ready(Err(self.handle_connection_error( + InternalConnectionError::new( + Code::QPACK_DECODER_STREAM_ERROR, + format!("QPACK decoder stream reset with error code {}", error_code), + ), + ))); + } + Poll::Ready(Err(StreamErrorIncoming::Unknown(error))) => { + return Poll::Ready(Err(self.handle_connection_error( + InternalConnectionError::new( + Code::QPACK_DECODER_STREAM_ERROR, + format!("QPACK decoder stream error: {}", error), + ), + ))); + } + } + } + + Poll::Ready(Ok(())) + } + /// Waits for the control stream to be received and reads subsequent frames. #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))] pub fn poll_control( @@ -665,35 +873,33 @@ where T: From + Copy, VarInt: From, { - { - //= https://www.rfc-editor.org/rfc/rfc9114#section-5.2 - //# An endpoint MAY send multiple GOAWAY frames indicating different - //# identifiers, but the identifier in each frame MUST NOT be greater - //# than the identifier in any previous frame, since clients might - //# already have retried unprocessed requests on another HTTP connection. - - //= https://www.rfc-editor.org/rfc/rfc9114#section-5.2 - //# Like the server, - //# the client MAY send subsequent GOAWAY frames so long as the specified - //# push ID is no greater than any previously sent value. - if let Some(prev_id) = recv_closing.map(VarInt::from) { - if prev_id < id { - //= https://www.rfc-editor.org/rfc/rfc9114#section-5.2 - //# Receiving a GOAWAY containing a larger identifier than previously - //# received MUST be treated as a connection error of type H3_ID_ERROR. - return Err(self.handle_connection_error(InternalConnectionError::new( - Code::H3_ID_ERROR, - format!( - "received a GoAway ({}) greater than the former one ({})", - id, prev_id - ), - ))); - } + //= https://www.rfc-editor.org/rfc/rfc9114#section-5.2 + //# An endpoint MAY send multiple GOAWAY frames indicating different + //# identifiers, but the identifier in each frame MUST NOT be greater + //# than the identifier in any previous frame, since clients might + //# already have retried unprocessed requests on another HTTP connection. + + //= https://www.rfc-editor.org/rfc/rfc9114#section-5.2 + //# Like the server, + //# the client MAY send subsequent GOAWAY frames so long as the specified + //# push ID is no greater than any previously sent value. + if let Some(prev_id) = recv_closing.map(VarInt::from) { + if prev_id < id { + //= https://www.rfc-editor.org/rfc/rfc9114#section-5.2 + //# Receiving a GOAWAY containing a larger identifier than previously + //# received MUST be treated as a connection error of type H3_ID_ERROR. + return Err(self.handle_connection_error(InternalConnectionError::new( + Code::H3_ID_ERROR, + format!( + "received a GoAway ({}) greater than the former one ({})", + id, prev_id + ), + ))); } - *recv_closing = Some(id.into()); - self.set_closing(); - Ok(()) } + *recv_closing = Some(id.into()); + self.set_closing(); + Ok(()) } // start grease stream and send data @@ -812,6 +1018,7 @@ pub struct RequestStream { pub(super) trailers: Option, pub(super) conn_state: Arc, pub(super) max_field_section_size: u64, + pub(super) qpack_decoder: Option>>, send_grease_frame: bool, } @@ -822,12 +1029,14 @@ impl RequestStream { max_field_section_size: u64, conn_state: Arc, grease: bool, + qpack_decoder: Option>>, ) -> Self { Self { stream, conn_state, max_field_section_size, trailers: None, + qpack_decoder, send_grease_frame: grease, } } @@ -986,27 +1195,26 @@ where } } - let qpack::Decoded { fields, .. } = - match qpack::decode_stateless(&mut trailers, self.max_field_section_size) { - //= https://www.rfc-editor.org/rfc/rfc9114#section-4.2.2 - //# An HTTP/3 implementation MAY impose a limit on the maximum size of - //# the message header it will accept on an individual HTTP message. - Err(qpack::DecoderError::HeaderTooLong(cancel_size)) => { - return Poll::Ready(Err(StreamError::HeaderTooBig { - actual_size: cancel_size, - max_size: self.max_field_section_size, - })); - } - Ok(decoded) => decoded, - Err(_e) => { - return Poll::Ready(Err(self.handle_connection_error_on_stream( - InternalConnectionError { - code: Code::QPACK_DECOMPRESSION_FAILED, - message: "Failed to decode trailers".to_string(), - }, - ))) - } - }; + let qpack::Decoded { fields, .. } = match self.decode_header_block(&mut trailers) { + //= https://www.rfc-editor.org/rfc/rfc9114#section-4.2.2 + //# An HTTP/3 implementation MAY impose a limit on the maximum size of + //# the message header it will accept on an individual HTTP message. + Err(qpack::DecoderError::HeaderTooLong(cancel_size)) => { + return Poll::Ready(Err(StreamError::HeaderTooBig { + actual_size: cancel_size, + max_size: self.max_field_section_size, + })); + } + Ok(decoded) => decoded, + Err(_e) => { + return Poll::Ready(Err(self.handle_connection_error_on_stream( + InternalConnectionError { + code: Code::QPACK_DECOMPRESSION_FAILED, + message: "Failed to decode trailers".to_string(), + }, + ))) + } + }; Poll::Ready(Ok(Some( Header::try_from(fields) @@ -1026,6 +1234,26 @@ where pub fn stop_sending(&mut self, err_code: Code) { self.stream.stop_sending(err_code); } + + pub(super) fn decode_header_block( + &self, + encoded: &mut T, + ) -> Result { + let decoded = if let Some(decoder) = &self.qpack_decoder { + decoder + .lock() + .map_err(|_| qpack::DecoderError::UnknownPrefix(0))? + .decode_header(encoded)? + } else { + qpack::decode_stateless(encoded, self.max_field_section_size)? + }; + + if decoded.mem_size > self.max_field_section_size { + return Err(qpack::DecoderError::HeaderTooLong(decoded.mem_size)); + } + + Ok(decoded) + } } impl RequestStream @@ -1135,6 +1363,7 @@ where trailers: None, conn_state: self.conn_state.clone(), max_field_section_size: 0, + qpack_decoder: None, send_grease_frame: self.send_grease_frame, }, RequestStream { @@ -1142,6 +1371,7 @@ where trailers: self.trailers, conn_state: self.conn_state, max_field_section_size: self.max_field_section_size, + qpack_decoder: self.qpack_decoder, send_grease_frame: self.send_grease_frame, }, ) diff --git a/h3/src/qpack/decoder.rs b/h3/src/qpack/decoder.rs index 15cf68d..67e93c4 100644 --- a/h3/src/qpack/decoder.rs +++ b/h3/src/qpack/decoder.rs @@ -83,6 +83,16 @@ pub struct Decoder { } impl Decoder { + pub(crate) fn new( + max_table_capacity: u64, + max_blocked_streams: u64, + ) -> Result { + let mut table = DynamicTable::new(); + table.set_max_size(max_table_capacity.try_into()?)?; + table.set_max_blocked(max_blocked_streams.try_into()?)?; + Ok(Self { table }) + } + // Decode field lines received on Request of Push stream. // https://www.rfc-editor.org/rfc/rfc9204.html#name-field-line-representations pub fn decode_header(&self, buf: &mut T) -> Result { diff --git a/h3/src/qpack/mod.rs b/h3/src/qpack/mod.rs index 2e33747..af88d43 100644 --- a/h3/src/qpack/mod.rs +++ b/h3/src/qpack/mod.rs @@ -1,5 +1,5 @@ pub use self::{ - decoder::{decode_stateless, Decoded, DecoderError}, + decoder::{decode_stateless, Decoded, Decoder, DecoderError}, encoder::{encode_stateless, EncoderError}, field::HeaderField, }; diff --git a/h3/src/server/connection.rs b/h3/src/server/connection.rs index 3e52e9d..d5fcc85 100644 --- a/h3/src/server/connection.rs +++ b/h3/src/server/connection.rs @@ -131,11 +131,10 @@ where return Ok(None); } }; - + self.inner.send_grease_frame = false; let resolver = self.create_resolver_internal(stream); // send the grease frame only once - self.inner.send_grease_frame = false; Ok(Some(resolver)) } @@ -150,6 +149,7 @@ where send_grease_frame: self.inner.send_grease_frame, max_field_section_size: self.max_field_section_size, shared: self.inner.shared.clone(), + qpack_decoder: self.inner.qpack_decoder(), } } diff --git a/h3/src/server/request.rs b/h3/src/server/request.rs index 0b3588a..92ed54c 100644 --- a/h3/src/server/request.rs +++ b/h3/src/server/request.rs @@ -1,4 +1,7 @@ -use std::{convert::TryFrom, sync::Arc}; +use std::{ + convert::TryFrom, + sync::{Arc, Mutex}, +}; use bytes::Buf; use http::{Request, StatusCode}; @@ -19,7 +22,7 @@ use crate::{ frame::{Frame, PayloadLen}, headers::Header, }, - qpack, + qpack::{self}, quic::{self, SendStream, StreamId}, shared_state::{ConnectionState, SharedState}, }; @@ -40,6 +43,7 @@ where pub(super) send_grease_frame: bool, pub(super) max_field_section_size: u64, pub(super) shared: Arc, + pub(super) qpack_decoder: Arc>, } impl ConnectionState for RequestResolver @@ -126,7 +130,7 @@ where } }; - let decoded = match qpack::decode_stateless(&mut encoded, self.max_field_section_size) { + let decoded = match self.decode_header_block(&mut encoded) { //= https://www.rfc-editor.org/rfc/rfc9114#section-4.2.2 //# An HTTP/3 implementation MAY impose a limit on the maximum size of //# the message header it will accept on an individual HTTP message. @@ -152,6 +156,7 @@ where self.max_field_section_size, self.shared.clone(), self.send_grease_frame, + Some(self.qpack_decoder.clone()), ), }; @@ -161,6 +166,23 @@ where self.max_field_section_size, )) } + + fn decode_header_block( + &self, + encoded: &mut T, + ) -> Result { + let decoded = self + .qpack_decoder + .lock() + .map_err(|_| qpack::DecoderError::UnknownPrefix(0))? + .decode_header(encoded)?; + + if decoded.mem_size > self.max_field_section_size { + return Err(qpack::DecoderError::HeaderTooLong(decoded.mem_size)); + } + + Ok(decoded) + } } pub struct ResolvedRequest From abf8db3d5204779dbd33d7c124812999ab7a5834 Mon Sep 17 00:00:00 2001 From: gngpp Date: Tue, 9 Jun 2026 20:28:08 +0800 Subject: [PATCH 02/22] fix: poll_read data is buffered without bounds --- .gitignore | 1 + h3/src/client/connection.rs | 2 + h3/src/connection.rs | 2 +- h3/src/frame.rs | 74 ++++++++++++++++++++++++++----------- h3/src/qpack/decoder.rs | 12 ++++++ h3/src/server/request.rs | 2 +- 6 files changed, 70 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 973db0e..e06209f 100755 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ localhost-key.pem localhost.crt localhost.key localhost.pem +DATA.md \ No newline at end of file diff --git a/h3/src/client/connection.rs b/h3/src/client/connection.rs index 05e91c5..d0e64b8 100644 --- a/h3/src/client/connection.rs +++ b/h3/src/client/connection.rs @@ -295,6 +295,7 @@ where /// # -> JoinHandle>> /// # where /// # C: quic::Connection + Send + 'static, +/// # C::SendStream: quic::SendStreamUnframed, /// # C::SendStream: Send + 'static, /// # C::RecvStream: Send + 'static, /// # B: Buf + Send + 'static, @@ -320,6 +321,7 @@ where /// # -> Result<(), Box> /// # where /// # C: quic::Connection + Send + 'static, +/// # C::SendStream: quic::SendStreamUnframed, /// # C::SendStream: Send + 'static, /// # C::RecvStream: Send + 'static, /// # B: Buf + Send + 'static, diff --git a/h3/src/connection.rs b/h3/src/connection.rs index b38f06c..2ded2f5 100644 --- a/h3/src/connection.rs +++ b/h3/src/connection.rs @@ -1243,7 +1243,7 @@ where decoder .lock() .map_err(|_| qpack::DecoderError::UnknownPrefix(0))? - .decode_header(encoded)? + .decode_header_limited(encoded, self.max_field_section_size)? } else { qpack::decode_stateless(encoded, self.max_field_section_size)? }; diff --git a/h3/src/frame.rs b/h3/src/frame.rs index 00bde62..86bcabb 100644 --- a/h3/src/frame.rs +++ b/h3/src/frame.rs @@ -70,33 +70,33 @@ where ); loop { - let end = self.try_recv(cx)?; - - return match self.decoder.decode(self.stream.buf_mut())? { + match self.decoder.decode(self.stream.buf_mut())? { Some(Frame::Data(PayloadLen(len))) => { self.remaining_data = len; - Poll::Ready(Ok(Some(Frame::Data(PayloadLen(len))))) + return Poll::Ready(Ok(Some(Frame::Data(PayloadLen(len))))); } frame @ Some(Frame::WebTransportStream(_)) => { self.remaining_data = usize::MAX; - Poll::Ready(Ok(frame)) + return Poll::Ready(Ok(frame)); } - Some(frame) => Poll::Ready(Ok(Some(frame))), - None => match end { - // Received a chunk but frame is incomplete, poll until we get `Pending`. - Poll::Ready(false) => continue, - Poll::Pending => Poll::Pending, - Poll::Ready(true) => { - if self.stream.buf_mut().has_remaining() { - // Reached the end of receive stream, but there is still some data: - // The frame is incomplete. - Poll::Ready(Err(FrameStreamError::UnexpectedEnd)) - } else { - Poll::Ready(Ok(None)) - } + Some(frame) => return Poll::Ready(Ok(Some(frame))), + None => {} + } + + match self.try_recv(cx)? { + // Received a chunk but frame is incomplete, poll until we get `Pending`. + Poll::Ready(false) => continue, + Poll::Pending => return Poll::Pending, + Poll::Ready(true) => { + if self.stream.buf_mut().has_remaining() { + // Reached the end of receive stream, but there is still some data: + // The frame is incomplete. + return Poll::Ready(Err(FrameStreamError::UnexpectedEnd)); + } else { + return Poll::Ready(Ok(None)); } - }, - }; + } + } } } @@ -320,7 +320,13 @@ mod tests { use assert_matches::assert_matches; use bytes::{BufMut, Bytes, BytesMut}; use futures_util::future::poll_fn; - use std::collections::VecDeque; + use std::{ + collections::VecDeque, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + }; use crate::proto::{coding::Encode, frame::FrameType, varint::VarInt}; @@ -436,6 +442,26 @@ mod tests { assert_poll_matches!(|cx| stream.poll_next(cx), Ok(Some(Frame::Headers(_)))); } + #[tokio::test] + async fn poll_next_consumes_buffered_frame_before_reading_more() { + let mut recv = FakeRecv::default(); + let reads = recv.reads(); + let mut buf = BytesMut::with_capacity(64); + + Frame::headers(&b"header"[..]).encode_with_payload(&mut buf); + Frame::headers(&b"trailer"[..]).encode_with_payload(&mut buf); + recv.chunk(buf.freeze()); + recv.chunk(Bytes::from_static(b"unused")); + + let mut stream: FrameStream<_, ()> = FrameStream::new(BufRecvStream::new(recv)); + + assert_poll_matches!(|cx| stream.poll_next(cx), Ok(Some(Frame::Headers(_)))); + assert_eq!(reads.load(Ordering::Relaxed), 1); + + assert_poll_matches!(|cx| stream.poll_next(cx), Ok(Some(Frame::Headers(_)))); + assert_eq!(reads.load(Ordering::Relaxed), 1); + } + #[tokio::test] async fn poll_next_incomplete_frame() { let mut recv = FakeRecv::default(); @@ -595,6 +621,7 @@ mod tests { #[derive(Default)] struct FakeRecv { chunks: VecDeque, + reads: Arc, } impl FakeRecv { @@ -602,6 +629,10 @@ mod tests { self.chunks.push_back(buf); self } + + fn reads(&self) -> Arc { + self.reads.clone() + } } impl RecvStream for FakeRecv { @@ -611,6 +642,7 @@ mod tests { &mut self, _: &mut Context<'_>, ) -> Poll, StreamErrorIncoming>> { + self.reads.fetch_add(1, Ordering::Relaxed); Poll::Ready(Ok(self.chunks.pop_front())) } diff --git a/h3/src/qpack/decoder.rs b/h3/src/qpack/decoder.rs index 67e93c4..05e083b 100644 --- a/h3/src/qpack/decoder.rs +++ b/h3/src/qpack/decoder.rs @@ -95,7 +95,16 @@ impl Decoder { // Decode field lines received on Request of Push stream. // https://www.rfc-editor.org/rfc/rfc9204.html#name-field-line-representations + #[inline(always)] pub fn decode_header(&self, buf: &mut T) -> Result { + self.decode_header_limited(buf, u64::MAX) + } + + pub(crate) fn decode_header_limited( + &self, + buf: &mut T, + max_size: u64, + ) -> Result { let (required_ref, base) = HeaderPrefix::decode(buf)? .get(self.table.total_inserted(), self.table.max_mem_size())?; @@ -110,6 +119,9 @@ impl Decoder { while buf.has_remaining() { let field = Self::parse_header_field(&decoder_table, buf)?; mem_size += field.mem_size() as u64; + if mem_size > max_size { + return Err(DecoderError::HeaderTooLong(mem_size)); + } fields.push(field); } diff --git a/h3/src/server/request.rs b/h3/src/server/request.rs index 92ed54c..7a810eb 100644 --- a/h3/src/server/request.rs +++ b/h3/src/server/request.rs @@ -175,7 +175,7 @@ where .qpack_decoder .lock() .map_err(|_| qpack::DecoderError::UnknownPrefix(0))? - .decode_header(encoded)?; + .decode_header_limited(encoded, self.max_field_section_size)?; if decoded.mem_size > self.max_field_section_size { return Err(qpack::DecoderError::HeaderTooLong(decoded.mem_size)); From e66e52d07c91b4c9d0214925890a994c0fe29e6d Mon Sep 17 00:00:00 2001 From: gngpp Date: Wed, 10 Jun 2026 00:15:27 +0800 Subject: [PATCH 03/22] fmt --- h3/src/client/builder.rs | 14 ++++++----- h3/src/config.rs | 54 ++++++++++++++-------------------------- h3/src/connection.rs | 11 ++++---- 3 files changed, 32 insertions(+), 47 deletions(-) diff --git a/h3/src/client/builder.rs b/h3/src/client/builder.rs index b72416a..57fa4ca 100644 --- a/h3/src/client/builder.rs +++ b/h3/src/client/builder.rs @@ -114,19 +114,21 @@ impl Builder { /// Set the QPACK dynamic table capacity the encoder is permitted to use, in bytes. /// - /// Sent as `SETTINGS_QPACK_MAX_TABLE_CAPACITY` (0x1). A value of `0` (default) - /// means the encoder must not use the dynamic table. + /// Sent as `SETTINGS_QPACK_MAX_TABLE_CAPACITY` (0x1). When this is not called, + /// the setting is omitted and the protocol default of `0` applies. Passing `0` + /// explicitly sends the setting with value `0`. pub fn qpack_max_table_capacity(&mut self, value: u64) -> &mut Self { - self.config.settings.qpack_max_table_capacity = value; + self.config.settings.qpack_max_table_capacity = Some(value); self } /// Set the maximum number of blocked streams the QPACK decoder is willing to tolerate. /// - /// Sent as `SETTINGS_QPACK_BLOCKED_STREAMS` (0x7). A value of `0` (default) - /// means the decoder does not support blocking. + /// Sent as `SETTINGS_QPACK_BLOCKED_STREAMS` (0x7). When this is not called, + /// the setting is omitted and the protocol default of `0` applies. Passing `0` + /// explicitly sends the setting with value `0`. pub fn qpack_blocked_streams(&mut self, value: u64) -> &mut Self { - self.config.settings.qpack_blocked_streams = value; + self.config.settings.qpack_blocked_streams = Some(value); self } diff --git a/h3/src/config.rs b/h3/src/config.rs index b851fd0..ffc8eab 100644 --- a/h3/src/config.rs +++ b/h3/src/config.rs @@ -56,13 +56,13 @@ pub struct Settings { /// QPACK dynamic table capacity the encoder is permitted to use, in bytes. /// Sent as SETTINGS_QPACK_MAX_TABLE_CAPACITY (0x1). - /// A value of 0 (the default) means the encoder must not use the dynamic table. - pub(crate) qpack_max_table_capacity: u64, + /// When unset, the setting is omitted and the protocol default of 0 applies. + pub(crate) qpack_max_table_capacity: Option, /// Maximum number of blocked streams the decoder is willing to tolerate. /// Sent as SETTINGS_QPACK_BLOCKED_STREAMS (0x7). - /// A value of 0 (the default) means the decoder does not support blocking. - pub(crate) qpack_blocked_streams: u64, + /// When unset, the setting is omitted and the protocol default of 0 applies. + pub(crate) qpack_blocked_streams: Option, } impl From<&frame::Settings> for Settings { @@ -87,12 +87,8 @@ impl From<&frame::Settings> for Settings { .get(frame::SettingId::ENABLE_CONNECT_PROTOCOL) .map(|value| value != 0) .unwrap_or(defaults.enable_extended_connect), - qpack_max_table_capacity: settings - .get(frame::SettingId::QPACK_MAX_TABLE_CAPACITY) - .unwrap_or(defaults.qpack_max_table_capacity), - qpack_blocked_streams: settings - .get(frame::SettingId::QPACK_MAX_BLOCKED_STREAMS) - .unwrap_or(defaults.qpack_blocked_streams), + qpack_max_table_capacity: settings.get(frame::SettingId::QPACK_MAX_TABLE_CAPACITY), + qpack_blocked_streams: settings.get(frame::SettingId::QPACK_MAX_BLOCKED_STREAMS), } } } @@ -102,27 +98,17 @@ impl Config { /// or extra_settings. fn resolve_setting_value(&self, id: &frame::SettingId) -> Option { match *id { - frame::SettingId::MAX_HEADER_LIST_SIZE => { - Some(self.settings.max_field_section_size) - } + frame::SettingId::MAX_HEADER_LIST_SIZE => Some(self.settings.max_field_section_size), frame::SettingId::ENABLE_CONNECT_PROTOCOL => { Some(self.settings.enable_extended_connect as u64) } - frame::SettingId::ENABLE_WEBTRANSPORT => { - Some(self.settings.enable_webtransport as u64) - } - frame::SettingId::H3_DATAGRAM => { - Some(self.settings.enable_datagram as u64) - } + frame::SettingId::ENABLE_WEBTRANSPORT => Some(self.settings.enable_webtransport as u64), + frame::SettingId::H3_DATAGRAM => Some(self.settings.enable_datagram as u64), frame::SettingId::WEBTRANSPORT_MAX_SESSIONS => { Some(self.settings.max_webtransport_sessions) } - frame::SettingId::QPACK_MAX_TABLE_CAPACITY => { - Some(self.settings.qpack_max_table_capacity) - } - frame::SettingId::QPACK_MAX_BLOCKED_STREAMS => { - Some(self.settings.qpack_blocked_streams) - } + frame::SettingId::QPACK_MAX_TABLE_CAPACITY => self.settings.qpack_max_table_capacity, + frame::SettingId::QPACK_MAX_BLOCKED_STREAMS => self.settings.qpack_blocked_streams, _ => self .extra_settings .iter() @@ -166,14 +152,12 @@ impl TryFrom for frame::Settings { frame::SettingId::WEBTRANSPORT_MAX_SESSIONS, value.settings.max_webtransport_sessions, )?; - settings.insert( - frame::SettingId::QPACK_MAX_TABLE_CAPACITY, - value.settings.qpack_max_table_capacity, - )?; - settings.insert( - frame::SettingId::QPACK_MAX_BLOCKED_STREAMS, - value.settings.qpack_blocked_streams, - )?; + if let Some(value) = value.settings.qpack_max_table_capacity { + settings.insert(frame::SettingId::QPACK_MAX_TABLE_CAPACITY, value)?; + } + if let Some(value) = value.settings.qpack_blocked_streams { + settings.insert(frame::SettingId::QPACK_MAX_BLOCKED_STREAMS, value)?; + } // Append extra settings at the end in default mode for (id, val) in &value.extra_settings { @@ -223,8 +207,8 @@ impl Default for Settings { enable_extended_connect: false, enable_datagram: false, max_webtransport_sessions: 0, - qpack_max_table_capacity: 0, - qpack_blocked_streams: 0, + qpack_max_table_capacity: None, + qpack_blocked_streams: None, } } } diff --git a/h3/src/connection.rs b/h3/src/connection.rs index 2ded2f5..6347f9e 100644 --- a/h3/src/connection.rs +++ b/h3/src/connection.rs @@ -314,8 +314,8 @@ where //# request that the sender close the control stream. let decoder = Arc::new(Mutex::new( qpack::Decoder::new( - config.settings.qpack_max_table_capacity, - config.settings.qpack_blocked_streams, + config.settings.qpack_max_table_capacity.unwrap_or(0), + config.settings.qpack_blocked_streams.unwrap_or(0), ) .map_err(|err| { conn.close_raw_connection_with_h3_error(InternalConnectionError::new( @@ -430,11 +430,11 @@ where // Get all currently pending streams loop { + //= https://www.rfc-editor.org/rfc/rfc9204.html#section-4.2 + //# An endpoint MUST allow its peer to create an encoder stream and a + //# decoder stream even if the connection's settings prevent their use. match self .conn - //= https://www.rfc-editor.org/rfc/rfc9204.html#section-4.2 - //# An endpoint MUST allow its peer to create an encoder stream and a - //# decoder stream even if the connection's settings prevent their use. .poll_accept_recv(cx) .map_err(|e| self.handle_connection_error(e))? { @@ -553,7 +553,6 @@ where Ok(()) } - #[allow(missing_docs)] #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))] pub fn poll_qpack_encoder(&mut self, cx: &mut Context<'_>) -> Poll> where From 94576cd6b9050b860c300da6f2a6822cec74b074 Mon Sep 17 00:00:00 2001 From: gngpp Date: Wed, 10 Jun 2026 00:24:31 +0800 Subject: [PATCH 04/22] fmt --- h3/src/server/connection.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/h3/src/server/connection.rs b/h3/src/server/connection.rs index d5fcc85..ad87603 100644 --- a/h3/src/server/connection.rs +++ b/h3/src/server/connection.rs @@ -131,11 +131,10 @@ where return Ok(None); } }; + // send the grease frame only once self.inner.send_grease_frame = false; let resolver = self.create_resolver_internal(stream); - // send the grease frame only once - Ok(Some(resolver)) } From 59b77f7c1265f27c62b1cb2ad36eda95d952a8db Mon Sep 17 00:00:00 2001 From: gngpp Date: Wed, 10 Jun 2026 00:37:13 +0800 Subject: [PATCH 05/22] fmt --- h3/src/client/builder.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/h3/src/client/builder.rs b/h3/src/client/builder.rs index 57fa4ca..1df0993 100644 --- a/h3/src/client/builder.rs +++ b/h3/src/client/builder.rs @@ -168,8 +168,6 @@ impl Builder { let shared = SharedState::default(); let conn_state = Arc::new(shared); - let max_field_section_size = self.config.settings.max_field_section_size; - let send_grease_frame = self.config.send_grease; let inner = ConnectionInner::new(quic, conn_state.clone(), self.config.clone()).await?; let qpack_decoder = inner.qpack_decoder(); @@ -177,9 +175,9 @@ impl Builder { open, conn_state, qpack_decoder, - max_field_section_size, + max_field_section_size: self.config.settings.max_field_section_size, sender_count: Arc::new(AtomicUsize::new(1)), - send_grease_frame, + send_grease_frame: self.config.send_grease, _buf: PhantomData, }; From 462fce29bdf0d7bf45724319fb628a51d4c3be5b Mon Sep 17 00:00:00 2001 From: gngpp Date: Wed, 10 Jun 2026 14:39:18 +0800 Subject: [PATCH 06/22] fix dynamic table decompression contention --- examples/client.rs | 27 ++++++++++++++++++- h3/src/client/stream.rs | 58 ++++++++++++++++++++++++++--------------- h3/src/connection.rs | 1 + 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/examples/client.rs b/examples/client.rs index 87039ce..d23a766 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -135,7 +135,32 @@ async fn main() -> Result<(), Box> { let request = async move { info!("sending request ..."); - let req = http::Request::builder().uri(uri).body(()).unwrap(); + let req = http::Request::builder().uri(uri.clone()).body(()).unwrap(); + + // sending request results in a bidirectional stream, + // which is also used for receiving response + let mut stream: h3::client::RequestStream, _> = + send_request.send_request(req).await?; + + // finish on the sending side + stream.finish().await?; + + info!("receiving response ..."); + + let resp = stream.recv_response().await?; + + info!("response: {:?} {}", resp.version(), resp.status()); + info!("headers: {:#?}", resp.headers()); + + // `recv_data()` must be called after `recv_response()` for + // receiving potential response body + while let Some(mut chunk) = stream.recv_data().await? { + let mut out = tokio::io::stdout(); + out.write_all_buf(&mut chunk).await.unwrap(); + out.flush().await.unwrap(); + } + + let req = http::Request::builder().uri(uri.clone()).body(()).unwrap(); // sending request results in a bidirectional stream, // which is also used for receiving response diff --git a/h3/src/client/stream.rs b/h3/src/client/stream.rs index 8f3b360..6376ca8 100644 --- a/h3/src/client/stream.rs +++ b/h3/src/client/stream.rs @@ -97,7 +97,7 @@ where /// [`recv_data()`]: #method.recv_data #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))] pub async fn recv_response(&mut self) -> Result, StreamError> { - let mut frame = future::poll_fn(|cx| self.inner.stream.poll_next(cx)) + let frame = future::poll_fn(|cx| self.inner.stream.poll_next(cx)) .await .map_err(|e| self.handle_frame_stream_error_on_request_stream(e))? .ok_or_else(|| { @@ -123,26 +123,42 @@ where //# mismatch, it MUST respond with a connection error of type //# H3_GENERAL_PROTOCOL_ERROR. - let decoded = if let Frame::Headers(ref mut encoded) = frame { - match self.inner.decode_header_block(encoded) { - //= https://www.rfc-editor.org/rfc/rfc9114#section-4.2.2 - //# An HTTP/3 implementation MAY impose a limit on the maximum size of - //# the message header it will accept on an individual HTTP message. - Err(qpack::DecoderError::HeaderTooLong(cancel_size)) => { - self.inner.stop_sending(Code::H3_REQUEST_CANCELLED); - return Err(StreamError::HeaderTooBig { - actual_size: cancel_size, - max_size: self.inner.max_field_section_size, - }); - } - Ok(decoded) => decoded, - Err(_e) => { - return Err( - self.handle_connection_error_on_stream(InternalConnectionError { - code: Code::QPACK_DECOMPRESSION_FAILED, - message: "Failed to decode headers".to_string(), - }), - ) + let decoded = if let Frame::Headers(ref encoded) = frame { + loop { + let mut encoded = encoded.clone(); + match self.inner.decode_header_block(&mut encoded) { + //= https://www.rfc-editor.org/rfc/rfc9114#section-4.2.2 + //# An HTTP/3 implementation MAY impose a limit on the maximum size of + //# the message header it will accept on an individual HTTP message. + Err(qpack::DecoderError::HeaderTooLong(cancel_size)) => { + self.inner.stop_sending(Code::H3_REQUEST_CANCELLED); + return Err(StreamError::HeaderTooBig { + actual_size: cancel_size, + max_size: self.inner.max_field_section_size, + }); + } + Ok(decoded) => break decoded, + Err(qpack::DecoderError::MissingRefs(_)) => { + let mut registered = false; + future::poll_fn(|cx| { + if registered { + Poll::Ready(()) + } else { + self.waker().register(cx.waker()); + registered = true; + Poll::Pending + } + }) + .await; + } + Err(_e) => { + return Err(self.handle_connection_error_on_stream( + InternalConnectionError { + code: Code::QPACK_DECOMPRESSION_FAILED, + message: "Failed to decode headers".to_string(), + }, + )) + } } } } else { diff --git a/h3/src/connection.rs b/h3/src/connection.rs index 6347f9e..a3ecc9c 100644 --- a/h3/src/connection.rs +++ b/h3/src/connection.rs @@ -596,6 +596,7 @@ where ))) } }; + self.waker().wake(); if let Err(err) = decode_result { return Poll::Ready(Err(self.handle_connection_error( InternalConnectionError::new( From 65a554e2d430d0d2736f8145120df0a087056903 Mon Sep 17 00:00:00 2001 From: gngpp Date: Wed, 10 Jun 2026 14:46:55 +0800 Subject: [PATCH 07/22] update examples --- examples/client.rs | 89 ++++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/examples/client.rs b/examples/client.rs index d23a766..9b6d5e9 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -27,6 +27,14 @@ struct Opt { #[structopt(name = "keylogfile", long)] pub key_log_file: bool, + #[structopt( + long, + short = "n", + default_value = "1", + help = "Number of concurrent requests to send" + )] + pub requests: usize, + #[structopt()] pub uri: String, } @@ -42,6 +50,10 @@ async fn main() -> Result<(), Box> { let opt = Opt::from_args(); + if opt.requests == 0 { + Err("requests must be greater than 0")?; + } + // DNS lookup let uri = opt.uri.parse::()?; @@ -113,9 +125,9 @@ async fn main() -> Result<(), Box> { // h3_quinn implements the trait w/ quinn to make it work with h3. let quinn_conn = h3_quinn::Connection::new(conn); - let (mut driver, mut send_request) = h3::client::builder() - .qpack_max_table_capacity(65536u64) + let (mut driver, send_request) = h3::client::builder() .max_field_section_size(262144u64) + .qpack_max_table_capacity(65536u64) .qpack_blocked_streams(100u64) .enable_datagram(true) .send_grease(true) @@ -133,58 +145,51 @@ async fn main() -> Result<(), Box> { // So we "move" it. // vvvv let request = async move { - info!("sending request ..."); - - let req = http::Request::builder().uri(uri.clone()).body(()).unwrap(); + let mut requests = Vec::with_capacity(opt.requests); - // sending request results in a bidirectional stream, - // which is also used for receiving response - let mut stream: h3::client::RequestStream, _> = - send_request.send_request(req).await?; + for request_id in 0..opt.requests { + let uri = uri.clone(); + let mut send_request = send_request.clone(); - // finish on the sending side - stream.finish().await?; + requests.push(async move { + info!(request_id, "sending request ..."); - info!("receiving response ..."); + let req = http::Request::builder().uri(uri).body(()).unwrap(); - let resp = stream.recv_response().await?; + // sending request results in a bidirectional stream, + // which is also used for receiving response + let mut stream: h3::client::RequestStream, _> = + send_request.send_request(req).await?; - info!("response: {:?} {}", resp.version(), resp.status()); - info!("headers: {:#?}", resp.headers()); - - // `recv_data()` must be called after `recv_response()` for - // receiving potential response body - while let Some(mut chunk) = stream.recv_data().await? { - let mut out = tokio::io::stdout(); - out.write_all_buf(&mut chunk).await.unwrap(); - out.flush().await.unwrap(); - } + // finish on the sending side + stream.finish().await?; - let req = http::Request::builder().uri(uri.clone()).body(()).unwrap(); + info!(request_id, "receiving response ..."); - // sending request results in a bidirectional stream, - // which is also used for receiving response - let mut stream: h3::client::RequestStream, _> = - send_request.send_request(req).await?; + let resp = stream.recv_response().await?; - // finish on the sending side - stream.finish().await?; + info!( + request_id, + "response: {:?} {}", + resp.version(), + resp.status() + ); + info!(request_id, "headers: {:#?}", resp.headers()); - info!("receiving response ..."); + // `recv_data()` must be called after `recv_response()` for + // receiving potential response body + while let Some(mut chunk) = stream.recv_data().await? { + let mut out = tokio::io::stdout(); + out.write_all_buf(&mut chunk).await.unwrap(); + out.flush().await.unwrap(); + } - let resp = stream.recv_response().await?; - - info!("response: {:?} {}", resp.version(), resp.status()); - info!("headers: {:#?}", resp.headers()); - - // `recv_data()` must be called after `recv_response()` for - // receiving potential response body - while let Some(mut chunk) = stream.recv_data().await? { - let mut out = tokio::io::stdout(); - out.write_all_buf(&mut chunk).await.unwrap(); - out.flush().await.unwrap(); + Ok::<_, StreamError>(()) + }); } + future::try_join_all(requests).await?; + Ok::<_, StreamError>(()) }; From 1e08b063292041280ed2d28f83896aa85409a2c0 Mon Sep 17 00:00:00 2001 From: gngpp Date: Wed, 10 Jun 2026 18:56:02 +0800 Subject: [PATCH 08/22] Fix write to check poll_ready before sending data --- h3-quinn/src/lib.rs | 69 ++++++++++++------------------ h3-webtransport/src/stream.rs | 18 ++++---- h3/src/connection.rs | 76 +++++++++++++++++++++++++-------- h3/src/frame.rs | 8 +++- h3/src/quic.rs | 17 +++++++- h3/src/stream.rs | 80 ++++++++++++++++++++++++++++++++--- 6 files changed, 191 insertions(+), 77 deletions(-) diff --git a/h3-quinn/src/lib.rs b/h3-quinn/src/lib.rs index 4ac8a2a..b2a4e92 100644 --- a/h3-quinn/src/lib.rs +++ b/h3-quinn/src/lib.rs @@ -6,6 +6,7 @@ use std::{ convert::TryInto, future::Future, + marker::PhantomData, pin::Pin, sync::Arc, task::{self, ready, Poll}, @@ -309,6 +310,14 @@ where self.send.poll_ready(cx) } + fn poll_send_data( + &mut self, + cx: &mut task::Context<'_>, + data: &mut WriteBuf, + ) -> Poll> { + self.send.poll_send_data(cx, data) + } + fn poll_finish(&mut self, cx: &mut task::Context<'_>) -> Poll> { self.send.poll_finish(cx) } @@ -317,10 +326,6 @@ where self.send.reset(reset_code) } - fn send_data>>(&mut self, data: D) -> Result<(), StreamErrorIncoming> { - self.send.send_data(data) - } - fn send_id(&self) -> StreamId { self.send.send_id() } @@ -468,7 +473,7 @@ fn convert_write_error_to_stream_error(error: quinn::WriteError) -> StreamErrorI /// Implements a [`quic::SendStream`] backed by a [`quinn::SendStream`]. pub struct SendStream { stream: quinn::SendStream, - writing: Option>, + _marker: PhantomData, } impl SendStream @@ -478,7 +483,7 @@ where fn new(stream: quinn::SendStream) -> SendStream { Self { stream, - writing: None, + _marker: PhantomData, } } } @@ -488,25 +493,28 @@ where B: Buf, { #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))] - fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> Poll> { - if let Some(ref mut data) = self.writing { - while data.has_remaining() { - let stream = Pin::new(&mut self.stream); - let written = ready!(stream.poll_write(cx, data.chunk())) - .map_err(convert_write_error_to_stream_error)?; - data.advance(written); - } - } - // all data is written - self.writing = None; + fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> Poll> { Poll::Ready(Ok(())) } #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))] - fn poll_finish( + fn poll_send_data( &mut self, - _cx: &mut task::Context<'_>, + cx: &mut task::Context<'_>, + data: &mut WriteBuf, ) -> Poll> { + while data.has_remaining() { + let stream = Pin::new(&mut self.stream); + let written = ready!(stream.poll_write(cx, data.chunk())) + .map_err(convert_write_error_to_stream_error)?; + data.advance(written); + } + Poll::Ready(Ok(())) + } + + #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))] + fn poll_finish(&mut self, cx: &mut task::Context<'_>) -> Poll> { + ready!(self.poll_ready(cx))?; Poll::Ready( self.stream .finish() @@ -521,24 +529,6 @@ where .reset(VarInt::from_u64(reset_code).unwrap_or(VarInt::MAX)); } - #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))] - fn send_data>>(&mut self, data: D) -> Result<(), StreamErrorIncoming> { - if self.writing.is_some() { - // This can only happen if the traits are misused by h3 itself - // If this happens log an error and close the connection with H3_INTERNAL_ERROR - - #[cfg(feature = "tracing")] - tracing::error!("send_data called while send stream is not ready"); - return Err(StreamErrorIncoming::ConnectionErrorIncoming { - connection_error: ConnectionErrorIncoming::InternalError( - "internal error in the http stack".to_string(), - ), - }); - } - self.writing = Some(data.into()); - Ok(()) - } - #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))] fn send_id(&self) -> StreamId { let num: u64 = self.stream.id().into(); @@ -556,11 +546,6 @@ where cx: &mut task::Context<'_>, buf: &mut D, ) -> Poll> { - if self.writing.is_some() { - // This signifies a bug in implementation - panic!("poll_send called while send stream is not ready") - } - let s = Pin::new(&mut self.stream); let res = ready!(s.poll_write(cx, buf.chunk())); diff --git a/h3-webtransport/src/stream.rs b/h3-webtransport/src/stream.rs index b3b3d29..1823c8f 100644 --- a/h3-webtransport/src/stream.rs +++ b/h3-webtransport/src/stream.rs @@ -131,11 +131,12 @@ where self.stream.send_id() } - fn send_data>>( + fn poll_send_data( &mut self, - data: T, - ) -> Result<(), StreamErrorIncoming> { - self.stream.send_data(data) + cx: &mut std::task::Context<'_>, + data: &mut h3::stream::WriteBuf, + ) -> Poll> { + self.stream.poll_send_data(cx, data) } fn poll_ready( @@ -250,11 +251,12 @@ where self.stream.poll_ready(cx) } - fn send_data>>( + fn poll_send_data( &mut self, - data: T, - ) -> Result<(), StreamErrorIncoming> { - self.stream.send_data(data) + cx: &mut std::task::Context<'_>, + data: &mut h3::stream::WriteBuf, + ) -> Poll> { + self.stream.poll_send_data(cx, data) } } diff --git a/h3/src/connection.rs b/h3/src/connection.rs index a3ecc9c..841e883 100644 --- a/h3/src/connection.rs +++ b/h3/src/connection.rs @@ -137,7 +137,7 @@ where /// Grease steam is started without data Started(Option), /// Grease stream is started with data - DataPrepared(Option), + DataPrepared(Option, WriteBuf), /// Data is sent on grease stream DataSent(S), /// Grease stream is finished @@ -908,13 +908,13 @@ where if matches!(self.grease_step, GreaseStatus::NotStarted(_)) { self.grease_step = match self.conn.poll_open_send(cx) { Poll::Pending => return Poll::Pending, - Poll::Ready(Err(_)) => { + Poll::Ready(Err(_error)) => { // could not create grease stream // don't try again self.send_grease_stream_flag = false; #[cfg(feature = "tracing")] - warn!("grease stream creation failed with"); + warn!("grease stream creation failed: {}", _error); return Poll::Ready(()); } @@ -928,34 +928,76 @@ where //# sent when application-layer padding is desired. They MAY also be //# sent on connections where no data is currently being transferred. if let GreaseStatus::Started(stream) = &mut self.grease_step { - if let Some(stream) = stream { - if stream - .send_data((StreamType::grease(), Frame::Grease)) - .is_err() - { + let Some(mut stream) = stream.take() else { + // this should never happen + self.send_grease_stream_flag = false; + return Poll::Ready(()); + }; + + let mut frame = WriteBuf::from((StreamType::grease(), Frame::Grease)); + match stream.poll_ready(cx) { + Poll::Ready(Ok(())) => (), + Poll::Pending => { + self.grease_step = GreaseStatus::Started(Some(stream)); + return Poll::Pending; + } + Poll::Ready(Err(_error)) => { self.send_grease_stream_flag = false; #[cfg(feature = "tracing")] - warn!("write data on grease stream failed with"); + warn!( + "grease stream poll_ready failed before initial write: {}", + _error + ); return Poll::Ready(()); - }; + } + } + match stream.poll_send_data(cx, &mut frame) { + Poll::Ready(Ok(_)) => self.grease_step = GreaseStatus::DataSent(stream), + Poll::Pending => { + self.grease_step = GreaseStatus::DataPrepared(Some(stream), frame); + return Poll::Pending; + } + Poll::Ready(Err(_error)) => { + self.send_grease_stream_flag = false; + + #[cfg(feature = "tracing")] + warn!("grease stream initial poll_send_data failed: {}", _error); + + return Poll::Ready(()); + } } - self.grease_step = GreaseStatus::DataPrepared(stream.take()); }; - if let GreaseStatus::DataPrepared(stream) = &mut self.grease_step { + if let GreaseStatus::DataPrepared(stream, frame) = &mut self.grease_step { if let Some(stream) = stream { - match stream.poll_ready(cx) { + if let Err(_error) = ready!(stream.poll_ready(cx)) { + // could not write grease frame + // don't try again + self.send_grease_stream_flag = false; + + #[cfg(feature = "tracing")] + warn!( + "grease stream poll_ready failed while resuming pending write: {}", + _error + ); + + return Poll::Ready(()); + } + match stream.poll_send_data(cx, frame) { Poll::Ready(Ok(_)) => (), Poll::Pending => return Poll::Pending, - Poll::Ready(Err(_)) => { + Poll::Ready(Err(_error)) => { // could not write grease frame // don't try again self.send_grease_stream_flag = false; #[cfg(feature = "tracing")] - warn!("write data on grease stream failed with"); + warn!( + "grease stream poll_send_data failed while resuming pending write: {}", + _error + ); return Poll::Ready(()); } @@ -985,13 +1027,13 @@ where match stream.poll_finish(cx) { Poll::Ready(Ok(_)) => (), Poll::Pending => return Poll::Pending, - Poll::Ready(Err(_)) => { + Poll::Ready(Err(_error)) => { // could not finish grease stream // don't try again self.send_grease_stream_flag = false; #[cfg(feature = "tracing")] - warn!("finish grease stream failed with"); + warn!("grease stream finish failed: {}", _error); return Poll::Ready(()); } diff --git a/h3/src/frame.rs b/h3/src/frame.rs index 86bcabb..e30b302 100644 --- a/h3/src/frame.rs +++ b/h3/src/frame.rs @@ -173,8 +173,12 @@ where self.stream.poll_ready(cx) } - fn send_data>>(&mut self, data: D) -> Result<(), StreamErrorIncoming> { - self.stream.send_data(data) + fn poll_send_data( + &mut self, + cx: &mut Context<'_>, + data: &mut WriteBuf, + ) -> Poll> { + self.stream.poll_send_data(cx, data) } fn poll_finish(&mut self, cx: &mut Context<'_>) -> Poll> { diff --git a/h3/src/quic.rs b/h3/src/quic.rs index 923b46f..85395a0 100644 --- a/h3/src/quic.rs +++ b/h3/src/quic.rs @@ -170,10 +170,23 @@ pub trait OpenStreams { /// A trait describing the "send" actions of a QUIC stream. pub trait SendStream { /// Polls if the stream can send more data. + /// + /// This method must return [`Poll::Ready(Ok(()))`] before each call to + /// [`SendStream::poll_send_data`]. Implementations may also use this method + /// to flush data previously accepted by [`SendStream::poll_send_data`]. fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> Poll>; - /// Send more data on the stream. - fn send_data>>(&mut self, data: T) -> Result<(), StreamErrorIncoming>; + /// Begin sending data and drive the send operation with the provided context. + /// + /// Callers must call [`SendStream::poll_ready`] and receive + /// [`Poll::Ready(Ok(()))`] immediately before polling this method. If this + /// method returns [`Poll::Pending`], callers must poll + /// [`SendStream::poll_ready`] again before retrying this method. + fn poll_send_data( + &mut self, + cx: &mut task::Context<'_>, + data: &mut WriteBuf, + ) -> Poll>; /// Poll to finish the sending side of the stream. fn poll_finish(&mut self, cx: &mut task::Context<'_>) -> Poll>; diff --git a/h3/src/stream.rs b/h3/src/stream.rs index e87f896..561a489 100644 --- a/h3/src/stream.rs +++ b/h3/src/stream.rs @@ -34,9 +34,12 @@ where D: Into>, B: Buf, { - stream.send_data(data)?; - future::poll_fn(|cx| stream.poll_ready(cx)).await?; - + let mut data = data.into(); + future::poll_fn(|cx| { + ready!(stream.poll_ready(cx))?; + stream.poll_send_data(cx, &mut data) + }) + .await?; Ok(()) } @@ -51,7 +54,7 @@ const WRITE_BUF_ENCODE_SIZE: usize = 512; /// this type makes it possible to prefix wire data with the `StreamType`. /// /// Conveying frames as `Into` makes it possible to encode only when generating wire-format -/// data is necessary (say, in `quic::SendStream::send_data`). It also has a public API ergonomy +/// data is necessary (say, in `quic::SendStream::poll_send_data`). It also has a public API ergonomy /// advantage: `WriteBuf` doesn't have to appear in public associated types. On the other hand, /// QUIC implementers have to call `into()`, which will encode the header in `Self::buf`. pub struct WriteBuf { @@ -551,8 +554,12 @@ where self.stream.poll_ready(cx) } - fn send_data>>(&mut self, data: T) -> Result<(), StreamErrorIncoming> { - self.stream.send_data(data) + fn poll_send_data( + &mut self, + cx: &mut Context<'_>, + data: &mut WriteBuf, + ) -> Poll> { + self.stream.poll_send_data(cx, data) } } @@ -726,6 +733,67 @@ mod tests { use crate::proto::coding::BufExt; use super::*; + use std::sync::{Arc, Mutex}; + + #[derive(Default)] + struct WriteOrderState { + ready_calls: usize, + poll_send_calls: usize, + } + + struct WriteOrderStream { + state: Arc>, + } + + impl SendStream for WriteOrderStream { + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + self.state.lock().unwrap().ready_calls += 1; + Poll::Ready(Ok(())) + } + + fn poll_send_data( + &mut self, + cx: &mut Context<'_>, + data: &mut WriteBuf, + ) -> Poll> { + let mut state = self.state.lock().unwrap(); + state.poll_send_calls += 1; + if state.poll_send_calls == 1 { + data.advance(1); + cx.waker().wake_by_ref(); + Poll::Pending + } else { + data.advance(data.remaining()); + Poll::Ready(Ok(())) + } + } + + fn poll_finish(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn reset(&mut self, _reset_code: u64) {} + + fn send_id(&self) -> quic::StreamId { + 0u64.try_into().unwrap() + } + } + + #[tokio::test] + async fn write_uses_poll_send_data_and_flushes_pending_data() { + let state = Arc::new(Mutex::new(WriteOrderState::default())); + let mut stream = WriteOrderStream { + state: state.clone(), + }; + + write(&mut stream, Frame::Data(Bytes::from_static(b"hello"))) + .await + .unwrap(); + + let state = state.lock().unwrap(); + assert_eq!(state.poll_send_calls, 2); + assert_eq!(state.ready_calls, 2); + } #[test] fn write_wt_uni_header() { From 36de6b905925e9913545aa495feaf48e7897a6ff Mon Sep 17 00:00:00 2001 From: gngpp Date: Wed, 10 Jun 2026 23:14:14 +0800 Subject: [PATCH 09/22] waker --- h3/src/connection.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/h3/src/connection.rs b/h3/src/connection.rs index a3ecc9c..c4ca68e 100644 --- a/h3/src/connection.rs +++ b/h3/src/connection.rs @@ -581,6 +581,7 @@ where loop { while encoder_recv.has_remaining() { let before = encoder_recv.buf().remaining(); + let decoder_send_before = self.qpack_streams.decoder_send_buf.len(); let decoder = self.decoder.clone(); let decode_result = match decoder.lock() { Ok(mut decoder) => decoder.on_encoder_recv( @@ -596,7 +597,6 @@ where ))) } }; - self.waker().wake(); if let Err(err) = decode_result { return Poll::Ready(Err(self.handle_connection_error( InternalConnectionError::new( @@ -606,7 +606,14 @@ where ))); } - if encoder_recv.buf().remaining() == before { + let after = encoder_recv.buf().remaining(); + let made_progress = after != before + || self.qpack_streams.decoder_send_buf.len() != decoder_send_before; + if made_progress { + self.waker().wake(); + } + + if after == before { break; } } From 8c3808afcd8c13e1a6e91a7e9fd77935d1de7c30 Mon Sep 17 00:00:00 2001 From: gngpp Date: Wed, 10 Jun 2026 23:45:49 +0800 Subject: [PATCH 10/22] fix deadlock --- h3/src/client/builder.rs | 2 + h3/src/client/connection.rs | 3 + h3/src/client/stream.rs | 2 +- h3/src/connection.rs | 178 ++++++++++++++++++++++++++++++++---- h3/src/qpack/mod.rs | 2 +- h3/src/server/connection.rs | 1 + h3/src/server/request.rs | 35 ++++++- 7 files changed, 202 insertions(+), 21 deletions(-) diff --git a/h3/src/client/builder.rs b/h3/src/client/builder.rs index 1df0993..c8acab4 100644 --- a/h3/src/client/builder.rs +++ b/h3/src/client/builder.rs @@ -171,10 +171,12 @@ impl Builder { let inner = ConnectionInner::new(quic, conn_state.clone(), self.config.clone()).await?; let qpack_decoder = inner.qpack_decoder(); + let qpack_decoder_send_buf = inner.qpack_decoder_send_buf(); let send_request = SendRequest { open, conn_state, qpack_decoder, + qpack_decoder_send_buf, max_field_section_size: self.config.settings.max_field_section_size, sender_count: Arc::new(AtomicUsize::new(1)), send_grease_frame: self.config.send_grease, diff --git a/h3/src/client/connection.rs b/h3/src/client/connection.rs index d0e64b8..820a2d5 100644 --- a/h3/src/client/connection.rs +++ b/h3/src/client/connection.rs @@ -114,6 +114,7 @@ where pub(super) open: T, pub(super) conn_state: Arc, pub(super) qpack_decoder: Arc>, + pub(super) qpack_decoder_send_buf: Arc>, pub(super) max_field_section_size: u64, // maximum size for a header we receive // counts instances of SendRequest to close the connection when the last is dropped. pub(super) sender_count: Arc, @@ -222,6 +223,7 @@ where self.conn_state.clone(), self.send_grease_frame, Some(self.qpack_decoder.clone()), + Some(self.qpack_decoder_send_buf.clone()), ), }; // send the grease frame only once @@ -242,6 +244,7 @@ where Self { conn_state: self.conn_state.clone(), qpack_decoder: self.qpack_decoder.clone(), + qpack_decoder_send_buf: self.qpack_decoder_send_buf.clone(), open: self.open.clone(), max_field_section_size: self.max_field_section_size, sender_count: self.sender_count.clone(), diff --git a/h3/src/client/stream.rs b/h3/src/client/stream.rs index 6376ca8..175bcd1 100644 --- a/h3/src/client/stream.rs +++ b/h3/src/client/stream.rs @@ -126,7 +126,7 @@ where let decoded = if let Frame::Headers(ref encoded) = frame { loop { let mut encoded = encoded.clone(); - match self.inner.decode_header_block(&mut encoded) { + match self.inner.decode_header_block_tracked(&mut encoded) { //= https://www.rfc-editor.org/rfc/rfc9114#section-4.2.2 //# An HTTP/3 implementation MAY impose a limit on the maximum size of //# the message header it will accept on an individual HTTP message. diff --git a/h3/src/connection.rs b/h3/src/connection.rs index c4ca68e..46648d9 100644 --- a/h3/src/connection.rs +++ b/h3/src/connection.rs @@ -64,7 +64,7 @@ where B: Buf, { decoder_send: Option, - decoder_send_buf: BytesMut, + decoder_send_buf: Arc>, decoder_recv: Option>, encoder_send: Option, encoder_recv: Option>, @@ -301,7 +301,7 @@ where let qpack_streams = QpackStreams { decoder_send: qpack_decoder.ok(), - decoder_send_buf: BytesMut::new(), + decoder_send_buf: Arc::new(Mutex::new(BytesMut::new())), decoder_recv: None, encoder_send: qpack_encoder.ok(), encoder_recv: None, @@ -353,6 +353,11 @@ where self.decoder.clone() } + #[allow(missing_docs)] + pub(crate) fn qpack_decoder_send_buf(&self) -> Arc> { + self.qpack_streams.decoder_send_buf.clone() + } + /// Send GOAWAY with specified max_id, iff max_id is smaller than the previous one. #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))] pub async fn shutdown( @@ -581,13 +586,28 @@ where loop { while encoder_recv.has_remaining() { let before = encoder_recv.buf().remaining(); - let decoder_send_before = self.qpack_streams.decoder_send_buf.len(); + let decoder_send_before = self + .qpack_streams + .decoder_send_buf + .lock() + .map(|buf| buf.len()) + .unwrap_or(0); let decoder = self.decoder.clone(); + let decoder_send_buf = self.qpack_streams.decoder_send_buf.clone(); let decode_result = match decoder.lock() { - Ok(mut decoder) => decoder.on_encoder_recv( - encoder_recv.buf_mut(), - &mut self.qpack_streams.decoder_send_buf, - ), + Ok(mut decoder) => match decoder_send_buf.lock() { + Ok(mut decoder_send_buf) => { + decoder.on_encoder_recv(encoder_recv.buf_mut(), &mut *decoder_send_buf) + } + Err(_) => { + return Poll::Ready(Err(self.handle_connection_error( + InternalConnectionError::new( + Code::QPACK_DECODER_STREAM_ERROR, + "QPACK decoder stream buffer lock poisoned".to_string(), + ), + ))) + } + }, Err(_) => { return Poll::Ready(Err(self.handle_connection_error( InternalConnectionError::new( @@ -608,7 +628,12 @@ where let after = encoder_recv.buf().remaining(); let made_progress = after != before - || self.qpack_streams.decoder_send_buf.len() != decoder_send_before; + || self + .qpack_streams + .decoder_send_buf + .lock() + .map(|buf| buf.len() != decoder_send_before) + .unwrap_or(false); if made_progress { self.waker().wake(); } @@ -673,11 +698,16 @@ where where C::SendStream: quic::SendStreamUnframed, { - if !self.qpack_streams.decoder_send_buf.has_remaining() { - return Poll::Ready(Ok(())); - } + let decoder_send_buf = self.qpack_streams.decoder_send_buf.clone(); let Some(decoder_send) = self.qpack_streams.decoder_send.as_mut() else { + if decoder_send_buf + .lock() + .map(|buf| !buf.has_remaining()) + .unwrap_or(false) + { + return Poll::Ready(Ok(())); + } return Poll::Ready(Err(self.handle_connection_error( InternalConnectionError::new( Code::QPACK_DECODER_STREAM_ERROR, @@ -686,8 +716,24 @@ where ))); }; - while self.qpack_streams.decoder_send_buf.has_remaining() { - match decoder_send.poll_send(cx, &mut self.qpack_streams.decoder_send_buf) { + loop { + let mut decoder_send_buf = match decoder_send_buf.lock() { + Ok(buf) => buf, + Err(_) => { + return Poll::Ready(Err(self.handle_connection_error( + InternalConnectionError::new( + Code::QPACK_DECODER_STREAM_ERROR, + "QPACK decoder stream buffer lock poisoned".to_string(), + ), + ))) + } + }; + + if !decoder_send_buf.has_remaining() { + return Poll::Ready(Ok(())); + } + + match decoder_send.poll_send(cx, &mut *decoder_send_buf) { Poll::Ready(Ok(0)) | Poll::Pending => return Poll::Pending, Poll::Ready(Ok(_)) => {} Poll::Ready(Err(StreamErrorIncoming::ConnectionErrorIncoming { @@ -711,8 +757,6 @@ where } } } - - Poll::Ready(Ok(())) } /// Waits for the control stream to be received and reads subsequent frames. @@ -1026,10 +1070,74 @@ pub struct RequestStream { pub(super) conn_state: Arc, pub(super) max_field_section_size: u64, pub(super) qpack_decoder: Option>>, + qpack_request_state: Option, send_grease_frame: bool, } -impl RequestStream { +struct QpackRequestState { + stream_id: u64, + decoder_send_buf: Arc>, + shared: Arc, + cancel_on_drop: bool, +} + +impl QpackRequestState { + fn new( + stream_id: u64, + decoder_send_buf: Arc>, + shared: Arc, + ) -> Self { + Self { + stream_id, + decoder_send_buf, + shared, + cancel_on_drop: false, + } + } + + fn mark_cancel_on_drop(&mut self) { + self.cancel_on_drop = true; + } + + fn acknowledge(&mut self, decoded: &qpack::Decoded) -> Result<(), qpack::DecoderError> { + self.cancel_on_drop = false; + + if decoded.dyn_ref { + self.with_decoder_send_buf(|buf| qpack::ack_header(self.stream_id, buf))?; + } + + Ok(()) + } + + fn with_decoder_send_buf( + &self, + f: impl FnOnce(&mut BytesMut), + ) -> Result<(), qpack::DecoderError> { + let mut buf = self + .decoder_send_buf + .lock() + .map_err(|_| qpack::DecoderError::UnknownPrefix(0))?; + let before = buf.len(); + f(&mut buf); + if buf.len() != before { + self.shared.waker().wake(); + } + Ok(()) + } +} + +impl Drop for QpackRequestState { + fn drop(&mut self) { + if self.cancel_on_drop { + let _ = self.with_decoder_send_buf(|buf| qpack::stream_canceled(self.stream_id, buf)); + } + } +} + +impl RequestStream +where + S: quic::RecvStream, +{ #[allow(missing_docs)] pub fn new( stream: FrameStream, @@ -1037,13 +1145,23 @@ impl RequestStream { conn_state: Arc, grease: bool, qpack_decoder: Option>>, + qpack_decoder_send_buf: Option>>, ) -> Self { + let qpack_request_state = qpack_decoder_send_buf.map(|decoder_send_buf| { + QpackRequestState::new( + stream.id().into_inner(), + decoder_send_buf, + conn_state.clone(), + ) + }); + Self { stream, conn_state, max_field_section_size, trailers: None, qpack_decoder, + qpack_request_state, send_grease_frame: grease, } } @@ -1202,7 +1320,7 @@ where } } - let qpack::Decoded { fields, .. } = match self.decode_header_block(&mut trailers) { + let qpack::Decoded { fields, .. } = match self.decode_header_block_tracked(&mut trailers) { //= https://www.rfc-editor.org/rfc/rfc9114#section-4.2.2 //# An HTTP/3 implementation MAY impose a limit on the maximum size of //# the message header it will accept on an individual HTTP message. @@ -1261,6 +1379,30 @@ where Ok(decoded) } + + pub(super) fn decode_header_block_tracked( + &mut self, + encoded: &mut T, + ) -> Result { + let decoded = match self.decode_header_block(encoded) { + Ok(decoded) => decoded, + Err(error @ qpack::DecoderError::MissingRefs(required_ref)) => { + if required_ref > 0 { + if let Some(qpack_state) = &mut self.qpack_request_state { + qpack_state.mark_cancel_on_drop(); + } + } + return Err(error); + } + Err(error) => return Err(error), + }; + + if let Some(qpack_state) = &mut self.qpack_request_state { + qpack_state.acknowledge(&decoded)?; + } + + Ok(decoded) + } } impl RequestStream @@ -1371,6 +1513,7 @@ where conn_state: self.conn_state.clone(), max_field_section_size: 0, qpack_decoder: None, + qpack_request_state: None, send_grease_frame: self.send_grease_frame, }, RequestStream { @@ -1379,6 +1522,7 @@ where conn_state: self.conn_state, max_field_section_size: self.max_field_section_size, qpack_decoder: self.qpack_decoder, + qpack_request_state: self.qpack_request_state, send_grease_frame: self.send_grease_frame, }, ) diff --git a/h3/src/qpack/mod.rs b/h3/src/qpack/mod.rs index af88d43..bf92dcd 100644 --- a/h3/src/qpack/mod.rs +++ b/h3/src/qpack/mod.rs @@ -1,5 +1,5 @@ pub use self::{ - decoder::{decode_stateless, Decoded, Decoder, DecoderError}, + decoder::{ack_header, decode_stateless, stream_canceled, Decoded, Decoder, DecoderError}, encoder::{encode_stateless, EncoderError}, field::HeaderField, }; diff --git a/h3/src/server/connection.rs b/h3/src/server/connection.rs index ad87603..c680050 100644 --- a/h3/src/server/connection.rs +++ b/h3/src/server/connection.rs @@ -149,6 +149,7 @@ where max_field_section_size: self.max_field_section_size, shared: self.inner.shared.clone(), qpack_decoder: self.inner.qpack_decoder(), + qpack_decoder_send_buf: self.inner.qpack_decoder_send_buf(), } } diff --git a/h3/src/server/request.rs b/h3/src/server/request.rs index 7a810eb..9d404ed 100644 --- a/h3/src/server/request.rs +++ b/h3/src/server/request.rs @@ -3,7 +3,7 @@ use std::{ sync::{Arc, Mutex}, }; -use bytes::Buf; +use bytes::{Buf, BytesMut}; use http::{Request, StatusCode}; use tokio::sync::mpsc::UnboundedSender; @@ -44,6 +44,7 @@ where pub(super) max_field_section_size: u64, pub(super) shared: Arc, pub(super) qpack_decoder: Arc>, + pub(super) qpack_decoder_send_buf: Arc>, } impl ConnectionState for RequestResolver @@ -135,7 +136,12 @@ where //# An HTTP/3 implementation MAY impose a limit on the maximum size of //# the message header it will accept on an individual HTTP message. Err(qpack::DecoderError::HeaderTooLong(cancel_size)) => Err(cancel_size), - Ok(decoded) => Ok(decoded), + Ok(decoded) => { + if decoded.dyn_ref { + self.ack_header()?; + } + Ok(decoded) + } Err(_e) => { return Err( self.handle_connection_error_on_stream(InternalConnectionError { @@ -157,6 +163,7 @@ where self.shared.clone(), self.send_grease_frame, Some(self.qpack_decoder.clone()), + Some(self.qpack_decoder_send_buf.clone()), ), }; @@ -183,6 +190,30 @@ where Ok(decoded) } + + fn ack_header(&mut self) -> Result<(), StreamError> { + let qpack_decoder_send_buf = self.qpack_decoder_send_buf.clone(); + let mut decoder_send_buf = match qpack_decoder_send_buf.lock() { + Ok(buf) => buf, + Err(_) => { + return Err( + self.handle_connection_error_on_stream(InternalConnectionError { + code: Code::QPACK_DECOMPRESSION_FAILED, + message: "QPACK decoder stream buffer lock poisoned".to_string(), + }), + ) + } + }; + let before = decoder_send_buf.len(); + qpack::ack_header( + self.frame_stream.send_id().into_inner(), + &mut *decoder_send_buf, + ); + if decoder_send_buf.len() != before { + self.waker().wake(); + } + Ok(()) + } } pub struct ResolvedRequest From e0f17c7996558f664dbd601ed362587dc088602c Mon Sep 17 00:00:00 2001 From: gngpp Date: Thu, 11 Jun 2026 00:29:53 +0800 Subject: [PATCH 11/22] channel --- h3/src/client/builder.rs | 4 +- h3/src/client/connection.rs | 9 ++- h3/src/connection.rs | 157 ++++++++++++++---------------------- h3/src/server/connection.rs | 2 +- h3/src/server/request.rs | 39 ++++----- 5 files changed, 84 insertions(+), 127 deletions(-) diff --git a/h3/src/client/builder.rs b/h3/src/client/builder.rs index c8acab4..70f78f4 100644 --- a/h3/src/client/builder.rs +++ b/h3/src/client/builder.rs @@ -171,12 +171,12 @@ impl Builder { let inner = ConnectionInner::new(quic, conn_state.clone(), self.config.clone()).await?; let qpack_decoder = inner.qpack_decoder(); - let qpack_decoder_send_buf = inner.qpack_decoder_send_buf(); + let qpack_decoder_events = inner.qpack_decoder_events(); let send_request = SendRequest { open, conn_state, qpack_decoder, - qpack_decoder_send_buf, + qpack_decoder_events, max_field_section_size: self.config.settings.max_field_section_size, sender_count: Arc::new(AtomicUsize::new(1)), send_grease_frame: self.config.send_grease, diff --git a/h3/src/client/connection.rs b/h3/src/client/connection.rs index 820a2d5..0b6fa4c 100644 --- a/h3/src/client/connection.rs +++ b/h3/src/client/connection.rs @@ -9,12 +9,13 @@ use std::{ use bytes::{Buf, BytesMut}; use futures_util::future; use http::request; +use tokio::sync::mpsc; #[cfg(feature = "tracing")] use tracing::{info, instrument, trace}; use crate::{ - connection::{self, ConnectionInner}, + connection::{self, ConnectionInner, QpackDecoderEvent}, error::{ connection_error_creators::CloseStream, internal_error::InternalConnectionError, Code, ConnectionError, StreamError, @@ -114,7 +115,7 @@ where pub(super) open: T, pub(super) conn_state: Arc, pub(super) qpack_decoder: Arc>, - pub(super) qpack_decoder_send_buf: Arc>, + pub(super) qpack_decoder_events: mpsc::UnboundedSender, pub(super) max_field_section_size: u64, // maximum size for a header we receive // counts instances of SendRequest to close the connection when the last is dropped. pub(super) sender_count: Arc, @@ -223,7 +224,7 @@ where self.conn_state.clone(), self.send_grease_frame, Some(self.qpack_decoder.clone()), - Some(self.qpack_decoder_send_buf.clone()), + Some(self.qpack_decoder_events.clone()), ), }; // send the grease frame only once @@ -244,7 +245,7 @@ where Self { conn_state: self.conn_state.clone(), qpack_decoder: self.qpack_decoder.clone(), - qpack_decoder_send_buf: self.qpack_decoder_send_buf.clone(), + qpack_decoder_events: self.qpack_decoder_events.clone(), open: self.open.clone(), max_field_section_size: self.max_field_section_size, sender_count: self.sender_count.clone(), diff --git a/h3/src/connection.rs b/h3/src/connection.rs index 46648d9..0414dc0 100644 --- a/h3/src/connection.rs +++ b/h3/src/connection.rs @@ -9,6 +9,7 @@ use bytes::{Buf, Bytes, BytesMut}; use futures_util::{future, ready}; use http::HeaderMap; use stream::WriteBuf; +use tokio::sync::mpsc; #[cfg(feature = "tracing")] use tracing::{instrument, warn}; @@ -64,12 +65,19 @@ where B: Buf, { decoder_send: Option, - decoder_send_buf: Arc>, + decoder_send_buf: BytesMut, + decoder_events_send: mpsc::UnboundedSender, + decoder_events_recv: mpsc::UnboundedReceiver, decoder_recv: Option>, encoder_send: Option, encoder_recv: Option>, } +pub(crate) enum QpackDecoderEvent { + HeaderAck(u64), + StreamCancel(u64), +} + #[allow(missing_docs)] pub struct ConnectionInner where @@ -299,9 +307,12 @@ where Ok(control_send) => control_send, }; + let (decoder_events_send, decoder_events_recv) = mpsc::unbounded_channel(); let qpack_streams = QpackStreams { decoder_send: qpack_decoder.ok(), - decoder_send_buf: Arc::new(Mutex::new(BytesMut::new())), + decoder_send_buf: BytesMut::new(), + decoder_events_send, + decoder_events_recv, decoder_recv: None, encoder_send: qpack_encoder.ok(), encoder_recv: None, @@ -348,14 +359,12 @@ where Ok(conn_inner) } - #[allow(missing_docs)] pub(crate) fn qpack_decoder(&self) -> Arc> { self.decoder.clone() } - #[allow(missing_docs)] - pub(crate) fn qpack_decoder_send_buf(&self) -> Arc> { - self.qpack_streams.decoder_send_buf.clone() + pub(crate) fn qpack_decoder_events(&self) -> mpsc::UnboundedSender { + self.qpack_streams.decoder_events_send.clone() } /// Send GOAWAY with specified max_id, iff max_id is smaller than the previous one. @@ -586,28 +595,13 @@ where loop { while encoder_recv.has_remaining() { let before = encoder_recv.buf().remaining(); - let decoder_send_before = self - .qpack_streams - .decoder_send_buf - .lock() - .map(|buf| buf.len()) - .unwrap_or(0); + let decoder_send_before = self.qpack_streams.decoder_send_buf.len(); let decoder = self.decoder.clone(); - let decoder_send_buf = self.qpack_streams.decoder_send_buf.clone(); let decode_result = match decoder.lock() { - Ok(mut decoder) => match decoder_send_buf.lock() { - Ok(mut decoder_send_buf) => { - decoder.on_encoder_recv(encoder_recv.buf_mut(), &mut *decoder_send_buf) - } - Err(_) => { - return Poll::Ready(Err(self.handle_connection_error( - InternalConnectionError::new( - Code::QPACK_DECODER_STREAM_ERROR, - "QPACK decoder stream buffer lock poisoned".to_string(), - ), - ))) - } - }, + Ok(mut decoder) => decoder.on_encoder_recv( + encoder_recv.buf_mut(), + &mut self.qpack_streams.decoder_send_buf, + ), Err(_) => { return Poll::Ready(Err(self.handle_connection_error( InternalConnectionError::new( @@ -628,12 +622,7 @@ where let after = encoder_recv.buf().remaining(); let made_progress = after != before - || self - .qpack_streams - .decoder_send_buf - .lock() - .map(|buf| buf.len() != decoder_send_before) - .unwrap_or(false); + || self.qpack_streams.decoder_send_buf.len() != decoder_send_before; if made_progress { self.waker().wake(); } @@ -698,14 +687,10 @@ where where C::SendStream: quic::SendStreamUnframed, { - let decoder_send_buf = self.qpack_streams.decoder_send_buf.clone(); + self.drain_qpack_decoder_events(); let Some(decoder_send) = self.qpack_streams.decoder_send.as_mut() else { - if decoder_send_buf - .lock() - .map(|buf| !buf.has_remaining()) - .unwrap_or(false) - { + if !self.qpack_streams.decoder_send_buf.has_remaining() { return Poll::Ready(Ok(())); } return Poll::Ready(Err(self.handle_connection_error( @@ -716,24 +701,8 @@ where ))); }; - loop { - let mut decoder_send_buf = match decoder_send_buf.lock() { - Ok(buf) => buf, - Err(_) => { - return Poll::Ready(Err(self.handle_connection_error( - InternalConnectionError::new( - Code::QPACK_DECODER_STREAM_ERROR, - "QPACK decoder stream buffer lock poisoned".to_string(), - ), - ))) - } - }; - - if !decoder_send_buf.has_remaining() { - return Poll::Ready(Ok(())); - } - - match decoder_send.poll_send(cx, &mut *decoder_send_buf) { + while self.qpack_streams.decoder_send_buf.has_remaining() { + match decoder_send.poll_send(cx, &mut self.qpack_streams.decoder_send_buf) { Poll::Ready(Ok(0)) | Poll::Pending => return Poll::Pending, Poll::Ready(Ok(_)) => {} Poll::Ready(Err(StreamErrorIncoming::ConnectionErrorIncoming { @@ -757,6 +726,27 @@ where } } } + + Poll::Ready(Ok(())) + } + + fn drain_qpack_decoder_events(&mut self) { + let before = self.qpack_streams.decoder_send_buf.len(); + + while let Ok(event) = self.qpack_streams.decoder_events_recv.try_recv() { + match event { + QpackDecoderEvent::HeaderAck(stream_id) => { + qpack::ack_header(stream_id, &mut self.qpack_streams.decoder_send_buf) + } + QpackDecoderEvent::StreamCancel(stream_id) => { + qpack::stream_canceled(stream_id, &mut self.qpack_streams.decoder_send_buf) + } + } + } + + if self.qpack_streams.decoder_send_buf.len() != before { + self.waker().wake(); + } } /// Waits for the control stream to be received and reads subsequent frames. @@ -785,13 +775,7 @@ where })) => return Poll::Ready(Err(self.handle_connection_error(connection_error))), Err(FrameStreamError::Quic(StreamErrorIncoming::StreamTerminated { error_code: err, - })) => - //= https://www.rfc-editor.org/rfc/rfc9114#section-6.2.1 - //# If either control - //# stream is closed at any point, this MUST be treated as a connection - //# error of type H3_CLOSED_CRITICAL_STREAM. - // TODO: Add Test, that reset also triggers this - { + })) => { return Poll::Ready(Err(self.handle_connection_error( InternalConnectionError::new( Code::H3_CLOSED_CRITICAL_STREAM, @@ -807,12 +791,7 @@ where ), ))); } - Err(FrameStreamError::UnexpectedEnd) => - //= https://www.rfc-editor.org/rfc/rfc9114#section-7.1 - //# When a stream terminates cleanly, if the last frame on the stream was - //# truncated, this MUST be treated as a connection error of type - //# H3_FRAME_ERROR. - { + Err(FrameStreamError::UnexpectedEnd) => { return Poll::Ready(Err(self.handle_connection_error( InternalConnectionError::new( Code::H3_FRAME_ERROR, @@ -1076,7 +1055,7 @@ pub struct RequestStream { struct QpackRequestState { stream_id: u64, - decoder_send_buf: Arc>, + decoder_events: mpsc::UnboundedSender, shared: Arc, cancel_on_drop: bool, } @@ -1084,12 +1063,12 @@ struct QpackRequestState { impl QpackRequestState { fn new( stream_id: u64, - decoder_send_buf: Arc>, + decoder_events: mpsc::UnboundedSender, shared: Arc, ) -> Self { Self { stream_id, - decoder_send_buf, + decoder_events, shared, cancel_on_drop: false, } @@ -1103,25 +1082,17 @@ impl QpackRequestState { self.cancel_on_drop = false; if decoded.dyn_ref { - self.with_decoder_send_buf(|buf| qpack::ack_header(self.stream_id, buf))?; + self.send_event(QpackDecoderEvent::HeaderAck(self.stream_id))?; } Ok(()) } - fn with_decoder_send_buf( - &self, - f: impl FnOnce(&mut BytesMut), - ) -> Result<(), qpack::DecoderError> { - let mut buf = self - .decoder_send_buf - .lock() - .map_err(|_| qpack::DecoderError::UnknownPrefix(0))?; - let before = buf.len(); - f(&mut buf); - if buf.len() != before { - self.shared.waker().wake(); - } + fn send_event(&self, event: QpackDecoderEvent) -> Result<(), qpack::DecoderError> { + self.decoder_events + .send(event) + .map_err(|_| qpack::DecoderError::UnexpectedEnd)?; + self.shared.waker().wake(); Ok(()) } } @@ -1129,7 +1100,7 @@ impl QpackRequestState { impl Drop for QpackRequestState { fn drop(&mut self) { if self.cancel_on_drop { - let _ = self.with_decoder_send_buf(|buf| qpack::stream_canceled(self.stream_id, buf)); + let _ = self.send_event(QpackDecoderEvent::StreamCancel(self.stream_id)); } } } @@ -1139,20 +1110,16 @@ where S: quic::RecvStream, { #[allow(missing_docs)] - pub fn new( + pub(crate) fn new( stream: FrameStream, max_field_section_size: u64, conn_state: Arc, grease: bool, qpack_decoder: Option>>, - qpack_decoder_send_buf: Option>>, + qpack_decoder_events: Option>, ) -> Self { - let qpack_request_state = qpack_decoder_send_buf.map(|decoder_send_buf| { - QpackRequestState::new( - stream.id().into_inner(), - decoder_send_buf, - conn_state.clone(), - ) + let qpack_request_state = qpack_decoder_events.map(|decoder_events| { + QpackRequestState::new(stream.id().into_inner(), decoder_events, conn_state.clone()) }); Self { diff --git a/h3/src/server/connection.rs b/h3/src/server/connection.rs index c680050..b6a3e63 100644 --- a/h3/src/server/connection.rs +++ b/h3/src/server/connection.rs @@ -149,7 +149,7 @@ where max_field_section_size: self.max_field_section_size, shared: self.inner.shared.clone(), qpack_decoder: self.inner.qpack_decoder(), - qpack_decoder_send_buf: self.inner.qpack_decoder_send_buf(), + qpack_decoder_events: self.inner.qpack_decoder_events(), } } diff --git a/h3/src/server/request.rs b/h3/src/server/request.rs index 9d404ed..fe1fcde 100644 --- a/h3/src/server/request.rs +++ b/h3/src/server/request.rs @@ -3,15 +3,15 @@ use std::{ sync::{Arc, Mutex}, }; -use bytes::{Buf, BytesMut}; +use bytes::Buf; use http::{Request, StatusCode}; -use tokio::sync::mpsc::UnboundedSender; +use tokio::sync::mpsc::{self, UnboundedSender}; #[cfg(feature = "tracing")] use tracing::instrument; use crate::{ - connection::{self}, + connection::{self, QpackDecoderEvent}, error::{ connection_error_creators::{CloseStream, HandleFrameStreamErrorOnRequestStream}, internal_error::InternalConnectionError, @@ -44,7 +44,7 @@ where pub(super) max_field_section_size: u64, pub(super) shared: Arc, pub(super) qpack_decoder: Arc>, - pub(super) qpack_decoder_send_buf: Arc>, + pub(super) qpack_decoder_events: mpsc::UnboundedSender, } impl ConnectionState for RequestResolver @@ -163,7 +163,7 @@ where self.shared.clone(), self.send_grease_frame, Some(self.qpack_decoder.clone()), - Some(self.qpack_decoder_send_buf.clone()), + Some(self.qpack_decoder_events.clone()), ), }; @@ -192,26 +192,15 @@ where } fn ack_header(&mut self) -> Result<(), StreamError> { - let qpack_decoder_send_buf = self.qpack_decoder_send_buf.clone(); - let mut decoder_send_buf = match qpack_decoder_send_buf.lock() { - Ok(buf) => buf, - Err(_) => { - return Err( - self.handle_connection_error_on_stream(InternalConnectionError { - code: Code::QPACK_DECOMPRESSION_FAILED, - message: "QPACK decoder stream buffer lock poisoned".to_string(), - }), - ) - } - }; - let before = decoder_send_buf.len(); - qpack::ack_header( - self.frame_stream.send_id().into_inner(), - &mut *decoder_send_buf, - ); - if decoder_send_buf.len() != before { - self.waker().wake(); - } + self.qpack_decoder_events + .send(QpackDecoderEvent::HeaderAck(self.frame_stream.send_id().into_inner())) + .map_err(|_| { + self.handle_connection_error_on_stream(InternalConnectionError { + code: Code::QPACK_DECOMPRESSION_FAILED, + message: "QPACK decoder event channel closed".to_string(), + }) + })?; + self.waker().wake(); Ok(()) } } From 1031e05076cc177cbe16fb3186d0c2671c14ed4f Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Mon, 15 Jun 2026 11:39:42 +0800 Subject: [PATCH 12/22] feat(qpack): optimize QPACK dynamic table decoder (#7) --- h3/src/client/connection.rs | 6 +- h3/src/client/stream.rs | 1 + h3/src/connection.rs | 175 +++++++++++++++++----- h3/src/error/connection_error_creators.rs | 2 +- h3/src/server/request.rs | 44 +++--- 5 files changed, 158 insertions(+), 70 deletions(-) diff --git a/h3/src/client/connection.rs b/h3/src/client/connection.rs index 0b6fa4c..454c338 100644 --- a/h3/src/client/connection.rs +++ b/h3/src/client/connection.rs @@ -2,7 +2,7 @@ use std::{ marker::PhantomData, - sync::{atomic::AtomicUsize, Arc, Mutex}, + sync::{atomic::AtomicUsize, Arc}, task::{Context, Poll}, }; @@ -15,7 +15,7 @@ use tokio::sync::mpsc; use tracing::{info, instrument, trace}; use crate::{ - connection::{self, ConnectionInner, QpackDecoderEvent}, + connection::{self, ConnectionInner, QpackDecoder, QpackDecoderEvent}, error::{ connection_error_creators::CloseStream, internal_error::InternalConnectionError, Code, ConnectionError, StreamError, @@ -114,7 +114,7 @@ where { pub(super) open: T, pub(super) conn_state: Arc, - pub(super) qpack_decoder: Arc>, + pub(super) qpack_decoder: QpackDecoder, pub(super) qpack_decoder_events: mpsc::UnboundedSender, pub(super) max_field_section_size: u64, // maximum size for a header we receive // counts instances of SendRequest to close the connection when the last is dropped. diff --git a/h3/src/client/stream.rs b/h3/src/client/stream.rs index 175bcd1..43387da 100644 --- a/h3/src/client/stream.rs +++ b/h3/src/client/stream.rs @@ -125,6 +125,7 @@ where let decoded = if let Frame::Headers(ref encoded) = frame { loop { + // QPACK decoding advances the buffer; MissingRefs must retry from the original block. let mut encoded = encoded.clone(); match self.inner.decode_header_block_tracked(&mut encoded) { //= https://www.rfc-editor.org/rfc/rfc9114#section-4.2.2 diff --git a/h3/src/connection.rs b/h3/src/connection.rs index 0414dc0..af52056 100644 --- a/h3/src/connection.rs +++ b/h3/src/connection.rs @@ -1,12 +1,13 @@ use std::{ convert::TryFrom, marker::PhantomData, - sync::{Arc, Mutex}, + ops::Deref, + sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, TryLockError}, task::{Context, Poll}, }; use bytes::{Buf, Bytes, BytesMut}; -use futures_util::{future, ready}; +use futures_util::{future, ready, task::AtomicWaker}; use http::HeaderMap; use stream::WriteBuf; use tokio::sync::mpsc; @@ -78,6 +79,78 @@ pub(crate) enum QpackDecoderEvent { StreamCancel(u64), } +struct QpackDecoderState { + decoder: RwLock, + write_waker: AtomicWaker, +} + +#[derive(Clone)] +pub(crate) struct QpackDecoder(Arc); + +pub(crate) struct QpackDecoderReadGuard<'a> { + guard: Option>, + state: Arc, +} + +impl Deref for QpackDecoderReadGuard<'_> { + type Target = qpack::Decoder; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + self.guard + .as_deref() + .expect("QPACK decoder read guard is present") + } +} + +impl Drop for QpackDecoderReadGuard<'_> { + fn drop(&mut self) { + let _ = self.guard.take(); + self.state.write_waker.wake(); + } +} + +impl QpackDecoder { + #[inline(always)] + fn new(decoder: qpack::Decoder) -> Self { + Self(Arc::new(QpackDecoderState { + decoder: RwLock::new(decoder), + write_waker: AtomicWaker::new(), + })) + } + + #[inline(always)] + pub(crate) fn read(&self) -> Result, qpack::DecoderError> { + self.0 + .decoder + .read() + .map(|guard| QpackDecoderReadGuard { + guard: Some(guard), + state: self.0.clone(), + }) + .map_err(|_| qpack::DecoderError::UnexpectedEnd) + } + + #[inline(always)] + fn try_write( + &self, + cx: &mut Context<'_>, + ) -> Result>, qpack::DecoderError> { + match self.0.decoder.try_write() { + Ok(guard) => Ok(Some(guard)), + Err(TryLockError::WouldBlock) => { + self.0.write_waker.register(cx.waker()); + match self.0.decoder.try_write() { + Ok(guard) => Ok(Some(guard)), + Err(TryLockError::WouldBlock) => Ok(None), + Err(TryLockError::Poisoned(_)) => Err(qpack::DecoderError::UnexpectedEnd), + } + } + Err(TryLockError::Poisoned(_)) => Err(qpack::DecoderError::UnexpectedEnd), + } + } +} + #[allow(missing_docs)] pub struct ConnectionInner where @@ -92,7 +165,7 @@ where qpack_streams: QpackStreams, /// QPACK decoder state for field sections that reference the dynamic table. - decoder: Arc>, + qpack_decoder: QpackDecoder, /// Buffers incoming uni/recv streams which have yet to be claimed. /// @@ -323,7 +396,7 @@ where //# The //# sender MUST NOT close the control stream, and the receiver MUST NOT //# request that the sender close the control stream. - let decoder = Arc::new(Mutex::new( + let qpack_decoder = QpackDecoder::new( qpack::Decoder::new( config.settings.qpack_max_table_capacity.unwrap_or(0), config.settings.qpack_blocked_streams.unwrap_or(0), @@ -334,7 +407,7 @@ where format!("invalid QPACK decoder configuration: {}", err), )) })?, - )); + ); let mut conn_inner = Self { shared, @@ -342,7 +415,7 @@ where control_send, control_recv: None, qpack_streams, - decoder, + qpack_decoder, handled_connection_error: None, pending_recv_streams: Vec::with_capacity(3), got_peer_settings: false, @@ -359,14 +432,6 @@ where Ok(conn_inner) } - pub(crate) fn qpack_decoder(&self) -> Arc> { - self.decoder.clone() - } - - pub(crate) fn qpack_decoder_events(&self) -> mpsc::UnboundedSender { - self.qpack_streams.decoder_events_send.clone() - } - /// Send GOAWAY with specified max_id, iff max_id is smaller than the previous one. #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))] pub async fn shutdown( @@ -596,26 +661,37 @@ where while encoder_recv.has_remaining() { let before = encoder_recv.buf().remaining(); let decoder_send_before = self.qpack_streams.decoder_send_buf.len(); - let decoder = self.decoder.clone(); - let decode_result = match decoder.lock() { - Ok(mut decoder) => decoder.on_encoder_recv( + let decoder = self.qpack_decoder.clone(); + + let decode_result = { + let mut decoder = match decoder.try_write(cx) { + Ok(Some(decoder)) => decoder, + Ok(None) => { + self.qpack_streams.encoder_recv = + Some(AcceptedRecvStream::Encoder(encoder_recv)); + return Poll::Pending; + } + Err(err) => { + return Poll::Ready(Err(self.handle_connection_error( + InternalConnectionError::new( + Code::QPACK_ENCODER_STREAM_ERROR, + format!("QPACK decoder state lock poisoned: {err}"), + ), + ))) + } + }; + + decoder.on_encoder_recv( encoder_recv.buf_mut(), &mut self.qpack_streams.decoder_send_buf, - ), - Err(_) => { - return Poll::Ready(Err(self.handle_connection_error( - InternalConnectionError::new( - Code::QPACK_ENCODER_STREAM_ERROR, - "QPACK decoder state lock poisoned".to_string(), - ), - ))) - } + ) }; + if let Err(err) = decode_result { return Poll::Ready(Err(self.handle_connection_error( InternalConnectionError::new( Code::QPACK_ENCODER_STREAM_ERROR, - format!("invalid QPACK encoder stream instruction: {}", err), + format!("invalid QPACK encoder stream instruction: {err}"), ), ))); } @@ -1040,6 +1116,16 @@ where pub fn accepted_streams_mut(&mut self) -> &mut AcceptedStreams { &mut self.accepted_streams } + + #[inline(always)] + pub(crate) fn qpack_decoder(&self) -> QpackDecoder { + self.qpack_decoder.clone() + } + + #[inline(always)] + pub(crate) fn qpack_decoder_events(&self) -> mpsc::UnboundedSender { + self.qpack_streams.decoder_events_send.clone() + } } #[allow(missing_docs)] @@ -1048,7 +1134,7 @@ pub struct RequestStream { pub(super) trailers: Option, pub(super) conn_state: Arc, pub(super) max_field_section_size: u64, - pub(super) qpack_decoder: Option>>, + pub(super) qpack_decoder: Option, qpack_request_state: Option, send_grease_frame: bool, } @@ -1115,7 +1201,7 @@ where max_field_section_size: u64, conn_state: Arc, grease: bool, - qpack_decoder: Option>>, + qpack_decoder: Option, qpack_decoder_events: Option>, ) -> Self { let qpack_request_state = qpack_decoder_events.map(|decoder_events| { @@ -1261,15 +1347,14 @@ where //= https://www.rfc-editor.org/rfc/rfc9114#section-4.1 //# Receipt of an invalid sequence of frames MUST be treated as a //# connection error of type H3_FRAME_UNEXPECTED. - match self.stream.poll_next(cx) { Poll::Ready(Err(frame_stream_error)) => { return Poll::Ready(Err( self.handle_frame_stream_error_on_request_stream(frame_stream_error) )) } + // Received a known frame after trailers -> fail. Poll::Ready(Ok(Some(trailing_frame))) => { - // Received a known frame after trailers -> fail. return Poll::Ready(Err(self.handle_connection_error_on_stream( InternalConnectionError::new( Code::H3_FRAME_UNEXPECTED, @@ -1279,15 +1364,18 @@ where } // Stream is finished no problematic frames received Poll::Ready(Ok(None)) => (), + // Save the trailers and try again. Poll::Pending => { - // save the trailers and try again. self.trailers = Some(trailers); return Poll::Pending; } } } - let qpack::Decoded { fields, .. } = match self.decode_header_block_tracked(&mut trailers) { + let decode_result = self.decode_header_block(&mut trailers); + let decode_result = self.track_decoded_header_block(decode_result); + + let qpack::Decoded { fields, .. } = match decode_result { //= https://www.rfc-editor.org/rfc/rfc9114#section-4.2.2 //# An HTTP/3 implementation MAY impose a limit on the maximum size of //# the message header it will accept on an individual HTTP message. @@ -1327,14 +1415,13 @@ where self.stream.stop_sending(err_code); } - pub(super) fn decode_header_block( + fn decode_header_block( &self, - encoded: &mut T, + encoded: &mut Bytes, ) -> Result { let decoded = if let Some(decoder) = &self.qpack_decoder { decoder - .lock() - .map_err(|_| qpack::DecoderError::UnknownPrefix(0))? + .read()? .decode_header_limited(encoded, self.max_field_section_size)? } else { qpack::decode_stateless(encoded, self.max_field_section_size)? @@ -1347,11 +1434,11 @@ where Ok(decoded) } - pub(super) fn decode_header_block_tracked( + fn track_decoded_header_block( &mut self, - encoded: &mut T, + decoded: Result, ) -> Result { - let decoded = match self.decode_header_block(encoded) { + let decoded = match decoded { Ok(decoded) => decoded, Err(error @ qpack::DecoderError::MissingRefs(required_ref)) => { if required_ref > 0 { @@ -1370,6 +1457,14 @@ where Ok(decoded) } + + pub(super) fn decode_header_block_tracked( + &mut self, + encoded: &mut Bytes, + ) -> Result { + let decoded = self.decode_header_block(encoded); + self.track_decoded_header_block(decoded) + } } impl RequestStream diff --git a/h3/src/error/connection_error_creators.rs b/h3/src/error/connection_error_creators.rs index 93193e2..cedd5f3 100644 --- a/h3/src/error/connection_error_creators.rs +++ b/h3/src/error/connection_error_creators.rs @@ -108,7 +108,7 @@ fn convert_to_connection_error(error: ErrorOrigin) -> ConnectionError { pub trait CloseStream: ConnectionState { /// Handles a connection error on a stream fn handle_connection_error_on_stream( - &mut self, + &self, internal_error: InternalConnectionError, ) -> StreamError { let err = self.set_conn_error_and_wake(internal_error); diff --git a/h3/src/server/request.rs b/h3/src/server/request.rs index fe1fcde..1fa05da 100644 --- a/h3/src/server/request.rs +++ b/h3/src/server/request.rs @@ -1,7 +1,4 @@ -use std::{ - convert::TryFrom, - sync::{Arc, Mutex}, -}; +use std::{convert::TryFrom, sync::Arc}; use bytes::Buf; use http::{Request, StatusCode}; @@ -11,7 +8,7 @@ use tokio::sync::mpsc::{self, UnboundedSender}; use tracing::instrument; use crate::{ - connection::{self, QpackDecoderEvent}, + connection::{self, QpackDecoder, QpackDecoderEvent}, error::{ connection_error_creators::{CloseStream, HandleFrameStreamErrorOnRequestStream}, internal_error::InternalConnectionError, @@ -43,7 +40,7 @@ where pub(super) send_grease_frame: bool, pub(super) max_field_section_size: u64, pub(super) shared: Arc, - pub(super) qpack_decoder: Arc>, + pub(super) qpack_decoder: QpackDecoder, pub(super) qpack_decoder_events: mpsc::UnboundedSender, } @@ -131,7 +128,17 @@ where } }; - let decoded = match self.decode_header_block(&mut encoded) { + let decoded = match self + .qpack_decoder + .read() + .map_err(|_| { + self.handle_connection_error_on_stream(InternalConnectionError { + code: Code::QPACK_DECOMPRESSION_FAILED, + message: "QPACK decoder state lock failed".to_string(), + }) + })? + .decode_header_limited(&mut encoded, self.max_field_section_size) + { //= https://www.rfc-editor.org/rfc/rfc9114#section-4.2.2 //# An HTTP/3 implementation MAY impose a limit on the maximum size of //# the message header it will accept on an individual HTTP message. @@ -174,26 +181,11 @@ where )) } - fn decode_header_block( - &self, - encoded: &mut T, - ) -> Result { - let decoded = self - .qpack_decoder - .lock() - .map_err(|_| qpack::DecoderError::UnknownPrefix(0))? - .decode_header_limited(encoded, self.max_field_section_size)?; - - if decoded.mem_size > self.max_field_section_size { - return Err(qpack::DecoderError::HeaderTooLong(decoded.mem_size)); - } - - Ok(decoded) - } - - fn ack_header(&mut self) -> Result<(), StreamError> { + fn ack_header(&self) -> Result<(), StreamError> { self.qpack_decoder_events - .send(QpackDecoderEvent::HeaderAck(self.frame_stream.send_id().into_inner())) + .send(QpackDecoderEvent::HeaderAck( + self.frame_stream.send_id().into_inner(), + )) .map_err(|_| { self.handle_connection_error_on_stream(InternalConnectionError { code: Code::QPACK_DECOMPRESSION_FAILED, From 391cfc21d366cb6e3505bd952128904665b72eb7 Mon Sep 17 00:00:00 2001 From: gngpp Date: Mon, 15 Jun 2026 13:09:02 +0800 Subject: [PATCH 13/22] fmt --- h3/src/frame.rs | 79 ++++++++++++++++++------------------- h3/src/server/connection.rs | 4 +- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/h3/src/frame.rs b/h3/src/frame.rs index 86bcabb..fdc2b53 100644 --- a/h3/src/frame.rs +++ b/h3/src/frame.rs @@ -70,33 +70,32 @@ where ); loop { - match self.decoder.decode(self.stream.buf_mut())? { + // Decode buffered frames before reading more from the transport. + return match self.decoder.decode(self.stream.buf_mut())? { Some(Frame::Data(PayloadLen(len))) => { self.remaining_data = len; - return Poll::Ready(Ok(Some(Frame::Data(PayloadLen(len))))); + Poll::Ready(Ok(Some(Frame::Data(PayloadLen(len))))) } frame @ Some(Frame::WebTransportStream(_)) => { self.remaining_data = usize::MAX; - return Poll::Ready(Ok(frame)); + Poll::Ready(Ok(frame)) } - Some(frame) => return Poll::Ready(Ok(Some(frame))), - None => {} - } - - match self.try_recv(cx)? { - // Received a chunk but frame is incomplete, poll until we get `Pending`. - Poll::Ready(false) => continue, - Poll::Pending => return Poll::Pending, - Poll::Ready(true) => { - if self.stream.buf_mut().has_remaining() { - // Reached the end of receive stream, but there is still some data: - // The frame is incomplete. - return Poll::Ready(Err(FrameStreamError::UnexpectedEnd)); - } else { - return Poll::Ready(Ok(None)); + Some(frame) => Poll::Ready(Ok(Some(frame))), + None => match self.try_recv(cx)? { + // Received a chunk but frame is incomplete, poll until we get `Pending`. + Poll::Ready(false) => continue, + Poll::Pending => Poll::Pending, + Poll::Ready(true) => { + if self.stream.buf_mut().has_remaining() { + // Reached the end of receive stream, but there is still some data: + // The frame is incomplete. + Poll::Ready(Err(FrameStreamError::UnexpectedEnd)) + } else { + Poll::Ready(Ok(None)) + } } - } - } + }, + }; } } @@ -442,26 +441,6 @@ mod tests { assert_poll_matches!(|cx| stream.poll_next(cx), Ok(Some(Frame::Headers(_)))); } - #[tokio::test] - async fn poll_next_consumes_buffered_frame_before_reading_more() { - let mut recv = FakeRecv::default(); - let reads = recv.reads(); - let mut buf = BytesMut::with_capacity(64); - - Frame::headers(&b"header"[..]).encode_with_payload(&mut buf); - Frame::headers(&b"trailer"[..]).encode_with_payload(&mut buf); - recv.chunk(buf.freeze()); - recv.chunk(Bytes::from_static(b"unused")); - - let mut stream: FrameStream<_, ()> = FrameStream::new(BufRecvStream::new(recv)); - - assert_poll_matches!(|cx| stream.poll_next(cx), Ok(Some(Frame::Headers(_)))); - assert_eq!(reads.load(Ordering::Relaxed), 1); - - assert_poll_matches!(|cx| stream.poll_next(cx), Ok(Some(Frame::Headers(_)))); - assert_eq!(reads.load(Ordering::Relaxed), 1); - } - #[tokio::test] async fn poll_next_incomplete_frame() { let mut recv = FakeRecv::default(); @@ -616,6 +595,26 @@ mod tests { ); } + #[tokio::test] + async fn poll_next_consumes_buffered_frame_before_reading_more() { + let mut recv = FakeRecv::default(); + let reads = recv.reads(); + let mut buf = BytesMut::with_capacity(64); + + Frame::headers(&b"header"[..]).encode_with_payload(&mut buf); + Frame::headers(&b"trailer"[..]).encode_with_payload(&mut buf); + recv.chunk(buf.freeze()); + recv.chunk(Bytes::from_static(b"unused")); + + let mut stream: FrameStream<_, ()> = FrameStream::new(BufRecvStream::new(recv)); + + assert_poll_matches!(|cx| stream.poll_next(cx), Ok(Some(Frame::Headers(_)))); + assert_eq!(reads.load(Ordering::Relaxed), 1); + + assert_poll_matches!(|cx| stream.poll_next(cx), Ok(Some(Frame::Headers(_)))); + assert_eq!(reads.load(Ordering::Relaxed), 1); + } + // Helpers #[derive(Default)] diff --git a/h3/src/server/connection.rs b/h3/src/server/connection.rs index b6a3e63..1638309 100644 --- a/h3/src/server/connection.rs +++ b/h3/src/server/connection.rs @@ -131,9 +131,11 @@ where return Ok(None); } }; + + let resolver = self.create_resolver_internal(stream); + // send the grease frame only once self.inner.send_grease_frame = false; - let resolver = self.create_resolver_internal(stream); Ok(Some(resolver)) } From fb8afe26752523315726a95e478f08f65125d1eb Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Tue, 16 Jun 2026 14:23:35 +0800 Subject: [PATCH 14/22] feat(client,qpack): Unblock poll from QPACK dynamic table decoder read lock (#8) --- h3/src/client/connection.rs | 4 +- h3/src/client/stream.rs | 7 +- h3/src/connection.rs | 119 +++++++--------------------- h3/src/qpack/mod.rs | 153 ++++++++++++++++++++++++++++++++++++ h3/src/server/connection.rs | 2 - h3/src/server/request.rs | 44 ++--------- 6 files changed, 194 insertions(+), 135 deletions(-) diff --git a/h3/src/client/connection.rs b/h3/src/client/connection.rs index 454c338..5f5530b 100644 --- a/h3/src/client/connection.rs +++ b/h3/src/client/connection.rs @@ -15,14 +15,14 @@ use tokio::sync::mpsc; use tracing::{info, instrument, trace}; use crate::{ - connection::{self, ConnectionInner, QpackDecoder, QpackDecoderEvent}, + connection::{self, ConnectionInner}, error::{ connection_error_creators::CloseStream, internal_error::InternalConnectionError, Code, ConnectionError, StreamError, }, frame::FrameStream, proto::{frame::Frame, headers::Header, push::PushId}, - qpack, + qpack::{self, QpackDecoder, QpackDecoderEvent}, quic::{self, StreamId}, shared_state::{ConnectionState, SharedState}, stream::{self, BufRecvStream}, diff --git a/h3/src/client/stream.rs b/h3/src/client/stream.rs index 43387da..e880d7d 100644 --- a/h3/src/client/stream.rs +++ b/h3/src/client/stream.rs @@ -127,7 +127,12 @@ where loop { // QPACK decoding advances the buffer; MissingRefs must retry from the original block. let mut encoded = encoded.clone(); - match self.inner.decode_header_block_tracked(&mut encoded) { + match future::poll_fn(|cx| { + self.inner + .poll_decode_header_block_tracked(cx, &mut encoded) + }) + .await + { //= https://www.rfc-editor.org/rfc/rfc9114#section-4.2.2 //# An HTTP/3 implementation MAY impose a limit on the maximum size of //# the message header it will accept on an individual HTTP message. diff --git a/h3/src/connection.rs b/h3/src/connection.rs index af52056..4ec7a19 100644 --- a/h3/src/connection.rs +++ b/h3/src/connection.rs @@ -1,13 +1,12 @@ use std::{ convert::TryFrom, marker::PhantomData, - ops::Deref, - sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, TryLockError}, + sync::Arc, task::{Context, Poll}, }; use bytes::{Buf, Bytes, BytesMut}; -use futures_util::{future, ready, task::AtomicWaker}; +use futures_util::{future, ready}; use http::HeaderMap; use stream::WriteBuf; use tokio::sync::mpsc; @@ -32,6 +31,7 @@ use crate::{ varint::VarInt, }, qpack, + qpack::{QpackDecoder, QpackDecoderEvent}, quic::{self, RecvStream, SendStream, SendStreamUnframed, StreamErrorIncoming}, shared_state::{ConnectionState, SharedState}, stream::{self, AcceptRecvStream, AcceptedRecvStream, BufRecvStream, UniStreamHeader}, @@ -74,83 +74,6 @@ where encoder_recv: Option>, } -pub(crate) enum QpackDecoderEvent { - HeaderAck(u64), - StreamCancel(u64), -} - -struct QpackDecoderState { - decoder: RwLock, - write_waker: AtomicWaker, -} - -#[derive(Clone)] -pub(crate) struct QpackDecoder(Arc); - -pub(crate) struct QpackDecoderReadGuard<'a> { - guard: Option>, - state: Arc, -} - -impl Deref for QpackDecoderReadGuard<'_> { - type Target = qpack::Decoder; - - #[inline(always)] - fn deref(&self) -> &Self::Target { - self.guard - .as_deref() - .expect("QPACK decoder read guard is present") - } -} - -impl Drop for QpackDecoderReadGuard<'_> { - fn drop(&mut self) { - let _ = self.guard.take(); - self.state.write_waker.wake(); - } -} - -impl QpackDecoder { - #[inline(always)] - fn new(decoder: qpack::Decoder) -> Self { - Self(Arc::new(QpackDecoderState { - decoder: RwLock::new(decoder), - write_waker: AtomicWaker::new(), - })) - } - - #[inline(always)] - pub(crate) fn read(&self) -> Result, qpack::DecoderError> { - self.0 - .decoder - .read() - .map(|guard| QpackDecoderReadGuard { - guard: Some(guard), - state: self.0.clone(), - }) - .map_err(|_| qpack::DecoderError::UnexpectedEnd) - } - - #[inline(always)] - fn try_write( - &self, - cx: &mut Context<'_>, - ) -> Result>, qpack::DecoderError> { - match self.0.decoder.try_write() { - Ok(guard) => Ok(Some(guard)), - Err(TryLockError::WouldBlock) => { - self.0.write_waker.register(cx.waker()); - match self.0.decoder.try_write() { - Ok(guard) => Ok(Some(guard)), - Err(TryLockError::WouldBlock) => Ok(None), - Err(TryLockError::Poisoned(_)) => Err(qpack::DecoderError::UnexpectedEnd), - } - } - Err(TryLockError::Poisoned(_)) => Err(qpack::DecoderError::UnexpectedEnd), - } - } -} - #[allow(missing_docs)] pub struct ConnectionInner where @@ -1372,7 +1295,13 @@ where } } - let decode_result = self.decode_header_block(&mut trailers); + let decode_result = match self.poll_decode_header_block(cx, &mut trailers) { + Poll::Ready(decode_result) => decode_result, + Poll::Pending => { + self.trailers = Some(trailers); + return Poll::Pending; + } + }; let decode_result = self.track_decoded_header_block(decode_result); let qpack::Decoded { fields, .. } = match decode_result { @@ -1415,23 +1344,28 @@ where self.stream.stop_sending(err_code); } - fn decode_header_block( + fn poll_decode_header_block( &self, + cx: &mut Context<'_>, encoded: &mut Bytes, - ) -> Result { + ) -> Poll> { let decoded = if let Some(decoder) = &self.qpack_decoder { - decoder - .read()? - .decode_header_limited(encoded, self.max_field_section_size)? + let decoder = match decoder.try_read(cx) { + Ok(Some(decoder)) => decoder, + Ok(None) => return Poll::Pending, + Err(err) => return Poll::Ready(Err(err)), + }; + + decoder.decode_header_limited(encoded, self.max_field_section_size)? } else { qpack::decode_stateless(encoded, self.max_field_section_size)? }; if decoded.mem_size > self.max_field_section_size { - return Err(qpack::DecoderError::HeaderTooLong(decoded.mem_size)); + return Poll::Ready(Err(qpack::DecoderError::HeaderTooLong(decoded.mem_size))); } - Ok(decoded) + Poll::Ready(Ok(decoded)) } fn track_decoded_header_block( @@ -1458,12 +1392,13 @@ where Ok(decoded) } - pub(super) fn decode_header_block_tracked( + pub(super) fn poll_decode_header_block_tracked( &mut self, + cx: &mut Context<'_>, encoded: &mut Bytes, - ) -> Result { - let decoded = self.decode_header_block(encoded); - self.track_decoded_header_block(decoded) + ) -> Poll> { + let decoded = ready!(self.poll_decode_header_block(cx, encoded)); + Poll::Ready(self.track_decoded_header_block(decoded)) } } diff --git a/h3/src/qpack/mod.rs b/h3/src/qpack/mod.rs index bf92dcd..f7aa294 100644 --- a/h3/src/qpack/mod.rs +++ b/h3/src/qpack/mod.rs @@ -4,6 +4,14 @@ pub use self::{ field::HeaderField, }; +use std::{ + ops::{Deref, DerefMut}, + sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, TryLockError}, + task::Context, +}; + +use futures_util::task::AtomicWaker; + mod block; mod dynamic; mod field; @@ -37,3 +45,148 @@ impl std::fmt::Display for Error { } } } + +/// Event emitted by request streams for QPACK decoder stream instructions. +pub(crate) enum QpackDecoderEvent { + HeaderAck(u64), + StreamCancel(u64), +} + +struct DecoderState { + decoder: RwLock, + read_waker: AtomicWaker, + write_waker: AtomicWaker, +} + +/// Shared QPACK decoder state for a single HTTP/3 connection. +/// +/// The decoder is read-mostly: header blocks decode through read guards, while the +/// peer encoder stream updates the dynamic table through a write guard. +#[derive(Clone)] +pub(crate) struct QpackDecoder(Arc); + +/// Read guard for QPACK header block decoding. +/// +/// Dropping the guard wakes any task waiting to process encoder stream updates. +pub(crate) struct QpackDecoderReadGuard<'a> { + guard: Option>, + state: Arc, +} + +/// Write guard for QPACK encoder stream processing. +/// +/// Dropping the guard wakes any task waiting to decode header blocks. +pub(crate) struct QpackDecoderWriteGuard<'a> { + guard: RwLockWriteGuard<'a, Decoder>, + state: Arc, +} + +impl Deref for QpackDecoderReadGuard<'_> { + type Target = Decoder; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + self.guard + .as_deref() + .expect("QPACK decoder read guard is present") + } +} + +impl Drop for QpackDecoderReadGuard<'_> { + fn drop(&mut self) { + let _ = self.guard.take(); + self.state.write_waker.wake(); + } +} + +impl Deref for QpackDecoderWriteGuard<'_> { + type Target = Decoder; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + &self.guard + } +} + +impl DerefMut for QpackDecoderWriteGuard<'_> { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.guard + } +} + +impl Drop for QpackDecoderWriteGuard<'_> { + fn drop(&mut self) { + self.state.read_waker.wake(); + } +} + +impl QpackDecoder { + /// Wraps a connection-owned QPACK decoder in shared read/write state. + #[inline(always)] + pub(crate) fn new(decoder: Decoder) -> Self { + Self(Arc::new(DecoderState { + decoder: RwLock::new(decoder), + read_waker: AtomicWaker::new(), + write_waker: AtomicWaker::new(), + })) + } + + /// Attempts to acquire a read guard without blocking the current task. + /// + /// If a writer currently holds the decoder, the provided waker is registered + /// and `Ok(None)` is returned. + #[inline(always)] + pub(crate) fn try_read( + &self, + cx: &mut Context<'_>, + ) -> Result>, DecoderError> { + match self.0.decoder.try_read() { + Ok(guard) => Ok(Some(QpackDecoderReadGuard { + guard: Some(guard), + state: self.0.clone(), + })), + Err(TryLockError::WouldBlock) => { + self.0.read_waker.register(cx.waker()); + match self.0.decoder.try_read() { + Ok(guard) => Ok(Some(QpackDecoderReadGuard { + guard: Some(guard), + state: self.0.clone(), + })), + Err(TryLockError::WouldBlock) => Ok(None), + Err(TryLockError::Poisoned(_)) => Err(DecoderError::UnexpectedEnd), + } + } + Err(TryLockError::Poisoned(_)) => Err(DecoderError::UnexpectedEnd), + } + } + + /// Attempts to acquire a write guard without blocking the current task. + /// + /// If readers currently hold the decoder, the provided waker is registered + /// and `Ok(None)` is returned. + #[inline(always)] + pub(crate) fn try_write( + &self, + cx: &mut Context<'_>, + ) -> Result>, DecoderError> { + match self.0.decoder.try_write() { + Ok(guard) => Ok(Some(QpackDecoderWriteGuard { + guard, + state: self.0.clone(), + })), + Err(TryLockError::WouldBlock) => { + self.0.write_waker.register(cx.waker()); + match self.0.decoder.try_write() { + Ok(guard) => Ok(Some(QpackDecoderWriteGuard { + guard, + state: self.0.clone(), + })), + Err(TryLockError::WouldBlock) => Ok(None), + Err(TryLockError::Poisoned(_)) => Err(DecoderError::UnexpectedEnd), + } + } + Err(TryLockError::Poisoned(_)) => Err(DecoderError::UnexpectedEnd), + } + } +} diff --git a/h3/src/server/connection.rs b/h3/src/server/connection.rs index 1638309..3e52e9d 100644 --- a/h3/src/server/connection.rs +++ b/h3/src/server/connection.rs @@ -150,8 +150,6 @@ where send_grease_frame: self.inner.send_grease_frame, max_field_section_size: self.max_field_section_size, shared: self.inner.shared.clone(), - qpack_decoder: self.inner.qpack_decoder(), - qpack_decoder_events: self.inner.qpack_decoder_events(), } } diff --git a/h3/src/server/request.rs b/h3/src/server/request.rs index 1fa05da..d2fa9ba 100644 --- a/h3/src/server/request.rs +++ b/h3/src/server/request.rs @@ -3,12 +3,12 @@ use std::{convert::TryFrom, sync::Arc}; use bytes::Buf; use http::{Request, StatusCode}; -use tokio::sync::mpsc::{self, UnboundedSender}; +use tokio::sync::mpsc::UnboundedSender; #[cfg(feature = "tracing")] use tracing::instrument; use crate::{ - connection::{self, QpackDecoder, QpackDecoderEvent}, + connection::{self}, error::{ connection_error_creators::{CloseStream, HandleFrameStreamErrorOnRequestStream}, internal_error::InternalConnectionError, @@ -40,8 +40,6 @@ where pub(super) send_grease_frame: bool, pub(super) max_field_section_size: u64, pub(super) shared: Arc, - pub(super) qpack_decoder: QpackDecoder, - pub(super) qpack_decoder_events: mpsc::UnboundedSender, } impl ConnectionState for RequestResolver @@ -128,27 +126,12 @@ where } }; - let decoded = match self - .qpack_decoder - .read() - .map_err(|_| { - self.handle_connection_error_on_stream(InternalConnectionError { - code: Code::QPACK_DECOMPRESSION_FAILED, - message: "QPACK decoder state lock failed".to_string(), - }) - })? - .decode_header_limited(&mut encoded, self.max_field_section_size) - { + let decoded = match qpack::decode_stateless(&mut encoded, self.max_field_section_size) { //= https://www.rfc-editor.org/rfc/rfc9114#section-4.2.2 //# An HTTP/3 implementation MAY impose a limit on the maximum size of //# the message header it will accept on an individual HTTP message. Err(qpack::DecoderError::HeaderTooLong(cancel_size)) => Err(cancel_size), - Ok(decoded) => { - if decoded.dyn_ref { - self.ack_header()?; - } - Ok(decoded) - } + Ok(decoded) => Ok(decoded), Err(_e) => { return Err( self.handle_connection_error_on_stream(InternalConnectionError { @@ -169,8 +152,8 @@ where self.max_field_section_size, self.shared.clone(), self.send_grease_frame, - Some(self.qpack_decoder.clone()), - Some(self.qpack_decoder_events.clone()), + None, + None, ), }; @@ -180,21 +163,6 @@ where self.max_field_section_size, )) } - - fn ack_header(&self) -> Result<(), StreamError> { - self.qpack_decoder_events - .send(QpackDecoderEvent::HeaderAck( - self.frame_stream.send_id().into_inner(), - )) - .map_err(|_| { - self.handle_connection_error_on_stream(InternalConnectionError { - code: Code::QPACK_DECOMPRESSION_FAILED, - message: "QPACK decoder event channel closed".to_string(), - }) - })?; - self.waker().wake(); - Ok(()) - } } pub struct ResolvedRequest From ac3921e2911af80952899d276e05bd2359094b62 Mon Sep 17 00:00:00 2001 From: gngpp Date: Tue, 16 Jun 2026 14:30:53 +0800 Subject: [PATCH 15/22] fmt --- h3/src/qpack/mod.rs | 27 ++++++++++++--------------- h3/src/server/request.rs | 2 +- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/h3/src/qpack/mod.rs b/h3/src/qpack/mod.rs index f7aa294..6e22e96 100644 --- a/h3/src/qpack/mod.rs +++ b/h3/src/qpack/mod.rs @@ -69,15 +69,7 @@ pub(crate) struct QpackDecoder(Arc); /// /// Dropping the guard wakes any task waiting to process encoder stream updates. pub(crate) struct QpackDecoderReadGuard<'a> { - guard: Option>, - state: Arc, -} - -/// Write guard for QPACK encoder stream processing. -/// -/// Dropping the guard wakes any task waiting to decode header blocks. -pub(crate) struct QpackDecoderWriteGuard<'a> { - guard: RwLockWriteGuard<'a, Decoder>, + guard: RwLockReadGuard<'a, Decoder>, state: Arc, } @@ -86,19 +78,24 @@ impl Deref for QpackDecoderReadGuard<'_> { #[inline(always)] fn deref(&self) -> &Self::Target { - self.guard - .as_deref() - .expect("QPACK decoder read guard is present") + &self.guard } } impl Drop for QpackDecoderReadGuard<'_> { fn drop(&mut self) { - let _ = self.guard.take(); self.state.write_waker.wake(); } } +/// Write guard for QPACK encoder stream processing. +/// +/// Dropping the guard wakes any task waiting to decode header blocks. +pub(crate) struct QpackDecoderWriteGuard<'a> { + guard: RwLockWriteGuard<'a, Decoder>, + state: Arc, +} + impl Deref for QpackDecoderWriteGuard<'_> { type Target = Decoder; @@ -143,14 +140,14 @@ impl QpackDecoder { ) -> Result>, DecoderError> { match self.0.decoder.try_read() { Ok(guard) => Ok(Some(QpackDecoderReadGuard { - guard: Some(guard), + guard, state: self.0.clone(), })), Err(TryLockError::WouldBlock) => { self.0.read_waker.register(cx.waker()); match self.0.decoder.try_read() { Ok(guard) => Ok(Some(QpackDecoderReadGuard { - guard: Some(guard), + guard, state: self.0.clone(), })), Err(TryLockError::WouldBlock) => Ok(None), diff --git a/h3/src/server/request.rs b/h3/src/server/request.rs index d2fa9ba..d36e448 100644 --- a/h3/src/server/request.rs +++ b/h3/src/server/request.rs @@ -19,7 +19,7 @@ use crate::{ frame::{Frame, PayloadLen}, headers::Header, }, - qpack::{self}, + qpack, quic::{self, SendStream, StreamId}, shared_state::{ConnectionState, SharedState}, }; From d6ff4a987da22e08de517e677453ec9eb8b6db32 Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Tue, 16 Jun 2026 16:56:30 +0800 Subject: [PATCH 16/22] refactor(client,qpack): Improve QPACK dynamic table decoder (#9) --- examples/client.rs | 10 +- h3/src/client/builder.rs | 14 +- h3/src/client/connection.rs | 13 +- h3/src/connection.rs | 222 ++++++++----------------------- h3/src/qpack/mod.rs | 251 ++++++++++++++++++++++-------------- h3/src/server/connection.rs | 1 + h3/src/server/request.rs | 7 +- 7 files changed, 233 insertions(+), 285 deletions(-) diff --git a/examples/client.rs b/examples/client.rs index 9b6d5e9..b7d11d0 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -35,6 +35,12 @@ struct Opt { )] pub requests: usize, + #[structopt(long, default_value = "65536", help = "QPACK max table capacity")] + pub qpack_max_table_capacity: u64, + + #[structopt(long, default_value = "100", help = "QPACK blocked streams")] + pub qpack_blocked_streams: u64, + #[structopt()] pub uri: String, } @@ -127,8 +133,8 @@ async fn main() -> Result<(), Box> { let (mut driver, send_request) = h3::client::builder() .max_field_section_size(262144u64) - .qpack_max_table_capacity(65536u64) - .qpack_blocked_streams(100u64) + .qpack_max_table_capacity(opt.qpack_max_table_capacity) + .qpack_blocked_streams(opt.qpack_blocked_streams) .enable_datagram(true) .send_grease(true) .build(quinn_conn) diff --git a/h3/src/client/builder.rs b/h3/src/client/builder.rs index 70f78f4..abc3280 100644 --- a/h3/src/client/builder.rs +++ b/h3/src/client/builder.rs @@ -117,8 +117,8 @@ impl Builder { /// Sent as `SETTINGS_QPACK_MAX_TABLE_CAPACITY` (0x1). When this is not called, /// the setting is omitted and the protocol default of `0` applies. Passing `0` /// explicitly sends the setting with value `0`. - pub fn qpack_max_table_capacity(&mut self, value: u64) -> &mut Self { - self.config.settings.qpack_max_table_capacity = Some(value); + pub fn qpack_max_table_capacity>>(&mut self, value: T) -> &mut Self { + self.config.settings.qpack_max_table_capacity = value.into(); self } @@ -127,8 +127,8 @@ impl Builder { /// Sent as `SETTINGS_QPACK_BLOCKED_STREAMS` (0x7). When this is not called, /// the setting is omitted and the protocol default of `0` applies. Passing `0` /// explicitly sends the setting with value `0`. - pub fn qpack_blocked_streams(&mut self, value: u64) -> &mut Self { - self.config.settings.qpack_blocked_streams = Some(value); + pub fn qpack_blocked_streams>>(&mut self, value: T) -> &mut Self { + self.config.settings.qpack_blocked_streams = value.into(); self } @@ -170,13 +170,11 @@ impl Builder { let conn_state = Arc::new(shared); let inner = ConnectionInner::new(quic, conn_state.clone(), self.config.clone()).await?; - let qpack_decoder = inner.qpack_decoder(); - let qpack_decoder_events = inner.qpack_decoder_events(); let send_request = SendRequest { open, conn_state, - qpack_decoder, - qpack_decoder_events, + qpack_decoder: inner.qpack_decoder(), + use_qpack_dynamic_table: self.config.settings.qpack_max_table_capacity.unwrap_or(0) > 0, max_field_section_size: self.config.settings.max_field_section_size, sender_count: Arc::new(AtomicUsize::new(1)), send_grease_frame: self.config.send_grease, diff --git a/h3/src/client/connection.rs b/h3/src/client/connection.rs index 5f5530b..de7ce07 100644 --- a/h3/src/client/connection.rs +++ b/h3/src/client/connection.rs @@ -9,7 +9,6 @@ use std::{ use bytes::{Buf, BytesMut}; use futures_util::future; use http::request; -use tokio::sync::mpsc; #[cfg(feature = "tracing")] use tracing::{info, instrument, trace}; @@ -22,7 +21,7 @@ use crate::{ }, frame::FrameStream, proto::{frame::Frame, headers::Header, push::PushId}, - qpack::{self, QpackDecoder, QpackDecoderEvent}, + qpack::{self, QpackDecoder}, quic::{self, StreamId}, shared_state::{ConnectionState, SharedState}, stream::{self, BufRecvStream}, @@ -115,7 +114,7 @@ where pub(super) open: T, pub(super) conn_state: Arc, pub(super) qpack_decoder: QpackDecoder, - pub(super) qpack_decoder_events: mpsc::UnboundedSender, + pub(super) use_qpack_dynamic_table: bool, pub(super) max_field_section_size: u64, // maximum size for a header we receive // counts instances of SendRequest to close the connection when the last is dropped. pub(super) sender_count: Arc, @@ -223,8 +222,8 @@ where self.max_field_section_size, self.conn_state.clone(), self.send_grease_frame, - Some(self.qpack_decoder.clone()), - Some(self.qpack_decoder_events.clone()), + self.qpack_decoder.clone(), + self.use_qpack_dynamic_table, ), }; // send the grease frame only once @@ -245,7 +244,7 @@ where Self { conn_state: self.conn_state.clone(), qpack_decoder: self.qpack_decoder.clone(), - qpack_decoder_events: self.qpack_decoder_events.clone(), + use_qpack_dynamic_table: self.use_qpack_dynamic_table, open: self.open.clone(), max_field_section_size: self.max_field_section_size, sender_count: self.sender_count.clone(), @@ -409,7 +408,7 @@ where return Poll::Ready(err); } - if let Poll::Ready(Err(err)) = self.inner.poll_qpack_encoder(cx) { + if let Poll::Ready(Err(err)) = self.inner.poll_qpack_encoder_stream(cx) { return Poll::Ready(err); } diff --git a/h3/src/connection.rs b/h3/src/connection.rs index 4ec7a19..9f9d7e9 100644 --- a/h3/src/connection.rs +++ b/h3/src/connection.rs @@ -67,7 +67,6 @@ where { decoder_send: Option, decoder_send_buf: BytesMut, - decoder_events_send: mpsc::UnboundedSender, decoder_events_recv: mpsc::UnboundedReceiver, decoder_recv: Option>, encoder_send: Option, @@ -307,7 +306,6 @@ where let qpack_streams = QpackStreams { decoder_send: qpack_decoder.ok(), decoder_send_buf: BytesMut::new(), - decoder_events_send, decoder_events_recv, decoder_recv: None, encoder_send: qpack_encoder.ok(), @@ -330,6 +328,7 @@ where format!("invalid QPACK decoder configuration: {}", err), )) })?, + decoder_events_send, ); let mut conn_inner = Self { @@ -431,20 +430,13 @@ where let _ = self.poll_connection_error(cx)?; // Get all currently pending streams - loop { - //= https://www.rfc-editor.org/rfc/rfc9204.html#section-4.2 - //# An endpoint MUST allow its peer to create an encoder stream and a - //# decoder stream even if the connection's settings prevent their use. - match self - .conn - .poll_accept_recv(cx) - .map_err(|e| self.handle_connection_error(e))? - { - Poll::Ready(stream) => self - .pending_recv_streams - .push(Some(AcceptRecvStream::new(stream))), - Poll::Pending => break, - } + while let Poll::Ready(stream) = self + .conn + .poll_accept_recv(cx) + .map_err(|e| self.handle_connection_error(e))? + { + self.pending_recv_streams + .push(Some(AcceptRecvStream::new(stream))); } for stream in self.pending_recv_streams.iter_mut().filter(|s| s.is_some()) { @@ -556,7 +548,10 @@ where } #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))] - pub fn poll_qpack_encoder(&mut self, cx: &mut Context<'_>) -> Poll> + pub fn poll_qpack_encoder_stream( + &mut self, + cx: &mut Context<'_>, + ) -> Poll> where C::SendStream: quic::SendStreamUnframed, { @@ -586,39 +581,27 @@ where let decoder_send_before = self.qpack_streams.decoder_send_buf.len(); let decoder = self.qpack_decoder.clone(); - let decode_result = { - let mut decoder = match decoder.try_write(cx) { - Ok(Some(decoder)) => decoder, - Ok(None) => { - self.qpack_streams.encoder_recv = - Some(AcceptedRecvStream::Encoder(encoder_recv)); - return Poll::Pending; - } - Err(err) => { - return Poll::Ready(Err(self.handle_connection_error( - InternalConnectionError::new( - Code::QPACK_ENCODER_STREAM_ERROR, - format!("QPACK decoder state lock poisoned: {err}"), - ), - ))) - } - }; - - decoder.on_encoder_recv( - encoder_recv.buf_mut(), - &mut self.qpack_streams.decoder_send_buf, - ) + match decoder.poll_on_recv_encoder( + cx, + encoder_recv.buf_mut(), + &mut self.qpack_streams.decoder_send_buf, + ) { + Poll::Ready(Ok(_)) => (), + Poll::Ready(Err(err)) => { + return Poll::Ready(Err(self.handle_connection_error( + InternalConnectionError::new( + Code::QPACK_ENCODER_STREAM_ERROR, + format!("invalid QPACK encoder stream instruction: {err}"), + ), + ))) + } + Poll::Pending => { + self.qpack_streams.encoder_recv = + Some(AcceptedRecvStream::Encoder(encoder_recv)); + return Poll::Pending; + } }; - if let Err(err) = decode_result { - return Poll::Ready(Err(self.handle_connection_error( - InternalConnectionError::new( - Code::QPACK_ENCODER_STREAM_ERROR, - format!("invalid QPACK encoder stream instruction: {err}"), - ), - ))); - } - let after = encoder_recv.buf().remaining(); let made_progress = after != before || self.qpack_streams.decoder_send_buf.len() != decoder_send_before; @@ -1044,11 +1027,6 @@ where pub(crate) fn qpack_decoder(&self) -> QpackDecoder { self.qpack_decoder.clone() } - - #[inline(always)] - pub(crate) fn qpack_decoder_events(&self) -> mpsc::UnboundedSender { - self.qpack_streams.decoder_events_send.clone() - } } #[allow(missing_docs)] @@ -1057,63 +1035,11 @@ pub struct RequestStream { pub(super) trailers: Option, pub(super) conn_state: Arc, pub(super) max_field_section_size: u64, - pub(super) qpack_decoder: Option, - qpack_request_state: Option, + pub(super) qpack_decoder: QpackDecoder, + use_qpack_dynamic_table: bool, send_grease_frame: bool, } -struct QpackRequestState { - stream_id: u64, - decoder_events: mpsc::UnboundedSender, - shared: Arc, - cancel_on_drop: bool, -} - -impl QpackRequestState { - fn new( - stream_id: u64, - decoder_events: mpsc::UnboundedSender, - shared: Arc, - ) -> Self { - Self { - stream_id, - decoder_events, - shared, - cancel_on_drop: false, - } - } - - fn mark_cancel_on_drop(&mut self) { - self.cancel_on_drop = true; - } - - fn acknowledge(&mut self, decoded: &qpack::Decoded) -> Result<(), qpack::DecoderError> { - self.cancel_on_drop = false; - - if decoded.dyn_ref { - self.send_event(QpackDecoderEvent::HeaderAck(self.stream_id))?; - } - - Ok(()) - } - - fn send_event(&self, event: QpackDecoderEvent) -> Result<(), qpack::DecoderError> { - self.decoder_events - .send(event) - .map_err(|_| qpack::DecoderError::UnexpectedEnd)?; - self.shared.waker().wake(); - Ok(()) - } -} - -impl Drop for QpackRequestState { - fn drop(&mut self) { - if self.cancel_on_drop { - let _ = self.send_event(QpackDecoderEvent::StreamCancel(self.stream_id)); - } - } -} - impl RequestStream where S: quic::RecvStream, @@ -1124,12 +1050,14 @@ where max_field_section_size: u64, conn_state: Arc, grease: bool, - qpack_decoder: Option, - qpack_decoder_events: Option>, + qpack_decoder: QpackDecoder, + use_qpack_dynamic_table: bool, ) -> Self { - let qpack_request_state = qpack_decoder_events.map(|decoder_events| { - QpackRequestState::new(stream.id().into_inner(), decoder_events, conn_state.clone()) - }); + let qpack_decoder = if use_qpack_dynamic_table { + qpack_decoder.track_stream(stream.id().into_inner(), conn_state.clone()) + } else { + qpack_decoder + }; Self { stream, @@ -1137,7 +1065,7 @@ where max_field_section_size, trailers: None, qpack_decoder, - qpack_request_state, + use_qpack_dynamic_table, send_grease_frame: grease, } } @@ -1295,14 +1223,13 @@ where } } - let decode_result = match self.poll_decode_header_block(cx, &mut trailers) { + let decode_result = match self.poll_decode_header_block_tracked(cx, &mut trailers) { Poll::Ready(decode_result) => decode_result, Poll::Pending => { self.trailers = Some(trailers); return Poll::Pending; } }; - let decode_result = self.track_decoded_header_block(decode_result); let qpack::Decoded { fields, .. } = match decode_result { //= https://www.rfc-editor.org/rfc/rfc9114#section-4.2.2 @@ -1344,61 +1271,18 @@ where self.stream.stop_sending(err_code); } - fn poll_decode_header_block( - &self, - cx: &mut Context<'_>, - encoded: &mut Bytes, - ) -> Poll> { - let decoded = if let Some(decoder) = &self.qpack_decoder { - let decoder = match decoder.try_read(cx) { - Ok(Some(decoder)) => decoder, - Ok(None) => return Poll::Pending, - Err(err) => return Poll::Ready(Err(err)), - }; - - decoder.decode_header_limited(encoded, self.max_field_section_size)? - } else { - qpack::decode_stateless(encoded, self.max_field_section_size)? - }; - - if decoded.mem_size > self.max_field_section_size { - return Poll::Ready(Err(qpack::DecoderError::HeaderTooLong(decoded.mem_size))); - } - - Poll::Ready(Ok(decoded)) - } - - fn track_decoded_header_block( - &mut self, - decoded: Result, - ) -> Result { - let decoded = match decoded { - Ok(decoded) => decoded, - Err(error @ qpack::DecoderError::MissingRefs(required_ref)) => { - if required_ref > 0 { - if let Some(qpack_state) = &mut self.qpack_request_state { - qpack_state.mark_cancel_on_drop(); - } - } - return Err(error); - } - Err(error) => return Err(error), - }; - - if let Some(qpack_state) = &mut self.qpack_request_state { - qpack_state.acknowledge(&decoded)?; - } - - Ok(decoded) - } - - pub(super) fn poll_decode_header_block_tracked( + #[inline(always)] + pub(crate) fn poll_decode_header_block_tracked( &mut self, cx: &mut Context<'_>, encoded: &mut Bytes, ) -> Poll> { - let decoded = ready!(self.poll_decode_header_block(cx, encoded)); - Poll::Ready(self.track_decoded_header_block(decoded)) + self.qpack_decoder.poll_decode_header_limited_tracked( + cx, + encoded, + self.max_field_section_size, + self.use_qpack_dynamic_table, + ) } } @@ -1509,8 +1393,8 @@ where trailers: None, conn_state: self.conn_state.clone(), max_field_section_size: 0, - qpack_decoder: None, - qpack_request_state: None, + qpack_decoder: self.qpack_decoder.clone(), + use_qpack_dynamic_table: false, send_grease_frame: self.send_grease_frame, }, RequestStream { @@ -1519,7 +1403,7 @@ where conn_state: self.conn_state, max_field_section_size: self.max_field_section_size, qpack_decoder: self.qpack_decoder, - qpack_request_state: self.qpack_request_state, + use_qpack_dynamic_table: self.use_qpack_dynamic_table, send_grease_frame: self.send_grease_frame, }, ) diff --git a/h3/src/qpack/mod.rs b/h3/src/qpack/mod.rs index 6e22e96..c7ec163 100644 --- a/h3/src/qpack/mod.rs +++ b/h3/src/qpack/mod.rs @@ -5,12 +5,15 @@ pub use self::{ }; use std::{ - ops::{Deref, DerefMut}, - sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, TryLockError}, - task::Context, + sync::{Arc, RwLock, TryLockError}, + task::{ready, Context, Poll}, }; +use bytes::{Buf, BufMut}; use futures_util::task::AtomicWaker; +use tokio::sync::mpsc; + +use crate::shared_state::{ConnectionState, SharedState}; mod block; mod dynamic; @@ -52,8 +55,9 @@ pub(crate) enum QpackDecoderEvent { StreamCancel(u64), } -struct DecoderState { +struct QpackDecoderInner { decoder: RwLock, + decoder_events: mpsc::UnboundedSender, read_waker: AtomicWaker, write_waker: AtomicWaker, } @@ -62,128 +66,183 @@ struct DecoderState { /// /// The decoder is read-mostly: header blocks decode through read guards, while the /// peer encoder stream updates the dynamic table through a write guard. -#[derive(Clone)] -pub(crate) struct QpackDecoder(Arc); - -/// Read guard for QPACK header block decoding. -/// -/// Dropping the guard wakes any task waiting to process encoder stream updates. -pub(crate) struct QpackDecoderReadGuard<'a> { - guard: RwLockReadGuard<'a, Decoder>, - state: Arc, +pub(crate) struct QpackDecoder { + inner: Arc, + stream_state: Option, } -impl Deref for QpackDecoderReadGuard<'_> { - type Target = Decoder; - - #[inline(always)] - fn deref(&self) -> &Self::Target { - &self.guard - } +/// Per-stream QPACK decoder state used to emit decoder stream instructions. +struct QpackDecoderStreamState { + stream_id: u64, + shared: Arc, + cancel_on_drop: bool, } -impl Drop for QpackDecoderReadGuard<'_> { - fn drop(&mut self) { - self.state.write_waker.wake(); +impl Clone for QpackDecoder { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + stream_state: None, + } } } -/// Write guard for QPACK encoder stream processing. -/// -/// Dropping the guard wakes any task waiting to decode header blocks. -pub(crate) struct QpackDecoderWriteGuard<'a> { - guard: RwLockWriteGuard<'a, Decoder>, - state: Arc, -} - -impl Deref for QpackDecoderWriteGuard<'_> { - type Target = Decoder; - - #[inline(always)] - fn deref(&self) -> &Self::Target { - &self.guard +impl Drop for QpackDecoder { + fn drop(&mut self) { + if let Some(stream_state) = &self.stream_state { + if stream_state.cancel_on_drop { + if self + .inner + .decoder_events + .send(QpackDecoderEvent::StreamCancel(stream_state.stream_id)) + .is_ok() + { + stream_state.shared.waker().wake(); + } + } + } } } -impl DerefMut for QpackDecoderWriteGuard<'_> { +impl QpackDecoder { + /// Creates a new [`QpackDecoder`] instance. #[inline(always)] - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.guard - } -} - -impl Drop for QpackDecoderWriteGuard<'_> { - fn drop(&mut self) { - self.state.read_waker.wake(); + pub(crate) fn new( + decoder: Decoder, + decoder_events: mpsc::UnboundedSender, + ) -> Self { + Self { + inner: Arc::new(QpackDecoderInner { + decoder: RwLock::new(decoder), + decoder_events, + read_waker: AtomicWaker::new(), + write_waker: AtomicWaker::new(), + }), + stream_state: None, + } } -} -impl QpackDecoder { - /// Wraps a connection-owned QPACK decoder in shared read/write state. + /// Returns a stream-scoped decoder handle that tracks decoder stream instructions. #[inline(always)] - pub(crate) fn new(decoder: Decoder) -> Self { - Self(Arc::new(DecoderState { - decoder: RwLock::new(decoder), - read_waker: AtomicWaker::new(), - write_waker: AtomicWaker::new(), - })) + pub(crate) fn track_stream(mut self, stream_id: u64, shared: Arc) -> Self { + self.stream_state = Some(QpackDecoderStreamState { + stream_id, + shared, + cancel_on_drop: false, + }); + self } - /// Attempts to acquire a read guard without blocking the current task. + /// Processes bytes received from the peer QPACK encoder stream. /// - /// If a writer currently holds the decoder, the provided waker is registered - /// and `Ok(None)` is returned. - #[inline(always)] - pub(crate) fn try_read( + /// Returns `Poll::Pending` when a header decode currently holds a read lock; + /// the provided waker is registered and will be woken once a write may make progress. + pub(crate) fn poll_on_recv_encoder( &self, cx: &mut Context<'_>, - ) -> Result>, DecoderError> { - match self.0.decoder.try_read() { - Ok(guard) => Ok(Some(QpackDecoderReadGuard { - guard, - state: self.0.clone(), - })), + read: &mut R, + write: &mut W, + ) -> Poll> { + let mut decoder = match self.inner.decoder.try_write() { + Ok(guard) => guard, Err(TryLockError::WouldBlock) => { - self.0.read_waker.register(cx.waker()); - match self.0.decoder.try_read() { - Ok(guard) => Ok(Some(QpackDecoderReadGuard { - guard, - state: self.0.clone(), - })), - Err(TryLockError::WouldBlock) => Ok(None), - Err(TryLockError::Poisoned(_)) => Err(DecoderError::UnexpectedEnd), + self.inner.write_waker.register(cx.waker()); + match self.inner.decoder.try_write() { + Ok(guard) => guard, + Err(TryLockError::WouldBlock) => return Poll::Pending, + Err(TryLockError::Poisoned(_)) => { + return Poll::Ready(Err(DecoderError::UnexpectedEnd)) + } } } - Err(TryLockError::Poisoned(_)) => Err(DecoderError::UnexpectedEnd), - } + Err(TryLockError::Poisoned(_)) => return Poll::Ready(Err(DecoderError::UnexpectedEnd)), + }; + + let result = decoder.on_encoder_recv(read, write); + drop(decoder); + self.inner.read_waker.wake(); + + Poll::Ready(result) } - /// Attempts to acquire a write guard without blocking the current task. + /// Decodes a header block using either stateless QPACK or the dynamic table. /// - /// If readers currently hold the decoder, the provided waker is registered - /// and `Ok(None)` is returned. - #[inline(always)] - pub(crate) fn try_write( + /// When `use_dynamic_table` is `false`, no lock is acquired and dynamic table + /// references are rejected by `decode_stateless`. + pub(crate) fn poll_decode_header_limited( &self, cx: &mut Context<'_>, - ) -> Result>, DecoderError> { - match self.0.decoder.try_write() { - Ok(guard) => Ok(Some(QpackDecoderWriteGuard { - guard, - state: self.0.clone(), - })), + encoded: &mut T, + max_size: u64, + use_dynamic_table: bool, + ) -> Poll> { + if !use_dynamic_table { + return Poll::Ready(decode_stateless(encoded, max_size)); + } + + let decoder = match self.inner.decoder.try_read() { + Ok(guard) => guard, Err(TryLockError::WouldBlock) => { - self.0.write_waker.register(cx.waker()); - match self.0.decoder.try_write() { - Ok(guard) => Ok(Some(QpackDecoderWriteGuard { - guard, - state: self.0.clone(), - })), - Err(TryLockError::WouldBlock) => Ok(None), - Err(TryLockError::Poisoned(_)) => Err(DecoderError::UnexpectedEnd), + self.inner.read_waker.register(cx.waker()); + match self.inner.decoder.try_read() { + Ok(guard) => guard, + Err(TryLockError::WouldBlock) => return Poll::Pending, + Err(TryLockError::Poisoned(_)) => { + return Poll::Ready(Err(DecoderError::UnexpectedEnd)) + } } } - Err(TryLockError::Poisoned(_)) => Err(DecoderError::UnexpectedEnd), + Err(TryLockError::Poisoned(_)) => return Poll::Ready(Err(DecoderError::UnexpectedEnd)), + }; + + let decoded = decoder.decode_header_limited(encoded, max_size); + drop(decoder); + self.inner.write_waker.wake(); + + Poll::Ready(decoded) + } + + /// Decodes a header block and tracks decoder stream instructions for it. + pub(crate) fn poll_decode_header_limited_tracked( + &mut self, + cx: &mut Context<'_>, + encoded: &mut T, + max_size: u64, + use_dynamic_table: bool, + ) -> Poll> { + let decoded = match ready!(self.poll_decode_header_limited( + cx, + encoded, + max_size, + use_dynamic_table, + )) { + Ok(decoded) => decoded, + Err(error @ DecoderError::MissingRefs(required_ref)) => { + if required_ref > 0 { + if let Some(stream_state) = &mut self.stream_state { + stream_state.cancel_on_drop = true; + } + } + return Poll::Ready(Err(error)); + } + Err(error) => return Poll::Ready(Err(error)), + }; + + if let Some(stream_state) = &mut self.stream_state { + stream_state.cancel_on_drop = false; + if decoded.dyn_ref { + if self + .inner + .decoder_events + .send(QpackDecoderEvent::HeaderAck(stream_state.stream_id)) + .is_err() + { + return Poll::Ready(Err(DecoderError::UnexpectedEnd)); + } + stream_state.shared.waker().wake(); + } } + + Poll::Ready(Ok(decoded)) } } diff --git a/h3/src/server/connection.rs b/h3/src/server/connection.rs index 3e52e9d..4138a16 100644 --- a/h3/src/server/connection.rs +++ b/h3/src/server/connection.rs @@ -150,6 +150,7 @@ where send_grease_frame: self.inner.send_grease_frame, max_field_section_size: self.max_field_section_size, shared: self.inner.shared.clone(), + qpack_decoder: self.inner.qpack_decoder(), } } diff --git a/h3/src/server/request.rs b/h3/src/server/request.rs index d36e448..533a84f 100644 --- a/h3/src/server/request.rs +++ b/h3/src/server/request.rs @@ -19,7 +19,7 @@ use crate::{ frame::{Frame, PayloadLen}, headers::Header, }, - qpack, + qpack::{self, QpackDecoder}, quic::{self, SendStream, StreamId}, shared_state::{ConnectionState, SharedState}, }; @@ -40,6 +40,7 @@ where pub(super) send_grease_frame: bool, pub(super) max_field_section_size: u64, pub(super) shared: Arc, + pub(super) qpack_decoder: QpackDecoder, } impl ConnectionState for RequestResolver @@ -152,8 +153,8 @@ where self.max_field_section_size, self.shared.clone(), self.send_grease_frame, - None, - None, + self.qpack_decoder, + false, ), }; From 074675c4cc3f6e601676d6279e96e1e0d936410f Mon Sep 17 00:00:00 2001 From: gngpp Date: Tue, 16 Jun 2026 17:00:58 +0800 Subject: [PATCH 17/22] update examples --- examples/client.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/examples/client.rs b/examples/client.rs index b7d11d0..5d4bbf7 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -35,13 +35,16 @@ struct Opt { )] pub requests: usize, - #[structopt(long, default_value = "65536", help = "QPACK max table capacity")] - pub qpack_max_table_capacity: u64, + #[structopt(long, help = "QPACK max table capacity")] + pub qpack_max_table_capacity: Option, - #[structopt(long, default_value = "100", help = "QPACK blocked streams")] - pub qpack_blocked_streams: u64, + #[structopt(long, help = "QPACK blocked streams")] + pub qpack_blocked_streams: Option, - #[structopt()] + #[structopt( + default_value = "https://cloudflare-quic.com", + help = "URI of the server to connect to" + )] pub uri: String, } From 537bb34af1ebb90fd29040f5420f35406ced4b8e Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Tue, 16 Jun 2026 17:13:04 +0800 Subject: [PATCH 18/22] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- h3/src/connection.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/h3/src/connection.rs b/h3/src/connection.rs index 9f9d7e9..9bb09c7 100644 --- a/h3/src/connection.rs +++ b/h3/src/connection.rs @@ -1241,6 +1241,12 @@ where max_size: self.max_field_section_size, })); } + Err(qpack::DecoderError::MissingRefs(_)) => { + // QPACK decoding advances the buffer; MissingRefs must retry from the original block. + self.trailers = Some(trailers); + self.waker().register(cx.waker()); + return Poll::Pending; + } Ok(decoded) => decoded, Err(_e) => { return Poll::Ready(Err(self.handle_connection_error_on_stream( From 05d7e1b30011179e5b2b33abcd4713b3901cfb2c Mon Sep 17 00:00:00 2001 From: gngpp Date: Tue, 16 Jun 2026 17:17:02 +0800 Subject: [PATCH 19/22] Potential fix for pull request finding --- h3/src/qpack/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/h3/src/qpack/mod.rs b/h3/src/qpack/mod.rs index c7ec163..5a55f38 100644 --- a/h3/src/qpack/mod.rs +++ b/h3/src/qpack/mod.rs @@ -128,7 +128,9 @@ impl QpackDecoder { self.stream_state = Some(QpackDecoderStreamState { stream_id, shared, - cancel_on_drop: false, + // Until the header block is successfully decoded, dropping the tracked + // stream abandons it and must notify the peer encoder. + cancel_on_drop: true, }); self } From 056d1732006e1feb7a5155af7c061da1e320cc54 Mon Sep 17 00:00:00 2001 From: gngpp Date: Tue, 16 Jun 2026 17:19:45 +0800 Subject: [PATCH 20/22] Potential fix for pull request finding --- h3/src/connection.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/h3/src/connection.rs b/h3/src/connection.rs index 9bb09c7..1ef7b94 100644 --- a/h3/src/connection.rs +++ b/h3/src/connection.rs @@ -1223,10 +1223,12 @@ where } } + // QPACK decoding advances the buffer; retry paths must keep the original block. + let encoded_trailers = trailers.clone(); let decode_result = match self.poll_decode_header_block_tracked(cx, &mut trailers) { Poll::Ready(decode_result) => decode_result, Poll::Pending => { - self.trailers = Some(trailers); + self.trailers = Some(encoded_trailers); return Poll::Pending; } }; @@ -1243,7 +1245,7 @@ where } Err(qpack::DecoderError::MissingRefs(_)) => { // QPACK decoding advances the buffer; MissingRefs must retry from the original block. - self.trailers = Some(trailers); + self.trailers = Some(encoded_trailers); self.waker().register(cx.waker()); return Poll::Pending; } From a4b0c3f8cb799c2776358599e10b20778b0ec772 Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Wed, 17 Jun 2026 13:06:56 +0800 Subject: [PATCH 21/22] fix(client,qpack): Fix wakeup deadlock risk in QPACK decoder rwlock (#10) --- h3/src/connection.rs | 2 +- h3/src/qpack/mod.rs | 106 +++++++++++++++++++++++++------------------ 2 files changed, 62 insertions(+), 46 deletions(-) diff --git a/h3/src/connection.rs b/h3/src/connection.rs index 1ef7b94..28d7b6a 100644 --- a/h3/src/connection.rs +++ b/h3/src/connection.rs @@ -1285,7 +1285,7 @@ where cx: &mut Context<'_>, encoded: &mut Bytes, ) -> Poll> { - self.qpack_decoder.poll_decode_header_limited_tracked( + self.qpack_decoder.poll_decode_header_tracked( cx, encoded, self.max_field_section_size, diff --git a/h3/src/qpack/mod.rs b/h3/src/qpack/mod.rs index 5a55f38..6ade0cf 100644 --- a/h3/src/qpack/mod.rs +++ b/h3/src/qpack/mod.rs @@ -145,33 +145,43 @@ impl QpackDecoder { read: &mut R, write: &mut W, ) -> Poll> { - let mut decoder = match self.inner.decoder.try_write() { - Ok(guard) => guard, + match self.inner.decoder.try_write() { + Ok(mut decoder) => { + let result = decoder.on_encoder_recv(read, write); + drop(decoder); + self.inner.read_waker.wake(); + Poll::Ready(result) + } Err(TryLockError::WouldBlock) => { + // A header decode is holding a read lock. Register before retrying + // so a read-lock release cannot be missed between attempts. self.inner.write_waker.register(cx.waker()); match self.inner.decoder.try_write() { - Ok(guard) => guard, - Err(TryLockError::WouldBlock) => return Poll::Pending, - Err(TryLockError::Poisoned(_)) => { - return Poll::Ready(Err(DecoderError::UnexpectedEnd)) + Ok(mut decoder) => { + // The read lock was released after registration; process now + // instead of returning Pending and waiting for another wake. + let result = decoder.on_encoder_recv(read, write); + drop(decoder); + self.inner.read_waker.wake(); + Poll::Ready(result) + } + Err(TryLockError::WouldBlock) => { + // Still blocked. The registered waker will be notified when + // the current readers release the decoder. + Poll::Pending } + Err(TryLockError::Poisoned(_)) => Poll::Ready(Err(DecoderError::UnexpectedEnd)), } } - Err(TryLockError::Poisoned(_)) => return Poll::Ready(Err(DecoderError::UnexpectedEnd)), - }; - - let result = decoder.on_encoder_recv(read, write); - drop(decoder); - self.inner.read_waker.wake(); - - Poll::Ready(result) + Err(TryLockError::Poisoned(_)) => Poll::Ready(Err(DecoderError::UnexpectedEnd)), + } } /// Decodes a header block using either stateless QPACK or the dynamic table. /// /// When `use_dynamic_table` is `false`, no lock is acquired and dynamic table /// references are rejected by `decode_stateless`. - pub(crate) fn poll_decode_header_limited( + pub(crate) fn poll_decode_header( &self, cx: &mut Context<'_>, encoded: &mut T, @@ -182,53 +192,59 @@ impl QpackDecoder { return Poll::Ready(decode_stateless(encoded, max_size)); } - let decoder = match self.inner.decoder.try_read() { - Ok(guard) => guard, + match self.inner.decoder.try_read() { + Ok(decoder) => { + let decoded = decoder.decode_header_limited(encoded, max_size); + drop(decoder); + self.inner.write_waker.wake(); + Poll::Ready(decoded) + } Err(TryLockError::WouldBlock) => { + // The encoder stream is updating the dynamic table. Register before + // retrying so a write-lock release cannot be missed between attempts. self.inner.read_waker.register(cx.waker()); match self.inner.decoder.try_read() { - Ok(guard) => guard, - Err(TryLockError::WouldBlock) => return Poll::Pending, - Err(TryLockError::Poisoned(_)) => { - return Poll::Ready(Err(DecoderError::UnexpectedEnd)) + Ok(decoder) => { + // The write lock was released after registration; decode now + // instead of returning Pending and waiting for another wake. + let decoded = decoder.decode_header_limited(encoded, max_size); + drop(decoder); + self.inner.write_waker.wake(); + Poll::Ready(decoded) } + Err(TryLockError::WouldBlock) => { + // Still blocked. The registered waker will be notified when + // the writer releases the decoder. + Poll::Pending + } + Err(TryLockError::Poisoned(_)) => Poll::Ready(Err(DecoderError::UnexpectedEnd)), } } - Err(TryLockError::Poisoned(_)) => return Poll::Ready(Err(DecoderError::UnexpectedEnd)), - }; - - let decoded = decoder.decode_header_limited(encoded, max_size); - drop(decoder); - self.inner.write_waker.wake(); - - Poll::Ready(decoded) + Err(TryLockError::Poisoned(_)) => Poll::Ready(Err(DecoderError::UnexpectedEnd)), + } } /// Decodes a header block and tracks decoder stream instructions for it. - pub(crate) fn poll_decode_header_limited_tracked( + pub(crate) fn poll_decode_header_tracked( &mut self, cx: &mut Context<'_>, encoded: &mut T, max_size: u64, use_dynamic_table: bool, ) -> Poll> { - let decoded = match ready!(self.poll_decode_header_limited( - cx, - encoded, - max_size, - use_dynamic_table, - )) { - Ok(decoded) => decoded, - Err(error @ DecoderError::MissingRefs(required_ref)) => { - if required_ref > 0 { - if let Some(stream_state) = &mut self.stream_state { - stream_state.cancel_on_drop = true; + let decoded = + match ready!(self.poll_decode_header(cx, encoded, max_size, use_dynamic_table,)) { + Ok(decoded) => decoded, + Err(error @ DecoderError::MissingRefs(required_ref)) => { + if required_ref > 0 { + if let Some(stream_state) = &mut self.stream_state { + stream_state.cancel_on_drop = true; + } } + return Poll::Ready(Err(error)); } - return Poll::Ready(Err(error)); - } - Err(error) => return Poll::Ready(Err(error)), - }; + Err(error) => return Poll::Ready(Err(error)), + }; if let Some(stream_state) = &mut self.stream_state { stream_state.cancel_on_drop = false; From fed08b6b7158f59ef062dccbb6bacd3c4a92dd16 Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Wed, 17 Jun 2026 14:08:25 +0800 Subject: [PATCH 22/22] perf(client,qpack): Simplify poll_decode_header invocation (#11) --- h3/src/client/stream.rs | 7 +----- h3/src/connection.rs | 6 ++--- h3/src/qpack/mod.rs | 52 +++++++++++++++++------------------------ 3 files changed, 26 insertions(+), 39 deletions(-) diff --git a/h3/src/client/stream.rs b/h3/src/client/stream.rs index e880d7d..d596bcd 100644 --- a/h3/src/client/stream.rs +++ b/h3/src/client/stream.rs @@ -127,12 +127,7 @@ where loop { // QPACK decoding advances the buffer; MissingRefs must retry from the original block. let mut encoded = encoded.clone(); - match future::poll_fn(|cx| { - self.inner - .poll_decode_header_block_tracked(cx, &mut encoded) - }) - .await - { + match future::poll_fn(|cx| self.inner.poll_decode_header(cx, &mut encoded)).await { //= https://www.rfc-editor.org/rfc/rfc9114#section-4.2.2 //# An HTTP/3 implementation MAY impose a limit on the maximum size of //# the message header it will accept on an individual HTTP message. diff --git a/h3/src/connection.rs b/h3/src/connection.rs index 28d7b6a..04ea81f 100644 --- a/h3/src/connection.rs +++ b/h3/src/connection.rs @@ -1225,7 +1225,7 @@ where // QPACK decoding advances the buffer; retry paths must keep the original block. let encoded_trailers = trailers.clone(); - let decode_result = match self.poll_decode_header_block_tracked(cx, &mut trailers) { + let decode_result = match self.poll_decode_header(cx, &mut trailers) { Poll::Ready(decode_result) => decode_result, Poll::Pending => { self.trailers = Some(encoded_trailers); @@ -1280,12 +1280,12 @@ where } #[inline(always)] - pub(crate) fn poll_decode_header_block_tracked( + pub(crate) fn poll_decode_header( &mut self, cx: &mut Context<'_>, encoded: &mut Bytes, ) -> Poll> { - self.qpack_decoder.poll_decode_header_tracked( + self.qpack_decoder.poll_decode_header( cx, encoded, self.max_field_section_size, diff --git a/h3/src/qpack/mod.rs b/h3/src/qpack/mod.rs index 6ade0cf..9fbf68e 100644 --- a/h3/src/qpack/mod.rs +++ b/h3/src/qpack/mod.rs @@ -6,7 +6,7 @@ pub use self::{ use std::{ sync::{Arc, RwLock, TryLockError}, - task::{ready, Context, Poll}, + task::{Context, Poll}, }; use bytes::{Buf, BufMut}; @@ -177,12 +177,12 @@ impl QpackDecoder { } } - /// Decodes a header block using either stateless QPACK or the dynamic table. + /// Decodes a header block and tracks decoder stream instructions for it. /// /// When `use_dynamic_table` is `false`, no lock is acquired and dynamic table /// references are rejected by `decode_stateless`. pub(crate) fn poll_decode_header( - &self, + &mut self, cx: &mut Context<'_>, encoded: &mut T, max_size: u64, @@ -192,12 +192,12 @@ impl QpackDecoder { return Poll::Ready(decode_stateless(encoded, max_size)); } - match self.inner.decoder.try_read() { + let decoded = match self.inner.decoder.try_read() { Ok(decoder) => { let decoded = decoder.decode_header_limited(encoded, max_size); drop(decoder); self.inner.write_waker.wake(); - Poll::Ready(decoded) + decoded } Err(TryLockError::WouldBlock) => { // The encoder stream is updating the dynamic table. Register before @@ -210,41 +210,33 @@ impl QpackDecoder { let decoded = decoder.decode_header_limited(encoded, max_size); drop(decoder); self.inner.write_waker.wake(); - Poll::Ready(decoded) + decoded } Err(TryLockError::WouldBlock) => { // Still blocked. The registered waker will be notified when // the writer releases the decoder. - Poll::Pending + return Poll::Pending; + } + Err(TryLockError::Poisoned(_)) => { + return Poll::Ready(Err(DecoderError::UnexpectedEnd)) } - Err(TryLockError::Poisoned(_)) => Poll::Ready(Err(DecoderError::UnexpectedEnd)), } } - Err(TryLockError::Poisoned(_)) => Poll::Ready(Err(DecoderError::UnexpectedEnd)), - } - } + Err(TryLockError::Poisoned(_)) => return Poll::Ready(Err(DecoderError::UnexpectedEnd)), + }; - /// Decodes a header block and tracks decoder stream instructions for it. - pub(crate) fn poll_decode_header_tracked( - &mut self, - cx: &mut Context<'_>, - encoded: &mut T, - max_size: u64, - use_dynamic_table: bool, - ) -> Poll> { - let decoded = - match ready!(self.poll_decode_header(cx, encoded, max_size, use_dynamic_table,)) { - Ok(decoded) => decoded, - Err(error @ DecoderError::MissingRefs(required_ref)) => { - if required_ref > 0 { - if let Some(stream_state) = &mut self.stream_state { - stream_state.cancel_on_drop = true; - } + let decoded = match decoded { + Ok(decoded) => decoded, + Err(error @ DecoderError::MissingRefs(required_ref)) => { + if required_ref > 0 { + if let Some(stream_state) = &mut self.stream_state { + stream_state.cancel_on_drop = true; } - return Poll::Ready(Err(error)); } - Err(error) => return Poll::Ready(Err(error)), - }; + return Poll::Ready(Err(error)); + } + Err(error) => return Poll::Ready(Err(error)), + }; if let Some(stream_state) = &mut self.stream_state { stream_state.cancel_on_drop = false;