Skip to content

fix core: don't depend on HTTP/2.0 pseudo-header field order#1302

Open
SSE4 wants to merge 1 commit into
userver-framework:developfrom
SSE4:h2-pseudo-header-order-fix
Open

fix core: don't depend on HTTP/2.0 pseudo-header field order#1302
SSE4 wants to merge 1 commit into
userver-framework:developfrom
SSE4:h2-pseudo-header-order-fix

Conversation

@SSE4

@SSE4 SSE4 commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

The server matches a request against its handlers as soon as the :path
pseudo-header is decoded, using whatever method has been parsed by that point.
RFC 9113 §8.3 doesn't fix an order among pseudo-header fields, and clients
disagree: curl sends :method first, h2load sends :path first. As a result
every h2load request gets 405 Method Not Allowed — matching runs with the
default HttpMethod::kUnknown and the cached result is never revisited:

$ h2load -n 1 -c 1 -v http://localhost:8080/ping
[stream_id=1] :status: 405

This matches the handler once both :method and :path are decoded. Since
pseudo-header fields precede all regular fields, that is the earliest correct
moment in every order: the per-handler request limits then cover the rest of
the header block, and a doomed request (405/404) is known as early as
possible — no resource-usage regression versus the old eager match. The
remainder of the block still has to be decoded either way (HPACK is stateful;
abandoning a block midway would desync the connection) and is bounded by
max_headers_size. A call at end-of-header-block (OnFrameRecv, fires after
all CONTINUATION frames) remains as a safety net; matching can't be postponed
to FinalizeRequest because the per-handler limits must be installed before
the request body's DATA frames arrive.

Tested with two new parametrized tests in the http2server functional suite:
the same request over four pseudo-header orders (h2load's exact order
included), as GET and as POST with a request body — the latter checks that
the per-handler limits are installed before the body arrives. Without the fix
the four :path-before-:method cases fail with 405; with it all pass. The
rest of the http2server suite (h2spec included) and the http2/connection unit
tests pass unchanged. Also verified with stock h2load under load: 2M
requests, 100% 200.

closes: #1300

The server matched a request against its handlers as soon as the :path
pseudo-header field was decoded, using whatever method had been parsed
up to that point. RFC 9113 8.3 fixes no order among the pseudo-header
fields, so a client that serializes :path before :method (h2load does)
got 405 Method Not Allowed for every request: matching ran with the
default HttpMethod::kUnknown and the cached match result was never
revisited.

Match the handler once both :method and :path are decoded instead.
Since all pseudo-header fields precede the regular ones, this is the
earliest correct moment in every order: the per-handler request limits
then cover the remaining header fields of the block, and a doomed
request (405/404) is known as early as possible. The remainder of the
block still has to be decoded either way - HPACK is stateful and a
block cannot be abandoned midway without desyncing the connection - and
is bounded by max_headers_size.

A call at the end of the header block (on_frame_recv fires after all
CONTINUATION frames) remains as a safety net for requests that never
completed the target. Matching cannot happen later, in request
finalization: the per-handler limits must be installed before the DATA
frames of the request body arrive.
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.

HTTP/2 requests are rejected with 405 depending on pseudo-header field order

1 participant