feat(core): add HTTP/2 response path
This commit is contained in:
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 1–9
|
||||
|
||||
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 1–9,
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user