Skip to content

Respond with 400 for malformed HTTP/2 request headers#364

Open
apoorvdarshan wants to merge 1 commit into
pgjones:mainfrom
apoorvdarshan:fix-h2-header-whitespace-400
Open

Respond with 400 for malformed HTTP/2 request headers#364
apoorvdarshan wants to merge 1 commit into
pgjones:mainfrom
apoorvdarshan:fix-h2-header-whitespace-400

Conversation

@apoorvdarshan

Copy link
Copy Markdown

Summary

In HTTP/2 mode, a request with a malformed header value — for example a value with leading or trailing whitespace, which RFC 9113 8.2.1 forbids — causes the whole connection to be torn down with a GOAWAY and no HTTP response. The client sees an abnormal connection failure instead of the 400 (Bad Request) the RFC recommends. This fixes issue #348.

The h2 library raises a connection-level h2.exceptions.ProtocolError while parsing such a request. H2Protocol.handle() caught it and only flushed the queued GOAWAY before closing, so no response ever reached the offending stream.

Fix

src/hypercorn/protocol/h2.py: on ProtocolError, send a 400 (Bad Request) for the offending stream before flushing and closing (RFC 9113 8.2.1).

Because h2 has already transitioned the connection to a closed state, its send API can no longer be used, so a minimal HEADERS frame is written directly. The header block is a single fully-indexed HPACK reference to the static-table entry :status: 400 (index 12, byte 0x8c), so no encoder state is required and it is safe to emit on a fresh connection. The offending stream is taken from connection.highest_inbound_stream_id; when that is 0 (a framing-level error with no request stream, e.g. garbage bytes) the behaviour is unchanged — the connection is simply closed.

Tests

  • Added test_protocol_handle_malformed_request_sends_400, which drives a real h2 client through H2Protocol, injects a hand-crafted HEADERS frame with a whitespace-surrounded header value, and asserts the client receives a :status: 400 response (and the connection is then closed). The test fails on the pristine code (client sees only ConnectionTerminated) and passes with the fix.
  • The existing test_protocol_handle_protocol_error (garbage input → just Closed()) still passes, confirming the framing-error path is unchanged.
  • Full suite green (197 passed); black, isort, flake8, and mypy are clean on the changed files.

Added a CHANGELOG entry.

Disclosure: prepared with AI assistance; reviewed and verified locally.

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