feat(core): add HTTP trailers and push streaming
This commit is contained in:
@@ -981,3 +981,22 @@ the allocation noise floor.
|
||||
window and pool byte capacity together; never raise credit independently of bounded storage.
|
||||
|
||||
---
|
||||
|
||||
## DEC-29 — Keep HTTP/2 opt-in until the adversarial phase is complete
|
||||
|
||||
**Context.** Trailers and push streaming make the protocol feature-complete for ordinary and gRPC-
|
||||
shaped traffic, but the dedicated rate-based and composite abuse controls are deliberately owned
|
||||
by the following security phase.
|
||||
|
||||
**Decision.** Keep `FlashConfiguration.http2Enabled` defaulting to `false` during this phase.
|
||||
Applications can enable the complete path explicitly; the default changes only after the hostile-
|
||||
peer suite and its limits are green.
|
||||
|
||||
**Consequence.** Existing deployments do not silently expose a newly completed protocol before its
|
||||
adversarial gate. This is rollout sequencing, not an architectural separation: both protocols use
|
||||
the same public request/response, header, trailer and streaming APIs.
|
||||
|
||||
**Revisit when.** At Phase 13 closure; either flip the default with evidence or record why it must
|
||||
remain opt-in.
|
||||
|
||||
---
|
||||
|
||||
@@ -73,7 +73,7 @@ Status values: `not started` / `in progress` / `blocked` / `done`.
|
||||
| 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 | done | `feature/core/http2` | Explicit stream transition table, bounded primitive stream table and pool, pseudo-header/message validation, protocol-neutral `Request` assembly, virtual-thread dispatch and exception path, cancellation-safe release, raw h2c + Java HTTP/2 integration. Phase 11 closed the two deferred content-length/DATA cases; h2spec sections 5/8 are now 39/39. JMH pooled lifecycle: 458.499 ns/op, 0.003 B/op, no GC. 618/618 tests green at phase closure. |
|
||||
| 11 — DATA, flow control, bodies | done | `feature/core/http2` | Two-level receive/send flow control, consumption-driven WINDOW_UPDATE hysteresis, bounded/coalescing DATA pool, inline and blocking streaming request bodies through the existing `RequestBody`, resumable fixed/known/unknown response streams, content-length and empty-DATA validation. Real TLS HTTP/2 transfer: 100 MiB upload + 100 MiB download verified byte-for-byte. h2spec combined sections 5, 6.1, 6.9 and 8: 50 passed, 1 tool-skipped, 0 failed. JMH: inline materialization exactly one 1,040-byte array; request streaming 0.001 B/op; response streaming 0.002 B/op; full pooled lifecycle 0.003 B/op. 633/633 tests green from a clean `-Pjmh` build. |
|
||||
| 12 — Trailers, half-close, gRPC | not started | — | — |
|
||||
| 12 — Trailers, half-close, gRPC | done | `feature/core/http2` | Protocol-neutral request/response trailers, bounded push streaming, four half-close orderings and authority-form CONNECT tunnels complete. Real grpcurl 1.9.3 unary/server-streaming/error interop passes. EX-48/49 fixed. HTTP/2 remains opt-in until the Phase 13 hostile-peer gate (DEC-29). 649/649 tests green from a clean `-Pjmh` build. |
|
||||
| 13 — Security hardening & abuse resistance | not started | — | — |
|
||||
| 14 — h2c prior knowledge + proxy support | not started | — | — |
|
||||
| 15 — RFC 8441 extended CONNECT (WS over h2) | not started | — | — |
|
||||
@@ -778,6 +778,22 @@ the final release. The regression test sends a complete request, immediately res
|
||||
a second request and proves that only the second handler invocation and response occur. **Phase**:
|
||||
10.
|
||||
|
||||
### EX-48 — HTTP/1.1 request trailers were parsed and discarded
|
||||
|
||||
Found while exposing the protocol-neutral request trailer API. `ChunkedInputStream` consumed and
|
||||
bounded the final trailer section but discarded every field, so no honest API could provide the
|
||||
same semantics on HTTP/1.1 and HTTP/2. **Fix**: parse the bounded section into a connection-owned
|
||||
`MutableHeaderMap`, expose it through `Request.trailers()` only after body EOF, reject malformed and
|
||||
framing-sensitive fields, and add HTTP/1 parity/regression tests. **Phase**: 12.
|
||||
|
||||
### EX-49 — CONNECT routes were registered as origin-form paths
|
||||
|
||||
Found while exercising an HTTP/2 tunnel. The public `connect("authority", handler)` API passed
|
||||
through the ordinary path sanitizer, which prepended `/`; both HTTP/1.1 authority-form request
|
||||
targets and HTTP/2 `:authority` arrive without that prefix, so the existing CONNECT API could
|
||||
never match its documented target. **Fix**: normalize CONNECT authority targets separately in the
|
||||
shared router registration path and verify a live bidirectional HTTP/2 tunnel. **Phase**: 12.
|
||||
|
||||
---
|
||||
|
||||
# PART III — The phases
|
||||
@@ -2730,11 +2746,11 @@ error.
|
||||
boundary in `DECISIONS.md` as `DEC-08`.
|
||||
|
||||
### Safety checks
|
||||
- [ ] Trailers without `END_STREAM` rejected
|
||||
- [ ] Pseudo-headers in trailers rejected
|
||||
- [ ] Trailer count and size bounded (they go through the same HPACK limits)
|
||||
- [ ] `ResponseStream.write` after `close` throws, does not corrupt the stream
|
||||
- [ ] CONNECT tunnels are bounded by the same timeouts and flow control as normal streams
|
||||
- [x] Trailers without `END_STREAM` rejected
|
||||
- [x] Pseudo-headers in trailers rejected
|
||||
- [x] Trailer count and size bounded (they go through the same HPACK limits)
|
||||
- [x] `ResponseStream.write` after `close` throws, does not corrupt the stream
|
||||
- [x] CONNECT tunnels are bounded by the same timeouts and flow control as normal streams
|
||||
|
||||
### Tests
|
||||
- `Http2TrailersTest`, `Http1TrailersTest` (the h1 rendering), `TrailerParityTest`.
|
||||
@@ -2748,11 +2764,12 @@ error.
|
||||
- `README.md` — the `ResponseStream` API, with a gRPC-shaped example.
|
||||
|
||||
### DoD
|
||||
- [ ] `grpcurl` completes a unary and a server-streaming call against a Flash handler.
|
||||
- [ ] Trailers work on both protocols through one API.
|
||||
- [ ] `FlashConfiguration.http2Enabled` flips to default `true` (the feature is now complete
|
||||
- [x] `grpcurl` completes a unary and a server-streaming call against a Flash handler.
|
||||
- [x] Trailers work on both protocols through one API.
|
||||
- [x] `FlashConfiguration.http2Enabled` flips to default `true` (the feature is now complete
|
||||
enough to be on by default) — or, if the team prefers a conservative rollout, stays
|
||||
`false` with the decision recorded.
|
||||
`false` with the decision recorded (`DEC-29`: retain opt-in until Phase 13's hostile-peer
|
||||
suite is complete).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Trailers and streaming
|
||||
|
||||
Flash exposes the same request and response model on HTTP/1.1 and HTTP/2. Request trailers are
|
||||
available through `Request.trailers()` after the body has reached EOF. Calling it earlier throws
|
||||
`IllegalStateException`; this prevents handlers from observing an incomplete trailer section.
|
||||
HTTP/1.1 reads trailers from the final chunk, while HTTP/2 decodes the trailing HEADERS block in
|
||||
the connection's existing HPACK context.
|
||||
|
||||
Response trailers are added with `Response.trailer(name, value)` or a `PreEncodedHeader`. HTTP/1.1
|
||||
uses chunked framing and writes the fields after the zero chunk. HTTP/2 writes a trailing HEADERS
|
||||
block with `END_STREAM`; the final DATA frame deliberately does not carry `END_STREAM`.
|
||||
|
||||
`Response.streaming(producer)` is the push alternative to `stream(InputStream, length)` and
|
||||
`chunked(InputStream)`. Its `ResponseStream` is a bounded blocking bridge. A producer runs on a
|
||||
virtual thread and blocks when the protocol writer or the HTTP/2 flow-control windows cannot make
|
||||
progress. This keeps backpressure explicit without callbacks or reactive types:
|
||||
|
||||
```java
|
||||
return response.type("application/grpc").streaming(stream -> {
|
||||
try {
|
||||
for (byte[] message : messages) stream.write(message, 0, message.length);
|
||||
stream.trailer("grpc-status", "0");
|
||||
} catch (IOException failure) {
|
||||
throw new UncheckedIOException(failure);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The transport supports the primitives required by gRPC, but the core does not provide protobuf
|
||||
codecs, generated stubs, service descriptors, or a gRPC service API. Those belong in a future
|
||||
`flash-ext-grpc` module. `GrpcInteropTest` verifies the boundary with the external `grpcurl` client
|
||||
and a hand-written wire-format handler.
|
||||
|
||||
CONNECT requests follow RFC 9113 request pseudo-header rules: `:authority` is required and
|
||||
`:scheme`/`:path` are forbidden. Their DATA remains subject to the ordinary request limits,
|
||||
timeouts and two-level flow control.
|
||||
Reference in New Issue
Block a user