Skip to content

fix(http2): reset only the stream whose request is over the body limit - #6389

Open
binaryfire wants to merge 3 commits into
swoole:masterfrom
binaryfire:fix/http2-oversized-body-stream-reset
Open

binaryfire wants to merge 3 commits into
swoole:masterfrom
binaryfire:fix/http2-oversized-body-stream-reset

Conversation

@binaryfire

@binaryfire binaryfire commented Nov 11, 2026 •

Copy link
Copy Markdown
Member

When one HTTP/2 request goes over package_max_length, the server closes the whole connection, and every other request on it fails:

  • Body over the limit. The server sends a malformed RST_STREAM: stream id 0, the ACK flag and a zero length, followed by the 4-byte error code. RST_STREAM on stream 0 is a connection error (RFC 9113 §6.4), and the client reads the error code as the next frame header. DATA the client already sent for the stream then fails with "http2 stream#1 not found", and the server closes the connection.
  • content-length over the limit. The server sends a 413, then GOAWAY, and closes the connection.
  • Refused stream with a body. A request refused by http2_max_concurrent_streams is reset with REFUSED_STREAM, then its DATA hits the same "stream not found" error.

Fix

An oversized request gets a complete 413 response, then RST_STREAM(NO_ERROR) so the client stops the upload (RFC 9113 §8.1). A refused stream still gets RST_STREAM(REFUSED_STREAM). Either way, the connection stays open:

  • DATA for a closed stream is discarded, but still counts against the connection flow-control window (RFC 9113 §5.1, §6.9). Otherwise the client runs out of connection credit and other streams stall.
  • A header block on a closed stream, such as trailers sent before the client got the reset, is decoded to keep the HPACK state, then discarded. A block with pseudo-headers is a new request on a closed stream, so it stays a connection error.
  • A content-length over the limit sends the 413, decodes the rest of the block, and resets the stream once the block ends.
  • The async server parses requests outside a coroutine, so it can't wait for room in the output buffer. If the buffer is full and the 413 or reset can't be sent, it closes the connection, as fix(http2): avoid worker crashes when the output buffer is full #6398 does for other frames sent while parsing. A lost 413 can also break header decoding for the rest of the connection.
  • http2_server_send_rst_stream() is removed; Http2Stream::reset() already builds a correct frame.

nginx does the same: it counts DATA for closed streams against the connection window before ignoring it, and sends RST_STREAM(NO_ERROR) after the response when the upload is still open.

Hypervel impact

Hypervel enables HTTP/2 by default. A gRPC message over package_max_length drops every other call on the same connection. Hypervel's regression test sends an oversized POST and a GET on one connection, and checks that the GET is answered.

Tests

request_body_limit.phpt covers all three cases in BASE, PROCESS and coroutine server modes. It checks that an oversized request gets a 413 and RST_STREAM(NO_ERROR), that a refused stream gets only RST_STREAM(REFUSED_STREAM), that another request on the connection is answered, that trailers and CONTINUATION sent after the reset still update the HPACK table, and that discarded DATA returns connection credit. It fails on master. request_body_limit_output_buffer_full.phpt fills the output buffer with a large response, then checks that each oversized case closes the connection without restarting the worker, in BASE and PROCESS modes.

Also run: the HTTP/2 server, coroutine HTTP server and HTTP/2 client suites, both new tests under Valgrind, and both new tests plus request_trailer*.phpt on a ZTS build with thread support.

When a request body went over package_max_length, the server sent a
malformed RST_STREAM: stream id 0, the ACK flag and a zero length, followed
by the 4-byte error code. Peers read that as a connection error and parsed
the error code as the next frame header. DATA the client had already sent
for the stream then failed with "stream not found" and the server closed the
connection. A content-length over the limit sent a 413 and GOAWAY and closed
the connection, and DATA for a stream refused by max_concurrent_streams also
closed it. Every other request on the connection was lost.

Now the stream gets a complete 413 response and RST_STREAM(NO_ERROR)
(RFC 9113 section 8.1), and the connection stays open:

- DATA for a closed stream is discarded, but still counts against the
  connection flow-control window (RFC 9113 sections 5.1 and 6.9).
- A header block on a closed stream, such as trailers sent before the client
  got the reset, is decoded to keep the HPACK state and then discarded. A
  block with pseudo-headers is a new request on a closed stream and is still
  a connection error.
- A content-length over the limit sends the 413, decodes the rest of the
  block and resets the stream once the block ends.
The async server parses requests outside a coroutine, so it can't wait for
output-buffer space. A 413 sent while the buffer is full is lost after its
header block has gone through the connection's HPACK encoder, and the client
can no longer decode later headers. A failed 413 now closes the connection,
like the other sends made while parsing.

Checking only the RST_STREAM that follows isn't enough. In PROCESS mode the
reactor can drain the buffer between the two sends, and when the header
block isn't finished the reset waits for a later CONTINUATION.
A large response fills the server's output buffer, then the client sends a
content-length over the limit, the same in an unfinished header block, or
DATA over the limit. Each must close the connection without restarting the
worker, in BASE and PROCESS modes.
@binaryfire
binaryfire force-pushed the fix/http2-oversized-body-stream-reset branch from e8ce96d to d25f196 Compare November 12, 2026 04:48
@codecov

codecov Bot commented Nov 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.07%. Comparing base (9cd45e0) to head (d25f196).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6389      +/-   ##
==========================================
- Coverage   84.08%   84.07%   -0.01%     
==========================================
  Files         115      115              
  Lines       20237    20237              
  Branches     3637     3638       +1     
==========================================
- Hits        17016    17015       -1     
- Misses       3221     3222       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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