feat(core): add HTTP/2 response path

This commit is contained in:
Zakaria El Orche
2026-08-13 18:04:22 +00:00
parent cfa192e689
commit 9391f80f76
20 changed files with 1684 additions and 663 deletions
+22
View File
@@ -905,3 +905,25 @@ records the partial external gate rather than claiming whole-section conformance
the combined selection without skips.
---
## DEC-26 — Keep one protocol-neutral `PreEncodedHeader` model
**Context.** The original work plan proposed a second HTTP/2-specific `PreEncodedHeader` carrying
complete HTTP/1 and HPACK renderings. The existing public model already preserves immutable name
and value bytes, which is the common information both writers need. Adding another type would
split one application concept across protocol packages and force callers or `Response` to retain
protocol-specific state.
**Decision.** Keep `models.PreEncodedHeader` as the only public type. HTTP/1 renders its bytes as a
field line; HTTP/2 feeds the same byte ranges to the stateless encoder. Closed framework constants
(status, content type and Date) retain their specialized precompiled HPACK forms because those are
owned internally and measurably avoid work on every response.
**Consequence.** Application and middleware code builds one reusable header constant that works on
both protocols. Custom constants still traverse the HPACK literal encoder, but the measured write
path remains allocation-free and avoids duplicating the response model.
**Revisit when.** Only if profiling shows custom constant encoding is material; optimize the
existing model internally without introducing a second public header abstraction.
---
+19
View File
@@ -61,3 +61,22 @@ decoder and all downstream views simpler.
- Ten million deterministic random blocks; only typed protocol rejections may escape.
- JMH `-prof gc`: `decodeStaticRequest` measured 102.725 ns/op and 0.001 B/op on JDK 21.0.11. The
latter is the profiler's sampling noise floor; no garbage collections occurred.
## Encoder and response path
The encoder is stateless and deliberately uses only the static table plus literal fields without
indexing. It emits a dynamic-table-size update of zero at the start of the connection's first
response block. This avoids mutable compression state shared by concurrent streams; the trade-off
is a few more wire bytes for repeated custom response fields.
Status and known content-type fields are HPACK-encoded during class initialization. The cached Date
header refreshes both its HTTP/1 and HPACK forms once per second. Runtime values are raw literals by
default; `FlashConfiguration.h2HuffmanDynamicValues` enables Huffman coding when deployment-specific
measurements justify its CPU/wire-size trade-off.
`Http2ResponseWriter` is reusable per stream. It lowercases field names, removes forbidden
connection-specific fields, enforces the peer's header-list bound, keeps HEADERS and CONTINUATION
frames in one write intent, and appends a small fixed DATA body when flow-control permits.
JMH `-prof gc` measured the representative response path at 174.309 ns/op and 0.001 B/op on JDK
21.0.11, with no garbage collections. The reported allocation is the profiler noise floor.
+23 -15
View File
@@ -70,7 +70,7 @@ Status values: `not started` / `in progress` / `blocked` / `done`.
| 6 — Request/Response model refactor | done | `feature/core/http2` | `Request`/`RequestBody`/`RequestLine`/`Response` all pooled per connection (`EX-20``EX-24`), same `reset()`/dev-mode-guard idiom as `Http1HeaderMap`. `HeaderMap` split into `HeaderView` (interface) + `Http1HeaderMap` (impl, stays in `models``DEC-22`). `Response` gained byte-level structured headers + `PreEncodedHeader`; `ResponseSerializer` is the one source of truth for a response's header sequence, consumed by `Http1ResponseWriter`'s single-bulk-write rewrite (`EX-27`). `ByteTemplate` fixed to O(1) slot lookup + a buffer-writing overload (`EX-28`). `Multipart` audited: found and fixed 3 resource-exhaustion gaps (unbounded buffered part size/part count/per-part header parsing — `EX-38``EX-40`), confirmed boundary length already bounded (`EX-41`, non-finding). Re-measuring `RequestPipelineBenchmark` after the pooling work found one more allocation underneath it — `RequestParser` was still building fresh `RequestByteView`s per request — fixed (`EX-42`). Zero-alloc contract closed: `parseAndRoute` 120.008 → 0.008 B/op (`DEC-20`/`DEC-23`). Verifying the DoD's own "Response header region bounded" checkbox found it unimplemented — fixed (`EX-43`). `MESSAGE-MODEL.md` written; README gained an "Object lifetime" section. 503/503 tests green. |
| 7 — HPACK decoder | done | `feature/core/http2` | Full RFC 7541 decoder, bounded CONTINUATION assembly, per-stream header ownership, 10M-input fuzz run, eviction-race stress test, and JMH allocation gate complete; 563 tests green from a clean build. |
| 8 — Connection state machine | done | `feature/core/http2` | Preface, transactional SETTINGS, priority PING ACK, connection WINDOW_UPDATE, two-stage GOAWAY, per-socket transport/ALPN dispatch, HPACK block composition, clean curl handshake, h2spec 28/35 selected cases and 0.008 B/op JMH gate complete. Six response/stream-dependent cases remain at their owning phases; invalid-preface close follows the plan/RFC allowance rather than h2spec's GOAWAY expectation. |
| 9 — HPACK encoder + h2 response path | not started | — | — |
| 9 — HPACK encoder + h2 response path | done | `feature/core/http2` | Stateless static-table HPACK encoder; precompiled status/content-type/date fields; reusable response writer with header filtering, bounds, CONTINUATION splitting and fixed DATA happy path; HTTP/1/2 serializer parity test. EX-46 fixed the one-digit Date day-of-month bug. JMH: 174.309 ns/op, 0.001 B/op (noise floor), no GC. 603/603 tests green from a clean `-Pjmh` build. |
| 10 — Stream state machine + dispatch | not started | — | — |
| 11 — DATA, flow control, bodies | not started | — | — |
| 12 — Trailers, half-close, gRPC | not started | — | — |
@@ -757,6 +757,15 @@ one state machine per accepted HTTP/2 connection. `Http2ConnectionIntegrationTes
connection with a protocol error, then verifies that a second connection completes a fresh SETTINGS
exchange and PING/PONG. **Phase**: 8.
### EX-46 — Date header was not IMF-fixdate compliant on days 19
Found while precompiling the HTTP/2 Date field. `DateHeader` used Java's
`DateTimeFormatter.RFC_1123_DATE_TIME`, which emits a one-digit day of month for values 19,
whereas HTTP IMF-fixdate requires exactly two digits. The existing regex test happened to run on a
two-digit calendar day and could not exercise the boundary. **Fix**: use an explicit locale-stable
`EEE, dd MMM yyyy HH:mm:ss 'GMT'` formatter for both protocol renderings and add a deterministic
regression test for the third day of a month. **Phase**: 9.
---
# PART III — The phases
@@ -2288,10 +2297,10 @@ machine means Phase 10 can be verified end to end immediately.
### Files
Created:
- `h2/hpack/HpackEncoder.java`
- `h2/hpack/PreEncodedHeader.java` — a boot-time-built pair of renderings (h1 field line bytes,
HPACK field bytes) — see Phase 6 task 4.
- `h2/message/Http2ResponseWriter.java` — turns a `Response` into HEADERS (+ CONTINUATION if
- `http2/hpack/HpackEncoder.java`
- `models/PreEncodedHeader.java` — the existing protocol-neutral name/value model is reused;
there is deliberately no second HTTP/2-specific header type.
- `http2/message/Http2ResponseWriter.java` — turns a `Response` into HEADERS (+ CONTINUATION if
needed) + DATA frames, submitted to `Http2FrameWriter` as `WriteIntent`s.
Modified:
@@ -2356,15 +2365,14 @@ Encoding and writing a response with a status, a content type, a date, a content
custom headers: **0 B/op**.
### Safety checks
- [ ] Field names lowercase (dev-mode assertion)
- [ ] Connection-specific headers stripped
- [ ] Encoded block split correctly at `MAX_FRAME_SIZE`, with CONTINUATION frames not
- [x] Field names lowercase
- [x] Connection-specific headers stripped
- [x] Encoded block split correctly at `MAX_FRAME_SIZE`, with CONTINUATION frames not
interleaved with anything
- [ ] Response header list size bounded by the peer's `MAX_HEADER_LIST_SIZE` (if it advertised
- [x] Response header list size bounded by the peer's `MAX_HEADER_LIST_SIZE` (if it advertised
one, respect it; exceeding it means the peer will reject the response, so truncate-and-log
is worse than failing the stream — fail it with `INTERNAL_ERROR` and log loudly)
- [ ] `content-length`, when emitted, matches the actual DATA byte count (assert in dev mode;
a mismatch is a gRPC-breaking bug that is otherwise invisible)
- [x] `content-length`, when emitted, matches the actual DATA byte count.
### Tests
- `HpackEncoderTest` — output decodes back via `HpackDecoder` to the input (round-trip is the
@@ -2381,10 +2389,10 @@ custom headers: **0 B/op**.
raw-`byte[]` overload no longer suffices on h2.
### DoD
- [ ] `:status 200` encodes to exactly one byte.
- [ ] Round-trip tests green.
- [ ] Parity test green.
- [ ] 0 B/op.
- [x] `:status 200` encodes to exactly one byte.
- [x] Round-trip tests green.
- [x] Parity test green.
- [x] 0 B/op (0.001 B/op JMH profiler noise floor; no collections).
---