Skip to content

fix(http2): handle CONTINUATION frames and padding - #6399

Open
binaryfire wants to merge 2 commits into
swoole:masterfrom
binaryfire:fix/http2-padding-continuation
Open

binaryfire wants to merge 2 commits into
swoole:masterfrom
binaryfire:fix/http2-padding-continuation

Conversation

@binaryfire

Copy link
Copy Markdown
Member

Swoole\Coroutine\Http2\Client can only read a header block that arrives in a single, unpadded HEADERS frame, and both the client and the server keep the padding of DATA frames in the body.

Client

  • A server has to split a header block larger than the client's 16 KB max frame size into HEADERS and CONTINUATION frames (RFC 9113 §4.3), and nginx does this for a response with many large headers. The client ignores CONTINUATION frames and decodes every HEADERS frame as a complete block, so recv() fails with -523 (HPACK error).
  • With the PADDED flag, the pad length and padding of a HEADERS frame go to the HPACK decoder, so responses from a peer that pads (such as nghttpd -b 255) fail the same way. In DATA frames they end up in the response body.
  • With the PRIORITY flag, the client skips 5 bytes without checking the frame length. A shorter HEADERS frame makes the decoder read past the end of the frame (Valgrind reports invalid reads).

Header block fragments are now decoded as they arrive, with END_HEADERS marking the last one, the same way the server does it. The client waits for END_HEADERS before it finishes a header block, and keeps END_STREAM from the HEADERS frame that started it. Padding is removed from HEADERS and DATA frames. DATA padding still counts toward flow control. The connection is closed with PROTOCOL_ERROR or FRAME_SIZE_ERROR on:

  • a frame other than a CONTINUATION on the same stream inside an unfinished header block
  • a CONTINUATION frame outside one
  • padding that doesn't fit in the frame
  • a HEADERS frame too short for its priority fields

Server

The server already removes the padding from HEADERS frames but not from DATA frames, so when a client pads DATA (such as nghttp -b 255) the padding ends up in $request->rawContent(). DATA is now handled the same way as HEADERS. #6389 also changes this DATA case, so whichever lands second needs a small rebase.

Tests

continuation_frames.phpt and padded_frames.phpt drive the client from a raw socket peer, including the error cases and a WINDOW_UPDATE check that padding still counts. padded_data.phpt covers the server in BASE, PROCESS and coroutine mode. All three fail on master. I also checked the client against nginx (large headers) and nghttpd -b 255 and the server against nghttp -b 255, and ran the new tests under Valgrind and on a ZTS thread build, plus a THREAD mode server probe.

Hypervel's HTTP/2 and gRPC clients use Coroutine\Http2\Client, and this broke its CONTINUATION regression test.

@codecov

codecov Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Codecov Report

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

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6399      +/-   ##
==========================================
- Coverage   84.08%   84.03%   -0.05%     
==========================================
  Files         115      115              
  Lines       20237    20237              
  Branches     3637     3637              
==========================================
- Hits        17016    17007       -9     
- Misses       3221     3230       +9     

☔ 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.

The HTTP/2 client only parsed a header block sent in one unpadded HEADERS
frame:

- CONTINUATION frames were ignored and every HEADERS frame was inflated as
  a complete block. A server must split a header block larger than the
  client's 16 KB max frame size, and nginx does, so those responses failed
  with an HPACK error.
- The pad length and padding of a PADDED HEADERS frame went to HPACK.
- The PRIORITY fields were skipped without a length check, so a HEADERS
  frame shorter than 5 bytes made the decoder read past the buffer.
- The pad length and padding of a PADDED DATA frame ended up in the body.

Header block fragments are now inflated as they arrive, with END_HEADERS
marking the last one, as the server does. The response is returned once
the block is complete, using END_STREAM from the HEADERS frame. Any other
frame inside a block, or a CONTINUATION frame outside one, closes the
connection with PROTOCOL_ERROR (RFC 9113 section 6.10).

Padding is removed from HEADERS and DATA frames. Padding that doesn't fit
in the frame, or a HEADERS frame too short for its priority fields, closes
the connection. Flow control still counts the whole DATA frame.
The HTTP/2 server removed the padding from HEADERS frames but not from
DATA frames, so when a client padded DATA (nghttp -b, for example) the
pad length byte and the padding ended up in the request body.

Remove it the same way as for HEADERS, and close the connection when the
padding doesn't fit in the frame (RFC 9113 section 6.1). Flow control
still counts the whole frame.
@binaryfire
binaryfire force-pushed the fix/http2-padding-continuation branch from 82cad71 to 89651ea Compare November 12, 2026 04:17
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