feat(core): add HTTP/2 flow-controlled bodies

This commit is contained in:
Zakaria El Orche
2026-08-13 19:00:19 +00:00
parent c96d51f7ea
commit 8d5340a0b4
21 changed files with 1679 additions and 101 deletions
+48
View File
@@ -0,0 +1,48 @@
# HTTP/2 bodies and flow control
HTTP/2 applies flow control independently to the connection and to every stream. Flash advertises
a 1 MiB receive window at both levels and sends WINDOW_UPDATE only after the application has
consumed at least half a window. A DATA frame decrements both windows by its complete payload
length, including the pad-length byte and padding; only its unpadded data reaches the handler.
## Request bodies
Known bodies up to 64 KiB remain in one reusable contiguous stream buffer. Their handler is
dispatched at END_STREAM, and `RequestBody.bytes()` performs the only allocation: the byte array
returned to application code. For a 1,024-byte body JMH reports exactly 1,040 B/op, the array plus
its object header, with no framework allocation around it.
Larger or unknown-length bodies dispatch after request headers. DATA is copied out of the frame
reader into a connection-owned pool of 64 reusable 16 KiB buffers. Small adjacent frames coalesce
inside a buffer, so the pool is bounded by bytes rather than frame count. The existing
`RequestBody.stream()` blocks only the handler's virtual thread when data is absent. Buffers return
to the pool as reads consume them, and that consumption reopens both receive windows. If a handler
does not read its body, the normal post-handler drain performs the same bounded consumption.
The connection window and pool both cover exactly 1 MiB, so the peer can never hold more credit
than the server can store before backpressure takes effect. Per-stream accepted body bytes remain
bounded by `MAX_REQUEST_BODY_SIZE`. Declared content length is parsed without a String and checked
against the unpadded DATA total at END_STREAM.
## Responses
Fixed byte arrays, known-length streams and unknown-length streams all use one resumable
`Http2ResponseWriter`. It emits DATA frames no larger than the peer's frame limit, the available
connection window, the available stream window and the reusable 16 KiB relay buffer. A
WINDOW_UPDATE schedules the stream on the shared virtual-thread executor; application streams are
never read by the demultiplexer.
`Response.chunked(InputStream)` means unknown-length streaming at the application API. HTTP/2 has
no chunked transfer coding, so Flash emits ordinary DATA followed by END_STREAM and never sends a
`transfer-encoding` field. `Response.stream(InputStream, length)` emits `content-length` and fails
the stream if the source ends before that length.
## Verification
- A real Java HTTP/2 client uploads and downloads 100 MiB over TLS; both directions are validated
byte-for-byte without materializing the test payload.
- A synthetic 100 MiB response proves serialized scratch storage stays below 64 KiB.
- h2spec sections 5, 6.1, 6.9 and 8: 50 passed, one h2spec-skipped case, zero failures.
- Clean Maven build with JMH sources: 633 tests, no failures.
- JMH request streaming: 159.408 ns/op, 0.001 B/op, no GC.
- JMH response streaming frame: 219.090 ns/op, 0.002 B/op, no GC.