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/.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/examples/client.rs b/examples/client.rs index f91a340..5d4bbf7 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; @@ -26,7 +27,24 @@ struct Opt { #[structopt(name = "keylogfile", long)] pub key_log_file: bool, - #[structopt()] + #[structopt( + long, + short = "n", + default_value = "1", + help = "Number of concurrent requests to send" + )] + pub requests: usize, + + #[structopt(long, help = "QPACK max table capacity")] + pub qpack_max_table_capacity: Option, + + #[structopt(long, help = "QPACK blocked streams")] + pub qpack_blocked_streams: Option, + + #[structopt( + default_value = "https://cloudflare-quic.com", + help = "URI of the server to connect to" + )] pub uri: String, } @@ -41,6 +59,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::()?; @@ -112,7 +134,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, send_request) = h3::client::builder() + .max_field_section_size(262144u64) + .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) + .await?; let drive = async move { return Err::<(), ConnectionError>(future::poll_fn(|cx| driver.poll_close(cx)).await); @@ -125,32 +154,51 @@ async fn main() -> Result<(), Box> { // So we "move" it. // vvvv let request = async move { - info!("sending request ..."); + let mut requests = Vec::with_capacity(opt.requests); + + for request_id in 0..opt.requests { + let uri = uri.clone(); + let mut send_request = send_request.clone(); - let req = http::Request::builder().uri(uri).body(()).unwrap(); + requests.push(async move { + info!(request_id, "sending request ..."); - // sending request results in a bidirectional stream, - // which is also used for receiving response - let mut stream = send_request.send_request(req).await?; + let req = http::Request::builder().uri(uri).body(()).unwrap(); - // finish on the sending side - stream.finish().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!("receiving response ..."); + // finish on the sending side + stream.finish().await?; - let resp = stream.recv_response().await?; + info!(request_id, "receiving response ..."); - info!("response: {:?} {}", resp.version(), resp.status()); - info!("headers: {:#?}", resp.headers()); + let resp = stream.recv_response().await?; - // `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(); + info!( + request_id, + "response: {:?} {}", + resp.version(), + resp.status() + ); + info!(request_id, "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>(()) }; 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-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/client/builder.rs b/h3/src/client/builder.rs index 99c4840..abc3280 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. - pub fn qpack_max_table_capacity(&mut self, value: u64) -> &mut Self { - self.config.settings.qpack_max_table_capacity = value; + /// 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: T) -> &mut Self { + self.config.settings.qpack_max_table_capacity = value.into(); 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. - pub fn qpack_blocked_streams(&mut self, value: u64) -> &mut Self { - self.config.settings.qpack_blocked_streams = value; + /// 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: T) -> &mut Self { + self.config.settings.qpack_blocked_streams = value.into(); self } @@ -166,16 +168,16 @@ 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 send_request = SendRequest { open, conn_state, - max_field_section_size, + 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, + send_grease_frame: self.config.send_grease, _buf: PhantomData, }; diff --git a/h3/src/client/connection.rs b/h3/src/client/connection.rs index 82b800e..de7ce07 100644 --- a/h3/src/client/connection.rs +++ b/h3/src/client/connection.rs @@ -21,7 +21,7 @@ use crate::{ }, frame::FrameStream, proto::{frame::Frame, headers::Header, push::PushId}, - qpack, + qpack::{self, QpackDecoder}, quic::{self, StreamId}, shared_state::{ConnectionState, SharedState}, stream::{self, BufRecvStream}, @@ -113,6 +113,8 @@ where { pub(super) open: T, pub(super) conn_state: Arc, + pub(super) qpack_decoder: QpackDecoder, + 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, @@ -220,6 +222,8 @@ where self.max_field_section_size, self.conn_state.clone(), self.send_grease_frame, + self.qpack_decoder.clone(), + self.use_qpack_dynamic_table, ), }; // send the grease frame only once @@ -239,6 +243,8 @@ where Self { conn_state: self.conn_state.clone(), + qpack_decoder: self.qpack_decoder.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(), @@ -292,6 +298,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, @@ -317,6 +324,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, @@ -377,6 +385,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 +404,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_stream(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..d596bcd 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,43 @@ 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 qpack::decode_stateless(encoded, self.inner.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)) => { - 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 { + // 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(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. + 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/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 02b9689..251f951 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}; @@ -30,7 +31,8 @@ use crate::{ varint::VarInt, }, qpack, - quic::{self, RecvStream, SendStream, StreamErrorIncoming}, + qpack::{QpackDecoder, QpackDecoderEvent}, + quic::{self, RecvStream, SendStream, SendStreamUnframed, StreamErrorIncoming}, shared_state::{ConnectionState, SharedState}, stream::{self, AcceptRecvStream, AcceptedRecvStream, BufRecvStream, UniStreamHeader}, webtransport::SessionId, @@ -64,6 +66,8 @@ where B: Buf, { decoder_send: Option, + decoder_send_buf: BytesMut, + decoder_events_recv: mpsc::UnboundedReceiver, decoder_recv: Option>, encoder_send: Option, encoder_recv: Option>, @@ -81,6 +85,10 @@ where control_send: C::SendStream, control_recv: Option>, qpack_streams: QpackStreams, + + /// QPACK decoder state for field sections that reference the dynamic table. + qpack_decoder: QpackDecoder, + /// 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. @@ -132,7 +140,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 @@ -193,6 +201,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, @@ -289,8 +302,11 @@ 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: BytesMut::new(), + decoder_events_recv, decoder_recv: None, encoder_send: qpack_encoder.ok(), encoder_recv: None, @@ -301,12 +317,27 @@ where //# The //# sender MUST NOT close the control stream, and the receiver MUST NOT //# request that the sender close the control stream. + 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), + ) + .map_err(|err| { + conn.close_raw_connection_with_h3_error(InternalConnectionError::new( + Code::QPACK_DECOMPRESSION_FAILED, + format!("invalid QPACK decoder configuration: {}", err), + )) + })?, + decoder_events_send, + ); + let mut conn_inner = Self { shared, conn, control_send, control_recv: None, qpack_streams, + qpack_decoder, handled_connection_error: None, pending_recv_streams: Vec::with_capacity(3), got_peer_settings: false, @@ -405,7 +436,7 @@ where .map_err(|e| self.handle_connection_error(e))? { self.pending_recv_streams - .push(Some(AcceptRecvStream::new(stream))) + .push(Some(AcceptRecvStream::new(stream))); } for stream in self.pending_recv_streams.iter_mut().filter(|s| s.is_some()) { @@ -446,6 +477,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 +493,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 +547,190 @@ where Ok(()) } + #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))] + pub fn poll_qpack_encoder_stream( + &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_send_before = self.qpack_streams.decoder_send_buf.len(); + let decoder = self.qpack_decoder.clone(); + + 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; + } + }; + + 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; + } + } + + 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, + { + self.drain_qpack_decoder_events(); + + let Some(decoder_send) = self.qpack_streams.decoder_send.as_mut() else { + if !self.qpack_streams.decoder_send_buf.has_remaining() { + return Poll::Ready(Ok(())); + } + 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(())) + } + + 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. #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))] pub fn poll_control( @@ -526,13 +757,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, @@ -548,12 +773,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, @@ -665,35 +885,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 @@ -702,13 +920,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(()); } @@ -722,34 +940,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(()); } @@ -779,13 +1039,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(()); } @@ -804,6 +1064,11 @@ 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() + } } #[allow(missing_docs)] @@ -812,22 +1077,37 @@ pub struct RequestStream { pub(super) trailers: Option, pub(super) conn_state: Arc, pub(super) max_field_section_size: u64, + pub(super) qpack_decoder: QpackDecoder, + use_qpack_dynamic_table: bool, send_grease_frame: bool, } -impl RequestStream { +impl RequestStream +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: QpackDecoder, + use_qpack_dynamic_table: bool, ) -> Self { + let qpack_decoder = if use_qpack_dynamic_table { + qpack_decoder.track_stream(stream.id().into_inner(), conn_state.clone()) + } else { + qpack_decoder + }; + Self { stream, conn_state, max_field_section_size, trailers: None, + qpack_decoder, + use_qpack_dynamic_table, send_grease_frame: grease, } } @@ -960,15 +1240,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, @@ -978,35 +1257,50 @@ 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 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(), - }, - ))) - } - }; + // 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(cx, &mut trailers) { + Poll::Ready(decode_result) => decode_result, + Poll::Pending => { + self.trailers = Some(encoded_trailers); + return Poll::Pending; + } + }; + + 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. + Err(qpack::DecoderError::HeaderTooLong(cancel_size)) => { + return Poll::Ready(Err(StreamError::HeaderTooBig { + actual_size: cancel_size, + 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(encoded_trailers); + self.waker().register(cx.waker()); + return Poll::Pending; + } + 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 +1320,20 @@ where pub fn stop_sending(&mut self, err_code: Code) { self.stream.stop_sending(err_code); } + + #[inline(always)] + pub(crate) fn poll_decode_header( + &mut self, + cx: &mut Context<'_>, + encoded: &mut Bytes, + ) -> Poll> { + self.qpack_decoder.poll_decode_header( + cx, + encoded, + self.max_field_section_size, + self.use_qpack_dynamic_table, + ) + } } impl RequestStream @@ -1135,6 +1443,8 @@ where trailers: None, conn_state: self.conn_state.clone(), max_field_section_size: 0, + qpack_decoder: self.qpack_decoder.clone(), + use_qpack_dynamic_table: false, send_grease_frame: self.send_grease_frame, }, RequestStream { @@ -1142,6 +1452,8 @@ where trailers: self.trailers, conn_state: self.conn_state, max_field_section_size: self.max_field_section_size, + qpack_decoder: self.qpack_decoder, + use_qpack_dynamic_table: self.use_qpack_dynamic_table, send_grease_frame: self.send_grease_frame, }, ) 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/frame.rs b/h3/src/frame.rs index 00bde62..bde5c11 100644 --- a/h3/src/frame.rs +++ b/h3/src/frame.rs @@ -70,8 +70,7 @@ where ); loop { - let end = self.try_recv(cx)?; - + // 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; @@ -82,7 +81,7 @@ where Poll::Ready(Ok(frame)) } Some(frame) => Poll::Ready(Ok(Some(frame))), - None => match end { + 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, @@ -173,8 +172,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> { @@ -320,7 +323,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}; @@ -590,11 +599,32 @@ 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)] struct FakeRecv { chunks: VecDeque, + reads: Arc, } impl FakeRecv { @@ -602,6 +632,10 @@ mod tests { self.chunks.push_back(buf); self } + + fn reads(&self) -> Arc { + self.reads.clone() + } } impl RecvStream for FakeRecv { @@ -611,6 +645,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 15cf68d..05e083b 100644 --- a/h3/src/qpack/decoder.rs +++ b/h3/src/qpack/decoder.rs @@ -83,9 +83,28 @@ 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 + #[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())?; @@ -100,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/qpack/mod.rs b/h3/src/qpack/mod.rs index 2e33747..9fbf68e 100644 --- a/h3/src/qpack/mod.rs +++ b/h3/src/qpack/mod.rs @@ -1,9 +1,20 @@ pub use self::{ - decoder::{decode_stateless, Decoded, DecoderError}, + decoder::{ack_header, decode_stateless, stream_canceled, Decoded, Decoder, DecoderError}, encoder::{encode_stateless, EncoderError}, field::HeaderField, }; +use std::{ + sync::{Arc, RwLock, TryLockError}, + task::{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; mod field; @@ -37,3 +48,211 @@ 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 QpackDecoderInner { + decoder: RwLock, + decoder_events: mpsc::UnboundedSender, + 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. +pub(crate) struct QpackDecoder { + inner: Arc, + stream_state: Option, +} + +/// Per-stream QPACK decoder state used to emit decoder stream instructions. +struct QpackDecoderStreamState { + stream_id: u64, + shared: Arc, + cancel_on_drop: bool, +} + +impl Clone for QpackDecoder { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + stream_state: None, + } + } +} + +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 QpackDecoder { + /// Creates a new [`QpackDecoder`] instance. + #[inline(always)] + 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, + } + } + + /// Returns a stream-scoped decoder handle that tracks decoder stream instructions. + #[inline(always)] + pub(crate) fn track_stream(mut self, stream_id: u64, shared: Arc) -> Self { + self.stream_state = Some(QpackDecoderStreamState { + stream_id, + shared, + // Until the header block is successfully decoded, dropping the tracked + // stream abandons it and must notify the peer encoder. + cancel_on_drop: true, + }); + self + } + + /// Processes bytes received from the peer QPACK encoder stream. + /// + /// 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<'_>, + read: &mut R, + write: &mut W, + ) -> Poll> { + 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(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(_)) => Poll::Ready(Err(DecoderError::UnexpectedEnd)), + } + } + + /// 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( + &mut self, + cx: &mut Context<'_>, + encoded: &mut T, + max_size: u64, + use_dynamic_table: bool, + ) -> Poll> { + if !use_dynamic_table { + return Poll::Ready(decode_stateless(encoded, max_size)); + } + + 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(); + 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(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(); + decoded + } + Err(TryLockError::WouldBlock) => { + // Still blocked. The registered waker will be notified when + // the writer releases the decoder. + return Poll::Pending; + } + Err(TryLockError::Poisoned(_)) => { + return Poll::Ready(Err(DecoderError::UnexpectedEnd)) + } + } + } + Err(TryLockError::Poisoned(_)) => return Poll::Ready(Err(DecoderError::UnexpectedEnd)), + }; + + 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)), + }; + + 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/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/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 0b3588a..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,6 +153,8 @@ where self.max_field_section_size, self.shared.clone(), self.send_grease_frame, + self.qpack_decoder, + false, ), }; 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() {