Replaces the buffer-the-whole-body-then-dispatch-handler path with an optional pull-based streaming API. Unblocks HttpArena upload profile (currently ~20 GiB peak at 1000 concurrent 20 MiB uploads) and enables proxy / direct-to-storage / incremental-hash use cases.
Full design lives in docs/PLAN_STREAMING_INGRESS.md. Tracking issue body summarizes the contract.
Userland API
TrueAsync\HttpRequest:
readBody(int \$maxLen = 65536): ?string — null = EOF
readBodyChunks(int \$maxLen = 65536): ?array — zero-copy variant
TrueAsync\HttpServerConfig:
setMaxBodyQueueBytes(int): static — per-stream watermark, default 256 KiB
Mode trichotomy (first call wins, mismatched calls throw LogicException):
| first call |
mode |
getBody() / awaitBody() |
buffered (current behavior) |
readBody*() |
streaming |
getFiles() / getPost() |
multipart-parsed |
Internals
- Per-request
zend_async_channel_t (ZEND_ASYNC_NEW_CHANNEL(16, false, false)) — backed by existing circular_buffer in ext-async, awaitable for free.
- Double byte limit: per-stream watermark (backpressure) + cumulative
max_body_size (413).
- Per-transport backpressure:
- H1:
llhttp_pause / llhttp_resume with 50% low-water threshold (anti-chatter)
- H2: defer
nghttp2_session_consume until drain; advertise SETTINGS_MAX_FRAME_SIZE = watermark
- H3: defer
ngtcp2_conn_extend_max_stream_offset; initial_max_stream_data sized to watermark
Expected impact
20 MiB upload × 1000 concurrent streams:
- before: ~20 GiB peak RSS
- after: ~320 MiB peak RSS — 60× reduction
For handlers that keep calling getBody() — zero overhead added.
PR breakdown
| PR |
Scope |
Blocks |
| #1 |
setMaxBodyQueueBytes + plumbing + PHPT |
— |
| #2 |
http_request_t body-mode trichotomy; lazy multipart activation |
#1 |
| #3 |
readBody() + H1 integration |
#2 |
| #4 |
H2 integration + SETTINGS_MAX_FRAME_SIZE |
#3 |
| #5 |
H3 integration |
#3 |
| #6 |
readBodyChunks() |
#3 |
| #7 |
HttpArena entry.php rewrite for /upload + RSS measurement |
#3–#6 |
PRs #4 and #5 are independent and can land in either order.
Out of scope
Streaming response body (already shipped — see commit history), iterator/generator wrapper, PSR-7 StreamInterface wrapper, C-level direct channel consumers, per-connection aggregate watermark.
See docs/PLAN_STREAMING_INGRESS.md §5 for full edge-case matrix and §6 for performance notes.
Replaces the buffer-the-whole-body-then-dispatch-handler path with an optional pull-based streaming API. Unblocks HttpArena
uploadprofile (currently ~20 GiB peak at 1000 concurrent 20 MiB uploads) and enables proxy / direct-to-storage / incremental-hash use cases.Full design lives in
docs/PLAN_STREAMING_INGRESS.md. Tracking issue body summarizes the contract.Userland API
TrueAsync\HttpRequest:readBody(int \$maxLen = 65536): ?string— null = EOFreadBodyChunks(int \$maxLen = 65536): ?array— zero-copy variantTrueAsync\HttpServerConfig:setMaxBodyQueueBytes(int): static— per-stream watermark, default 256 KiBMode trichotomy (first call wins, mismatched calls throw
LogicException):getBody()/awaitBody()readBody*()getFiles()/getPost()Internals
zend_async_channel_t(ZEND_ASYNC_NEW_CHANNEL(16, false, false)) — backed by existing circular_buffer in ext-async, awaitable for free.max_body_size(413).llhttp_pause/llhttp_resumewith 50% low-water threshold (anti-chatter)nghttp2_session_consumeuntil drain; advertiseSETTINGS_MAX_FRAME_SIZE = watermarkngtcp2_conn_extend_max_stream_offset;initial_max_stream_datasized to watermarkExpected impact
20 MiB upload × 1000 concurrent streams:
For handlers that keep calling
getBody()— zero overhead added.PR breakdown
setMaxBodyQueueBytes+ plumbing + PHPThttp_request_tbody-mode trichotomy; lazy multipart activationreadBody()+ H1 integrationreadBodyChunks()entry.phprewrite for/upload+ RSS measurementPRs #4 and #5 are independent and can land in either order.
Out of scope
Streaming response body (already shipped — see commit history), iterator/generator wrapper, PSR-7
StreamInterfacewrapper, C-level direct channel consumers, per-connection aggregate watermark.See
docs/PLAN_STREAMING_INGRESS.md§5 for full edge-case matrix and §6 for performance notes.