Conversation
Coverage Report for CI Build 0Coverage increased (+0.3%) to 90.805%Details
Uncovered Changes
Coverage Regressions2 previously-covered lines in 1 file lost coverage.
Coverage Stats
馃挍 - Coveralls |
While waiting for CONTINUATION frames, the client compared the size of the buffered compressed header block with its advertised SETTINGS_MAX_HEADER_LIST_SIZE, assuming a compressed block is never larger than the header list it decodes to. Huffman coding can make a string longer than its octets (RFC 7541 5.2), so a valid block whose decoded list is under the limit could exceed it on the wire. With a limit of 1,000 bytes, a 475-byte header list that encodes to 1,307 bytes was accepted in a single HEADERS frame and closed the connection with PROTOCOL_ERROR when split over HEADERS and CONTINUATION. The limit is enforced on the decoded header list, and the fragments buffered before END_HEADERS are bounded by an upper bound on the encoded size of a header list within the limit, assuming minimal integer representations (RFC 7541 5.1) and at most two dynamic table size updates (RFC 7541 4.2). HPACK decoders accept redundant zero continuation bytes in integers and any number of size updates, so no finite bound covers every block that decodes to such a list. The fragment that carries END_HEADERS isn't counted, and the complete block is then judged by its decoded size. A Huffman-coded octet takes at most 30 bits (RFC 7541 Appendix B). A minimal integer below 2^32 takes at most 6 bytes, so a field has at most 13 bytes of prefixes and integers (a representation byte, the name length and the value length) and less than 2 bytes of Huffman padding, 15 bytes in total. That's less than the 120 bytes its 32-byte overhead (RFC 9113 6.5.2) contributes at the 30/8 ratio. The two size updates take at most 6 bytes each. The bound is max_header_list_size * 30 / 8 + 12 bytes, and a peer that buffers more than that gets a connection error.
The docs said a header block larger than :max_header_list_size closes
the connection. A decoded response header list over the limit resets the
request with a PROTOCOL_ERROR stream error and returns a
{:max_header_list_size_exceeded, size, max_size} error, and promised
request headers over the limit reset the promised stream with
REFUSED_STREAM. The connection stays open in both cases.
Only the header block fragments buffered while waiting for END_HEADERS
are a connection error, when they're over an upper bound on the encoded
size of a header list within the limit. The fragment that carries
END_HEADERS isn't counted, and the complete block is then judged by its
decoded size.
The docs also say that the server setting is :infinity until the server
sends it, and that the client setting must be an integer, since
put_settings/2 and the :client_settings option raise on :infinity.
A PUSH_PROMISE was accepted as long as its :scheme and :authority were non-empty and its :path started with "/", so promises with :authority "user@localhost" or "localhost:abc", :path "/a b" or "/a#fragment", or :scheme "1https" were returned as :push_promise responses. RFC 9113 8.4.1 has the client reset a promised stream whose request is malformed with PROTOCOL_ERROR. The :scheme is checked against the RFC 3986 3.1 scheme grammar. The :authority is parsed as RFC 3986 3.2 [ userinfo "@" ] host [ ":" port ], where the host is an IP-literal or a reg-name and the port is zero or more digits. An IP-literal is an IPvFuture or an IPv6 address, optionally followed by "%25" and an RFC 6874 zone ID made of unreserved and percent-encoded characters. The IPv6 address must be made of HEXDIG, ":" and "." characters and is then checked with :inet.parse_ipv6strict_address/1, which on its own accepts a zone ID after a bare "%" and signs in embedded IPv4 octets. RFC 9113 8.3.1 forbids userinfo for the http and https schemes, and RFC 9110 4.2.1 and 4.2.2 forbid an empty host for them, so ":443" or "@localhost" is rejected for those schemes. Both checks compare the scheme case-insensitively, and other schemes keep accepting userinfo and an empty host. An :authority that is entirely empty is rejected for every scheme, as before. The :path is checked as an RFC 9110 absolute-path with an optional query (RFC 9113 8.3.1), made of pchar, "/" and "?" characters with valid percent-encoding.
The client :max_header_list_size setting is always an integer: it defaults to 256 KB, and the validation in connect/4 and put_settings/2 raises an ArgumentError for any other value, including :infinity. The branches that handled an :infinity client value when measuring decoded header lists and buffered header blocks could never run, so they're removed. The server's SETTINGS_MAX_HEADER_LIST_SIZE is still :infinity until the server sends it, and its handling is unchanged. Tests pin the validation that rejects :infinity for the client setting in connect/4 and put_settings/2.
The check for 204 and 304 responses, which must not have content, came before the check for 2xx responses to CONNECT. A CONNECT request answered with 204 was treated as a response without content, so the first DATA frame on the tunnel failed the request with a protocol error. RFC 9110 9.3.6 and RFC 9113 8.5 make any 2xx response to CONNECT establish a tunnel, so the CONNECT case is checked first and DATA frames after a 204 are delivered as tunnel data.
ericmj
force-pushed
the
http2-followups
branch
from
September 28, 2026 15:00
651b3cd to
f707415
Compare
ericmj
marked this pull request as ready for review
September 28, 2026 16:23
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Five fixes to HTTP/2 header and push handling, one commit each, meant to be rebase-merged rather than squashed.
max_header_list_sizeitself. Huffman coding can make a block larger than the list it decodes to (RFC 7541 5.2), so a list within the limit was a connection error when split across frames and accepted in one frame. The bound is nowmax_header_list_size * 30 / 8 + 12, an upper bound on the encoded size of a list within the limit when integers are minimally encoded and there are at most two table size updates, which is what a conforming encoder sends. The fragment that ends the block isn't counted. The size limit itself is enforced on the decoded list.:max_header_list_sizedocs said an oversized header block closes the connection. They now describe the stream errors for oversized responses and pushes, and the buffered-block bound. The docs also say the client value can't be:infinity.:scheme,:authorityand:pathchecked against RFC 3986, and userinfo in:authorityis rejected for http and https (RFC 9113 8.3.1). Invalid ones reset the promised stream.max_header_list_sizeis always an integer, so the:infinitybranches for it are removed.