Skip to content

Headers: Unit tests and alignment between protocols - #16

Merged
pgit merged 12 commits into
masterfrom
headers
Sep 17, 2026
Merged

pgit merged 12 commits into
masterfrom
headers

Conversation

@pgit

@pgit pgit commented Sep 17, 2026

Copy link
Copy Markdown
Owner

No description provided.

pgit and others added 12 commits September 17, 2026 19:49
…plete request

Requests and responses may outlive their session. The session only
remembered the latest reader and writer, and an earlier one clearing that
pointer on destruction made the session forget the later one. Operations
on a detached writer also still used the stream of the session that was
gone. The session now tracks all of its readers and writers and detaches
every one of them; a detached writer's write, submit or get_response
completes with connection_aborted.

The HTTP/1.1 client is treated as a protocol with "max concurrent
streams = 1". Interleaving requests used to corrupt the connection
silently, with every write reporting success. Now, instead of waiting,
which deadlocks when the caller is the one to finish the earlier one:

- async_submit() fails with would_block while the previous request is
  not complete (header and body written; a request without a body, by
  its framing only, is complete after submit)
- async_get_response() fails with would_block until the responses to
  earlier requests have been read completely
- after a request or response could not be completed, later submits or
  get_response calls fail with connection_aborted

Pipelining of complete requests keeps working. The rules and the design
decisions behind them are documented in README.md, "Concurrent Requests".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n members

Endpoint describes a local socket (address + fd); the drop rates are server
configuration. send_udp() and send_udp_gso() were free functions only called
by Http3ServerSession, which already owns the fd, the no_gso flag and the
config, so fold them into the session: send_udp_gso() becomes the body of
send_datagrams(), and send_udp() takes just the remote ngtcp2_addr and the
packet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Config::disable_gro skips enabling UDP_GRO on the server's UDP socket;
Config::disable_gso starts every session with no_gso_ set, so aggregated
packets go out with one sendto() each instead of a single UDP_SEGMENT
sendmsg(). The "UDP listening" log line reports both.

Single-threaded Release server, h2load over loopback, 1000 x 1MB, -c 10:

  config     download MB/s   upload MB/s
  both on         1385           1277
  GRO off         1409            752
  GSO off          565           1256
  both off         552            741

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
/eat_request responds first and drains afterwards, so h2load, which stops
uploading once the response is complete, only sends a fraction of the body
and upload benchmarks against it measure almost nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ADERS, add header tests

HTTP/1.1 copied the user's fields with message.set(), so of a field given
several times only the last value went out, in requests and responses alike.
User fields still replace defaults like User-Agent or Date.

nghttp2 closes the stream of a request HEADERS frame it cannot send, but not
that of a response: the client waited forever for it and the server's
async_write_eof() never completed. The stream is now reset instead. The
header block nghttp2 is willing to send is raised from 64 KiB to 1 MiB.

test_headers.cpp covers many, large, repeated and oversized header fields in
both directions for all three protocols.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
anyhttp::truncated() prints at most 256 bytes of a string, followed by
"... (N bytes, truncated)". All places logging sent or received header
fields use it, so a large field no longer floods the log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ceive

server::Config::max_header_size and client::Config::max_header_size bound the
header section a peer can send, 64 KiB by default; the server binary takes
--max-header-size. Neither nghttp2 nor nghttp3 enforces the limit it announces
(SETTINGS_MAX_HEADER_LIST_SIZE, SETTINGS_MAX_FIELD_SECTION_SIZE), so the header
callbacks count every field as name + value + 32 bytes themselves and stop
storing fields once the limit is exceeded. HTTP/1.1 leaves the counting to the
Beast parser's header limit.

A request beyond the limit is answered with 431 without ever reaching the
request handler; a response beyond it fails async_get_response() with
http::error::header_limit and resets the stream. HTTP/2 and HTTP/3 sessions
survive both, HTTP/1.1 connections are closed.

nghttp2 closes a connection whose peer sends more CONTINUATION frames than a
header section is allowed to need, 8 by default. That cap now follows
max_header_size, so raising the limit no longer runs into it instead.

Verified with 40 concurrent requests of 800 KB of headers each: the server's
peak RSS grows by nothing (HTTP/1.1) and 4 MB (HTTP/3) with the default limit,
against 32 MB and 25 MB with it effectively turned off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Looking up each expected field with equal_range built a fresh vector of
values per field, 200 of them for the larger tests. Flatten the actual
fields into (name, value) pairs once instead and match against that.

StrCaseEq keeps the name comparison case-insensitive, as the beast field
lookup it replaces was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The setting is advisory in both directions: nghttp2 stores the local and the
remote value but never compares a header section against either, and its own
default is unlimited. Header sections beyond max_header_size are rejected where
they arrive, so announcing the limit bought nothing but bytes on the wire.

HTTP/3 keeps announcing it, because nghttp3 writes SETTINGS_MAX_FIELD_SECTION_
SIZE into its SETTINGS frame unconditionally. Leaving that at the default would
announce an unlimited header section, in four more bytes of varint than the real
limit takes.

Also documents nghttp2_unique_ptr.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pgit
pgit merged commit 6d4f2fd into master Sep 17, 2026
4 checks passed
@pgit
pgit deleted the headers branch September 17, 2026 21:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant