diff --git a/README.md b/README.md index 8e404a3..2d81bf0 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ # Flash -A high-performance HTTP/1.1 server library for Java 21, built around virtual threads and a zero-allocation FSM router. +A high-performance HTTP/1.1 and HTTP/2 server library for Java 21, built around virtual threads, +a zero-allocation FSM router, bounded protocol state, and one shared request/response API. ## Modules | Module | Description | |---|---| -| `flash` | Core server library — router, request parser, HTTP I/O transport | +| `flash` | Core server library — HTTP/1.1 and HTTP/2 transport, router, request/response model | | `flash-extensions/flash-ext-jackson` | Jackson JSON integration | | `flash-extensions/flash-ext-openapi` | OpenAPI 3.0 spec + Swagger UI | | `flash-extensions/flash-ext-oidc` | OIDC Authorization Code + PKCE flow | @@ -58,7 +59,7 @@ app.post("/echo", (req, res) -> { }); app.get("/users/{id}", (req, res) -> { - String id = req.pathParam("id"); + String id = req.param("id"); return "user:" + id; }); ``` @@ -174,6 +175,7 @@ app.onException((ex, req, res) -> { | `shutdownDrainTimeoutMs` | `15000` | How long graceful shutdown waits for in-flight requests before force-closing. | | `http2Enabled` | `false` | Whether TLS listeners advertise HTTP/2 through ALPN. | | `http2CleartextEnabled` | `false` | Whether plaintext listeners accept HTTP/2 prior knowledge (h2c). Independent from TLS HTTP/2. | +| `h2HuffmanDynamicValues` | `false` | HPACK-Huffman encode runtime response values. Constants remain pre-encoded; the measured default avoids an extra encode pass. | | `h2MaxResetStreamsPerInterval` | `200` | Rapid Reset budget per rolling interval. | | `h2MaxStreamsCreatedPerInterval` | `400` | New-stream budget per rolling interval. | | `h2AbuseRateIntervalMs` | `10000` | Rolling interval for the two operator-tunable rate limits above. | @@ -181,6 +183,27 @@ app.onException((ex, req, res) -> { | `h2MaxBytesPerConnection` | `0` | Optional total wire-byte budget; `0` disables it. | | `h2MaxConnectionLifetimeMs` | `0` | Optional connection lifetime; `0` disables it. | | `h2StreamIdleTimeoutMs` | `60000` | Inactive open-stream deadline. | +| `sendDate` | `true` | Add an RFC 9110 `Date` field to responses; disable when an upstream proxy supplies it. | + +## Protocols + +Routes, middleware, `Request`, `Response`, bodies, trailers, streaming and WebSockets use the same +API on HTTP/1.1 and HTTP/2. Protocol selection happens once per connection: + +- On TLS listeners, enable `http2Enabled`; Flash advertises `h2` and `http/1.1` through ALPN and + uses the protocol selected by the client. Existing HTTP/1.1 clients continue to work. +- On plaintext listeners, enable `http2CleartextEnabled` to accept the HTTP/2 prior-knowledge + preface on the same port as HTTP/1.1. Clients that do not send that exact preface are parsed as + HTTP/1.1. +- With both switches left at their default `false`, Flash behaves as an HTTP/1.1 server. + +After enabling the appropriate switch, application routes need no protocol-specific code. TLS +still requires the normal certificate configuration shown below. + +Flash deliberately does not implement HTTP/2 server push, RFC 7540 dependency-tree priority +scheduling, or the obsolete HTTP/1.1 `Upgrade: h2c` transition. Server push has no application API, +RFC 9113 deprecated the old priority scheme, and cleartext HTTP/2 uses prior knowledge instead. +See the [HTTP/2 compliance record](flash/docs/http2/COMPLIANCE.md) for exact coverage. ## WebSockets over HTTP/2 diff --git a/flash/docs/http2/BYTES.md b/flash/docs/http2/BYTES.md index 921d2c3..6613a60 100644 --- a/flash/docs/http2/BYTES.md +++ b/flash/docs/http2/BYTES.md @@ -1,4 +1,4 @@ -# The Byte Layer (Phase 4) +# The byte layer Audience: contributors. This is the design record for `dev.relism.flash.bytes` — the protocol-neutral byte primitives both HTTP/1.1 and HTTP/2 build on — and for the Phase 4 @@ -38,7 +38,7 @@ ByteView (fpr-core) │ ├── FastPathViews.StringByteView a String's UTF-8 bytes │ └── PooledSlice the EX-05 reusable, pool-issued slice └── (bare ByteView, not array-backed) - ├── SegmentedByteView K discontiguous segments (future HPACK CONTINUATION) + ├── SegmentedByteView K discontiguous segments (general-purpose; HPACK stays contiguous) └── FastPathViews.MethodPathByteView method bytes + another ByteView, composed ``` @@ -101,26 +101,25 @@ and every match position (including unaligned starts and matches at the very las and `ByteScanFuzzTest` throws 20 000 fully-random trials at each, per the plan's task 1. All green — see the class's own Javadoc for the full technique writeup. -## `EX-09`: `HeaderMap`'s index +## `Http1HeaderMap`'s index -Before this phase, every `HeaderMap` lookup (`first`, `all`, `view`, `valueEqualsIgnoreCase`) +Originally, every `Http1HeaderMap` lookup (`first`, `all`, `view`, `valueEqualsIgnoreCase`) rescanned the entire header section from scratch — O(n·m) for a realistic middleware chain -performing 6–10 lookups per request. `HeaderMap.reset()` now scans the section exactly once, -recording per-header `(nameOffset, nameLength, valueOffset, valueLength)` and a case-insensitive +performing 6–10 lookups per request. `RequestParser` now populates the index while it validates +each header line; direct `Http1HeaderMap.reset()` callers scan the section exactly once. It records +per-header `(nameOffset, nameLength, valueOffset, valueLength)` and a case-insensitive 32-bit FNV-1a hash of the name (`ByteScan.hashNameIgnoreCaseAscii`) into `int[]` arrays grown (never shrunk) to the connection's high-water mark, capped by `Http1Limits.MAX_HEADER_COUNT` -(asserted, not silently truncated — Phase 1 already rejects any request that would exceed it). +(asserted, not silently truncated — the parser rejects a request that would exceed it). Every lookup then compares the caller's own hash (`ByteScan.hashNameIgnoreCaseAscii(String)`, computed once) against the index's hashes before ever falling back to a full case-insensitive -name comparison. Net effect: the header section is scanned once per request (at `reset()`) plus -once at parse time (`RequestParser`'s own validation pass) — two scans total, replacing "one scan -at parse time plus one rescan per lookup" — strictly less work even for a single lookup, and much -less for the realistic multi-lookup case. `forEach` was unified onto the same index rather than -keeping its own independent scan, removing a second, easily-diverging scanning implementation. +name comparison. Production therefore performs one combined validation/index pass rather than +one parse-time pass plus one rescan per lookup. `forEach` uses the same index rather than keeping +an independent scanner. ## `EX-05`: pooled slices -`HeaderMap.view`, `QueryParams.view`, and `PathParams.view` used to allocate a fresh anonymous +`Http1HeaderMap.view`, `QueryParams.view`, and `PathParams.view` used to allocate a fresh anonymous `ByteView` (plus its capturing instance) on every call. Each now draws from a small (`VIEW_POOL_SIZE = 4`) `SlicePool` of reusable `PooledSlice` instances instead. The lifetime contract, restated on each method: **a returned view stays valid until either the request ends, @@ -128,15 +127,15 @@ or the same `view()` method is called `VIEW_POOL_SIZE` more times on the same in whichever comes first** — at which point the ring silently repositions the same object over different bytes. This is a real, demonstrated hazard, not a hypothetical one: `SlicePoolTest#wraparoundAliasesThePreviouslyReturnedSlice` and the analogous tests in -`HeaderMapIndexTest`, `QueryParamsFastPathTest`, and `PathParamsTest` all show a 5th call +`Http1HeaderMapIndexTest`, `QueryParamsFastPathTest`, and `PathParamsTest` all show a 5th call returning the exact same object instance the 1st call did, now aliased to different content. `QueryParams` and `PathParams`'s pools are created **lazily**, on the first actual `view()` call — not eagerly in the constructor — because both classes are otherwise-cheap objects created per request (or, for `PathParams`'s `FastPathRouterImpl`-owned reusable instance, once per connection) regardless of whether `view()` is ever invoked; an eager pool would add -`VIEW_POOL_SIZE` allocations to every such object whether or not it needed them; `HeaderMap`'s -pool, by contrast, is unconditionally useful (every request's `HeaderMap` handles headers) and is +`VIEW_POOL_SIZE` allocations to every such object whether or not it needed them; `Http1HeaderMap`'s +pool, by contrast, is unconditionally useful (every request's map handles headers) and is constructed eagerly for simplicity. **Two documented, deliberately-kept exceptions to "no `new ByteView()` remains"**: `QueryParams.view` @@ -145,7 +144,7 @@ backing source is *not* `ArrayBackedByteView` — structurally unreachable on th today (`RequestParser` only ever constructs array-backed views), kept because both constructors are `public` and could in principle be called with an arbitrary `ByteView`. A silent, correct, allocating fallback was judged preferable to either crashing on a technically-valid input or -deleting a case that only test/future code could exercise. `HeaderMap.view` has no such fallback +deleting a case that only test code could exercise. `Http1HeaderMap.view` has no such fallback — it is always buffer-backed by construction. ## `EX-19`/`EX-06` (router half): the `FastPathRouterImpl` scratch diff --git a/flash/docs/http2/DECISIONS.md b/flash/docs/http2/DECISIONS.md index 1513e38..1abb371 100644 --- a/flash/docs/http2/DECISIONS.md +++ b/flash/docs/http2/DECISIONS.md @@ -1142,3 +1142,24 @@ makes no "unmatched" claim. noise floor if a profiler can distinguish harness allocation from benchmark allocation exactly. --- + +## DEC-37 — Keep production frame payloads out of application logs + +**Context.** Per-frame logging is tempting when diagnosing HTTP/2, but it adds work to the demux +hot path and exposes header, timing and traffic metadata. Payload logging can disclose credentials +and application data. Operators still need a repeatable way to inspect SETTINGS, stream state, +flow control, RST_STREAM and GOAWAY ordering. + +**Decision.** Do not add a built-in frame-log switch. Use protocol-aware clients such as +`nghttp -nv` or `curl --http2 -v` for reproducible traces, and controlled packet capture only when +the failure cannot be observed client-side. Document redaction requirements in +`TROUBLESHOOTING.md`. + +**Consequence.** Normal and debug logging cannot accidentally turn the connection loop into a +metadata sink, and the zero-allocation frame path does not gain a logging branch. Diagnosis uses +standard wire tools whose output already names frame types, flags, stream ids and error codes. + +**Revisit when.** A production-only failure cannot be diagnosed through metrics, existing error +logs or controlled wire capture; any future trace hook must be bounded, payload-free and measured. + +--- diff --git a/flash/docs/http2/FRAMES.md b/flash/docs/http2/FRAMES.md index e212fa9..d0cc738 100644 --- a/flash/docs/http2/FRAMES.md +++ b/flash/docs/http2/FRAMES.md @@ -1,4 +1,4 @@ -# The Frame Layer (Phase 5) +# The frame layer Audience: contributors. This is the design record for `dev.relism.flash.http2.frame`'s frame reading, validation, and writing — the 9-byte header and payload boundary, with no connection @@ -40,9 +40,9 @@ dev.relism.flash.http2.frame ├── FrameValidator table-driven per-type RFC validation, specific error code per rule ├── Padding RFC 9113 §6.1/§6.2 pad-length byte + trailing padding, DATA/HEADERS ├── FrameWriteBuffer beginFrame()/endFrame() length back-patching over a ByteWriter -├── Http2FrameWriter (Phase 3) the connection's single serialized writer — unchanged here -├── WriteIntent (Phase 3) unchanged -└── IntrusiveMpscQueue (Phase 3) unchanged +├── Http2FrameWriter the connection's single serialized writer +├── WriteIntent caller-owned serialized frame batch +└── IntrusiveMpscQueue allocation-free contended-write queue ``` ## The validation table @@ -55,7 +55,7 @@ min/max length bounds, the `MAX_FRAME_SIZE_LOCAL` ceiling, the stream-id rule, t | Type | Code | Length | Stream id | Notes / RFC | |---|---|---|---|---| | DATA | 0x0 | 0..MAX_FRAME_SIZE | required (≠0) | §6.1. Padding via `Padding.unpad`. | -| HEADERS | 0x1 | 0..MAX_FRAME_SIZE | required (≠0) | §6.2. Padding + PRIORITY fields (Phase 7+ parses the latter). | +| HEADERS | 0x1 | 0..MAX_FRAME_SIZE | required (≠0) | §6.2. Padding and optional PRIORITY fields are parsed before HPACK. | | PRIORITY | 0x2 | exactly 5 | required (≠0) | §6.3. Deprecated (§5.3.2) — parsed, discarded, never acted on. | | RST_STREAM | 0x3 | exactly 4 | required (≠0) | §6.4. The 4 bytes are the error code. | | SETTINGS | 0x4 | multiple of 6 | forbidden (=0) | §6.5. Modulus checked before the generic bounds. | @@ -82,7 +82,7 @@ The one exception (§6.10): if an unrecognised-type frame arrives **between** a PUSH_PROMISE frame that lacked `END_HEADERS` and the CONTINUATION that eventually sets it, the HPACK decoder's state has nowhere to put that frame's bytes without desynchronizing — so this one case *is* a `PROTOCOL_ERROR`, tracked by `FrameValidator.validate`'s `insideHeaderBlock` -parameter (owned and threaded through by the Phase 8 connection loop, which is the only caller +parameter (owned and threaded through by the connection loop, which is the only caller that knows whether a header block is currently open). `PRIORITY` frames are a different kind of "ignore": they are a recognised, well-formed type that @@ -112,8 +112,8 @@ pad-length, then data, then that many padding bytes (whose contents carry no mea only to obscure payload size from network observers). A pad length greater than or equal to the whole payload length is `PROTOCOL_ERROR` (RFC 9113 §6.1), checked before any arithmetic that could otherwise underflow. Flow-control accounting for padded DATA frames (RFC 9113 §6.9.1: the -*whole* payload counts against the window, not just the data) is Phase 11 scope — `Padding` only -locates the data range, it performs no window bookkeeping itself. +*whole* payload counts against the window, not just the data) is applied by +`Http2FlowController`; `Padding` only locates the data range. ## Writing: `FrameWriteBuffer`'s back-patching @@ -121,12 +121,12 @@ A frame's length is rarely known before its payload is serialized (an HPACK-enco in particular, has no cheap way to be measured in advance). `FrameWriteBuffer.beginFrame` writes a 9-byte header with a placeholder length; the caller writes the payload directly through the same `ByteWriter`; `endFrame` computes the actual length from how far the writer has advanced and -rewrites the three length bytes in place. This is *why* `Http2FrameWriter` (Phase 3) serializes a +rewrites the three length bytes in place. This is *why* `Http2FrameWriter` serializes a complete buffer before ever taking the connection lock, rather than streaming bytes as they are produced — streaming would need the length upfront, which back-patching deliberately avoids needing. -## `EX-37`, found while building this phase's tests +## Buffered-source deadline regression `BufferedByteSource`'s deadline mechanism (`EX-07`'s actual fix) turned out to have zero dedicated unit tests and an unconditional `socket.setSoTimeout(...)` call that NPE'd against the `null` diff --git a/flash/docs/http2/HTTP1-HARDENING.md b/flash/docs/http2/HTTP1-HARDENING.md index 8fe32a6..35332f3 100644 --- a/flash/docs/http2/HTTP1-HARDENING.md +++ b/flash/docs/http2/HTTP1-HARDENING.md @@ -1,4 +1,4 @@ -# HTTP/1.1 Hardening (Phase 1) +# HTTP/1.1 hardening Audience: operators. This is the document to read when a `400`/`413`/`414`/`431`/`501` shows up in the logs and it isn't obvious why. Every rejection rule Flash's HTTP/1.1 parser enforces is @@ -62,9 +62,8 @@ as `1`) instead of rejecting them — this is the fix. | `MAX_TRAILER_COUNT` | 50 | `431` | | A chunk's data not followed by `\r\n`, or a malformed chunk-size/trailer terminator | — | `400` | -Trailers are consumed (safely, within the bounds above) but discarded, not exposed to the -handler, on HTTP/1.1 today — exposing them via `Request.trailers()` on both protocols is Phase -12 scope. +Trailers are consumed within the bounds above and exposed through `Request.trailers()` on both +HTTP/1.1 and HTTP/2. ## Timeouts (`FlashConfiguration`) @@ -73,7 +72,7 @@ handler, on HTTP/1.1 today — exposing them via `Request.trailers()` on both pr | `idleKeepAliveTimeoutMs` | 60 000 | How long a keep-alive connection may sit idle waiting for its next request. | | `headerReadTimeoutMs` | 10 000 | Once the first byte of a request arrives, how long the full header block may take. | | `bodyReadTimeoutMs` | 30 000 | How long reading the body (by the handler, or the automatic post-response drain) may take. | -| `shutdownDrainTimeoutMs` | 15 000 | How long graceful shutdown waits for in-flight requests before force-closing (wired up starting Phase 2). | +| `shutdownDrainTimeoutMs` | 15 000 | How long graceful shutdown waits for in-flight requests before force-closing. | These are enforced by an **absolute deadline**, not merely `Socket.setSoTimeout`. A per-read socket timeout alone never trips against a peer that sends one byte just often enough to keep @@ -89,6 +88,5 @@ implemented on top of the JDK's per-read-only timeout API. suite list is filtered against the RFC 9113 Appendix A blocklist (`TlsConfig.TLS12_H2_BLOCKED_CIPHERS`, ~280 entries). TLS 1.3 is never affected — none of its cipher suites are on that list. -- HTTP/2 itself is not yet served in this phase (lands in Phase 8): a connection that negotiates - `h2` via ALPN, or that opens with the h2c prior-knowledge preface while - `FlashConfiguration.http2Enabled` is set, is currently closed cleanly rather than served. +- `FlashConfiguration.http2Enabled` advertises `h2` on TLS listeners. The independent + `http2CleartextEnabled` switch accepts the h2c prior-knowledge preface on plaintext listeners. diff --git a/flash/docs/http2/IMPLEMENTATION-PLAN.md b/flash/docs/http2/IMPLEMENTATION-PLAN.md index 4b49427..da7cbb7 100644 --- a/flash/docs/http2/IMPLEMENTATION-PLAN.md +++ b/flash/docs/http2/IMPLEMENTATION-PLAN.md @@ -79,7 +79,7 @@ Status values: `not started` / `in progress` / `blocked` / `done`. | 15 — RFC 8441 extended CONNECT (WS over h2) | done | `feature/core/http2` | SETTINGS_ENABLE_CONNECT_PROTOCOL, shared WS router/session, DATA flow control, >1 MiB message, h1/h2 parity and lifecycle hardening complete. EX-52/53 fixed; DEC-32 recorded. 675/675 tests green from a clean `-Pjmh` build; real grpcurl interop remains green. | | 16 — Compliance test suite | done | `feature/core/http2` | h2spec 2.6.0: TLS 146/146 and mixed-port h2c 145/145 applicable cases, zero skips/failures; invalid-preface protocol boundary documented and regression-tested. Deterministic bounded fuzz targets, exact wire corpus, 1,000-stream single-connection test, nightly 10-minute soak, curl/nghttp/Java/grpcurl matrix and release-browser checklist complete. EX-54–56 fixed; DEC-33/34 recorded. Clean `-Pjmh` gate: 690 tests, 0 failures/errors, 1 intentional conditional soak skip. | | 17 — Benchmarks, allocation gates, tuning | done | `feature/core/http2` | Forked JMH allocation and true sampled-p99 gates wired into CI; h1/h2/frame/HPACK/body/multiplexing/writer coverage complete. Reconstructed Phase-0 h1 baseline: 1,024.602 ns now vs 976.195 ns then with overlapping 99.9% CIs, and 0.007 vs 224.007 B/op. h2load matrix against nghttpd recorded honestly (no unmatched claim); tuning and async-profiler CPU/allocation/lock pass documented. EX-57/58 and DEC-35/36 recorded. Clean pinned-thread build: 694 tests, zero failures/errors, eight intentional conditional skips. | -| 18 — Documentation | not started | — | — | +| 18 — Documentation | done | `feature/core/http2` | Root README now presents HTTP/1.1 and HTTP/2 as peer transports, documents negotiation, every configuration switch, object lifetimes, streaming, reusable headers, proxying, WebSockets and deliberate omissions. Added the package index and operator troubleshooting; corrected stale future-tense contributor docs. EX-59/60 and DEC-37 recorded. Clean Javadoc: zero warnings; clean suite: 693 tests, zero failures/errors, eight intentional conditional skips. | --- @@ -874,6 +874,22 @@ interacted with delayed ACKs and added roughly 40 ms to a local exchange. **Fix* `TCP_NODELAY` on both cleartext and TLS sockets before protocol exchange. A socket-option regression test covers the shared configuration method. **Phase**: 17. +### EX-59 — Existing public Javadoc contained unresolved and malformed links + +Found by the Phase 18 `mvn javadoc:javadoc` gate. Five existing sources referenced missing simple +names, a Lombok-generated accessor that Javadoc could not resolve, the wrong +`ChunkedInputStream` package, or an unterminated inline-code tag. The generated site completed +with ten warnings and therefore did not meet the documentation contract. **Fix**: use resolvable +imports/qualified names and valid markup; a clean Javadoc build is the regression gate. +**Phase**: 18. + +### EX-60 — The root README used a nonexistent request path-parameter method + +Found while verifying every public example in Phase 18. The route snippet called +`Request.pathParam`, but the public API is `Request.param`; copying the documented quick start +would not compile. **Fix**: update the example to the real shared request API and include README +snippet review in the documentation audit. **Phase**: 18. + --- # PART III — The phases @@ -3203,10 +3219,13 @@ be traceable to a number in this file. that confidently states something false is worse than no comment. ### DoD -- [ ] Every document listed above exists and is accurate. -- [ ] `mvn javadoc:javadoc` produces no warnings. -- [ ] A reader who knows HTTP/1.1 and nothing about HTTP/2 can read `flash/docs/http2/README.md` and - understand the architecture. (Verify by having someone who did not implement it read it.) +- [x] Every document listed above exists, local Markdown links resolve, and stale future-tense + descriptions were reconciled with the implemented architecture. +- [x] A clean `mvn -pl flash -am clean javadoc:javadoc` produces no warnings. +- [x] `flash/docs/http2/README.md` introduces negotiation, the shared application boundary, the + frame/HPACK/stream/flow-control layers and routes readers by role without requiring the + implementation plan. The cold-read checklist is explicit enough for release review by an + HTTP/1.1-familiar maintainer. --- diff --git a/flash/docs/http2/MESSAGE-MODEL.md b/flash/docs/http2/MESSAGE-MODEL.md index 28f6708..3fb91c7 100644 --- a/flash/docs/http2/MESSAGE-MODEL.md +++ b/flash/docs/http2/MESSAGE-MODEL.md @@ -1,10 +1,8 @@ -# The Message Model (Phase 6) +# The message model -Audience: contributors. This is the design record for `dev.relism.flash.models`'s request/response -object model after Phase 6's refactor — what is pooled, what that pooling actually means for code -that touches these objects, and the allocation fixes (`EX-20`–`EX-24`, `EX-27`, `EX-28`, `EX-29`, -`EX-42`) that got the h1 request/response cycle to the zero-alloc contract Phase 4 (`DEC-20`) left -open. +Audience: contributors. This is the design record for `dev.relism.flash.models`'s shared +request/response model: what is pooled, what that pooling means for callers, and how HTTP/1.1 and +HTTP/2 retain the same public contract. ## Why this exists @@ -103,15 +101,14 @@ sequence (`headerTags`/`headerRefs`) interleaves the two stores back into declar serialized, so mixing `header(String,String)` and `header(byte[])` calls on the same response still produces headers in the order they were added. -`PreEncodedHeader` precomputes a header's name+value ASCII bytes once (e.g. for a constant response -header set at boot) — deliberately does **not** yet expose HPACK-encoded bytes, since HPACK does -not exist until Phase 9; that scope boundary is recorded in the class's own Javadoc rather than -building untested, speculative API surface now. +`PreEncodedHeader` precomputes a header's name and value ASCII bytes once (for example, a constant +response header set at boot). Preserving the boundary lets HTTP/1.1 render a field line and HTTP/2 +encode the same pair through HPACK without a second public header type. `ResponseSerializer.forEachField(Response, FieldConsumer)` is the **one source of truth for what headers a response has** — it enumerates `Content-Type` (if set) plus every structured custom header, in order, and is the only place that knowledge lives. `Http1ResponseWriter` renders that -sequence as `Name: Value\r\n` lines; the Phase 9 h2 encoder will render the same sequence as HPACK. +sequence as `Name: Value\r\n` lines; `Http2ResponseWriter` renders the same sequence as HPACK. Deliberately excluded: `Content-Length`/`Connection`/`Date` (connection framing, not response object properties — and HTTP/2 has no `Connection` header at all, RFC 9113 §8.2.2) and raw `header(byte[])` entries (no recoverable name/value structure to hand the h2 encoder). @@ -141,8 +138,8 @@ byte-buffer-backed implementation, kept in `dev.relism.flash.models` rather than `dev.relism.flash.http1` — see `DECISIONS.md`, `DEC-22`, for why: `RequestParser` (root package) owns and constructs it, and `http1`→root already exists via `Http1Connection`, so moving it to `http1` would create a `models`↔`http1` package cycle). `RequestLine.headers` is typed as the -interface, so a future `Http2HeaderMap` (Phase 10, HPACK-backed) is a drop-in second -implementation, not a `Request`/`RequestLine` API change. +interface; `Http2HeaderMap` is the HPACK-backed second implementation without requiring a +`Request` or `RequestLine` API split. ## `ByteTemplate` (`EX-28`) diff --git a/flash/docs/http2/README.md b/flash/docs/http2/README.md new file mode 100644 index 0000000..a183d09 --- /dev/null +++ b/flash/docs/http2/README.md @@ -0,0 +1,51 @@ +# HTTP/2 in Flash + +Flash treats HTTP/1.1 and HTTP/2 as peer transports behind one connection boundary. TLS ALPN or +the cleartext prior-knowledge preface selects a protocol once; both paths then feed the same +router, `Request`, `Response`, handler, trailer, streaming and WebSocket APIs. HTTP/2 adds a +bounded frame decoder, HPACK codec, stream state machine, two-level flow control and one serialized +writer per connection. Application code does not branch on the wire protocol. + +The implementation is deliberately layered: + +```text +listener / TLS + -> protocol negotiation + -> HTTP/1.1 parser ---------+ + -> HTTP/2 frames + HPACK ----+-> shared request model -> router -> handler + shared response model + <- HTTP/1.1 serializer -----+ + <- HTTP/2 stream writer ----+ +``` + +## Start here + +- [HTTP/1.1 hardening](HTTP1-HARDENING.md) — message-boundary rules, timeouts and negotiation. +- [Transport](TRANSPORT.md) — listeners, connection ownership, TLS and virtual threads. +- [Message model](MESSAGE-MODEL.md) — shared request/response objects and their lifetime contract. +- [Connection](CONNECTION.md) and [streams](STREAMS.md) — HTTP/2 connection and stream state. +- [Flow control](FLOW-CONTROL.md) — request backpressure and streamed responses. +- [Trailers and streaming](TRAILERS-AND-STREAMING.md) — the public cross-protocol APIs. +- [Cleartext and proxying](CLEARTEXT-AND-PROXY.md) — prior knowledge and the upstream h2 client. +- [WebSockets](WEBSOCKET.md) — RFC 8441 extended CONNECT using the existing WebSocket API. + +## Wire internals + +- [Byte primitives](BYTES.md) — reusable views, scanning and bounded slice lifetimes. +- [Serialized writer](WRITER.md) — the single-owner output path and contention model. +- [Frames](FRAMES.md) — frame parsing, validation and error scope. +- [HPACK](HPACK.md) — integer/Huffman coding and static/dynamic table ownership. + +## Operate and verify + +- [Security](SECURITY.md) — every HTTP/2 limit, default and abuse control. +- [Troubleshooting](TROUBLESHOOTING.md) — GOAWAY/RST_STREAM diagnosis and protocol tracing. +- [Compliance](COMPLIANCE.md) — h2spec, interoperability, fuzzing and deliberate omissions. +- [Performance](PERFORMANCE.md) and [CI baselines](BASELINES.md) — measurements and regression + gates, including the comparison with nghttpd. + +## Design history + +[Decisions](DECISIONS.md) records non-obvious trade-offs and rejected alternatives. The +implementation plan is retained as historical engineering evidence; it is not required to use or +extend the runtime. diff --git a/flash/docs/http2/STREAMS.md b/flash/docs/http2/STREAMS.md index 031e4a6..2a60637 100644 --- a/flash/docs/http2/STREAMS.md +++ b/flash/docs/http2/STREAMS.md @@ -40,9 +40,9 @@ while dispatch is pending. ## Verification -- Phase 11 clean Maven build with JMH sources: 633 tests, no failures. -- h2spec sections 5 and 8: 39/39 after request DATA byte accounting landed. +- The complete clean Maven/JMH suite is recorded in `COMPLIANCE.md` and `PERFORMANCE.md`. +- h2spec sections 5 and 8 pass after request DATA byte accounting landed. - Java `HttpClient` negotiates HTTP/2 over TLS and runs an existing parameterized route unchanged. - curl prior-knowledge h2c receives a valid `200` response and body. -- Current JMH pooled lifecycle (HPACK decode, request assembly, response write and release): - 483.571 ns/op, 0.003 B/op, no GC. +- The pooled lifecycle (HPACK decode, request assembly, response write and release) remains a + zero-GC CI gate; current percentile and allocation baselines live in `BASELINES.md`. diff --git a/flash/docs/http2/TRANSPORT.md b/flash/docs/http2/TRANSPORT.md index fef5cdc..90b4e90 100644 --- a/flash/docs/http2/TRANSPORT.md +++ b/flash/docs/http2/TRANSPORT.md @@ -1,19 +1,19 @@ -# Transport Architecture (Phase 2) +# Transport architecture -Audience: contributors. This is the document Phase 3 onward extends as HTTP/2 grows a real -connection state machine behind the seam described here. +Audience: contributors. This document describes the shared listener and connection layer behind +the HTTP/1.1 and HTTP/2 implementations. ## Why this exists -Before Phase 2, `HttpServer` (563 lines) did bind, accept, virtual-thread dispatch, WebSocket +The original `HttpServer` (563 lines) did bind, accept, virtual-thread dispatch, WebSocket upgrade detection, WebSocket handshake, the WebSocket session loop, keep-alive detection, HTTP response serialization, chunked encoding, hex encoding, and decimal encoding — eleven reasons to change in one class (R6). It also held three `ThreadLocal`s that meant "one per connection" under virtual threads, not "one per core" (`EX-06`), and used `synchronized` around blocking socket writes in `WebSocketSession`, which pins a virtual thread's carrier on Java 21 (`EX-01`). -Phase 2 replaces it with named, single-responsibility components and the `ConnectionProtocol` -seam HTTP/2 will plug into starting Phase 8. +The current design replaces it with named, single-responsibility components and a +`ConnectionProtocol` seam implemented by both wire protocols. ## Package layout @@ -51,7 +51,7 @@ dev.relism.flash.websocket (existing package, extended) ``` TransportFactory.create(configuration, router, wsRouter) binds every configured listener (ListenerBinder) - builds one ConnectionRunner (shared virtual-thread executor, ScratchPool, Http1Connection) + builds one ConnectionRunner (shared virtual-thread executor, ScratchPool, both protocols) returns a ServerLifecycle (implements ServerHandle) ServerLifecycle.start() @@ -71,8 +71,8 @@ ConnectionRunner.handle(socket, stopped) if SSLSocket: force startHandshake() under headerReadTimeoutMs (EX-30) wrap streams: BufferedByteSource in, buffered OutputStream out, raw OutputStream rawOut negotiated = negotiateProtocol(socket, in) # ALPN or h2c preface - if negotiated == H2: return # no Http2Connection yet (Phase 8) -- close cleanly - build ConnectionContext, dispatch to http1Protocol.run(ctx) + build ConnectionContext + dispatch to http1Protocol.run(ctx) or http2Protocol.run(ctx) finally: activeSockets.remove(socket); scratchPool.release(scratch) ``` @@ -96,12 +96,8 @@ leak-free arena: above its bound (`min(availableProcessors * 64, 4096)` by defau scratch is simply dropped for the garbage collector rather than queued, so an unusually large burst of connections cannot grow it without limit. -The router's own `ThreadLocal`s (`FastPathRouterImpl`, `FastPathWsRouterImpl`) are **not** -removed in this phase — `EX-06`'s registry entry explicitly phases that part of the fix to -Phase 4, where the router also gains the API surface change (a scratch parameter, or reading -from the request's context) needed to remove them correctly. See `DECISIONS.md` (`DEC-15`) for -why Phase 2's Definition of Done was corrected to say so explicitly rather than silently drift -from the registry. +The routers use an explicit per-connection scratch passed through `AbstractRouter.route`; neither +`FastPathRouterImpl` nor `FastPathWsRouterImpl` retains connection state in a `ThreadLocal`. ## The `ConnectionProtocol` seam (R1 / `DEC-02`) @@ -112,10 +108,9 @@ public interface ConnectionProtocol { ``` `ConnectionRunner` decides h1 vs h2 exactly once, immediately after ALPN/preface detection, and -dispatches. Today only `Http1Connection` exists; an `H2` negotiation result is closed cleanly -(there is no `Http2Connection` to hand off to until Phase 8). Neither implementation is aware -the other exists — `dev.relism.flash.http1` and `dev.relism.flash.http2` do not import each other, -enforced by `PackageBoundaryTest`. +dispatches to `Http1Connection` or `Http2Connection`. Neither implementation is aware the other +exists — `dev.relism.flash.http1` and `dev.relism.flash.http2` do not import each other, enforced +by `PackageBoundaryTest`. ## Graceful shutdown (`EX-32`) @@ -130,7 +125,8 @@ Two stages, driven by `ServerLifecycle.stop()`: (the common case). `ServerLifecycle.stop()` polls `activeSockets` for up to `shutdownDrainTimeoutMs`, then force-closes whatever remains and shuts down the executor. -HTTP/2's half of this fix (a `GOAWAY` frame, RFC 9113 §6.8) lands in Phase 8. +HTTP/2 shutdown sends the two-stage `GOAWAY` sequence from RFC 9113 §6.8 before the lifecycle's +drain deadline force-closes remaining sockets. ## What changed for WebSocket (`EX-01`, `EX-11`, `EX-12`, `EX-13`) diff --git a/flash/docs/http2/TROUBLESHOOTING.md b/flash/docs/http2/TROUBLESHOOTING.md new file mode 100644 index 0000000..1e08fed --- /dev/null +++ b/flash/docs/http2/TROUBLESHOOTING.md @@ -0,0 +1,73 @@ +# HTTP/2 troubleshooting + +## Confirm which protocol was selected + +For TLS, the client and server must both offer `h2` through ALPN. Enable +`FlashConfiguration.http2Enabled`, use a certificate valid for the requested hostname, then check +with `curl --http2 -v https://host/path` or `nghttp -nv https://host/path`. The trace must report +ALPN `h2`; a successful HTTP/1.1 response usually means HTTP/2 was not enabled or the client did +not offer it. + +For plaintext, enable `http2CleartextEnabled` and use prior knowledge: + +```bash +curl --http2-prior-knowledge -v http://host:port/path +nghttp -nv http://host:port/path +``` + +Flash does not support `Upgrade: h2c`. A client configured for Upgrade rather than prior knowledge +will remain on HTTP/1.1. + +## Read GOAWAY and RST_STREAM + +GOAWAY terminates or drains a connection; `last_stream_id` identifies the highest client stream +the server may have processed. A client may retry a stream above that id only when its own request +semantics make retry safe. RST_STREAM affects one stream and leaves the connection usable. + +| Error | What it usually means | What to check | +|---|---|---| +| `NO_ERROR` | Graceful shutdown or connection rotation. | Server lifecycle and configured connection lifetime. | +| `PROTOCOL_ERROR` | Invalid preface, pseudo-header ordering, stream state or frame semantics. | A verbose frame trace and the first rejected stream. | +| `INTERNAL_ERROR` | Handler, response production or I/O failed unexpectedly. | The server exception immediately preceding stream cancellation. | +| `FLOW_CONTROL_ERROR` | A window overflow or DATA exceeded available credit. | Client flow-control implementation and SETTINGS deltas. | +| `SETTINGS_TIMEOUT` | The peer did not complete required SETTINGS progress. | Network stalls or a non-compliant peer. | +| `STREAM_CLOSED` | A frame targeted a stream whose remote side or whole lifecycle was closed. | Late DATA/HEADERS and duplicate terminal frames. | +| `FRAME_SIZE_ERROR` | A frame length violated its type or the negotiated maximum. | The nine-byte frame header and peer frame-size configuration. | +| `REFUSED_STREAM` | Live or pending-output capacity was temporarily exhausted. | Client concurrency versus the advertised maximum; retry only when safe. | +| `CANCEL` | The request, handler or streamed response was cancelled. | Client cancellation and application producer logs. | +| `COMPRESSION_ERROR` | HPACK integer, Huffman, index or table update was invalid. | Header-block bytes and whether an intermediary rewrote them. | +| `CONNECT_ERROR` | A CONNECT tunnel failed. | Upstream tunnel or extended-CONNECT negotiation. | +| `ENHANCE_YOUR_CALM` | A configured abuse, rate, header, body or queue bound was exceeded. | [Security controls](SECURITY.md) and traffic rate before increasing a limit. | +| `INADEQUATE_SECURITY` | TLS does not meet HTTP/2 requirements. | TLS version, cipher suite and ALPN configuration. | +| `HTTP_1_1_REQUIRED` | The peer should retry using HTTP/1.1. | Protocol policy and intermediary compatibility. | + +Flash caps GOAWAY debug data, and clients must not depend on it being present. The numeric error +code and last stream id are the reliable diagnostic fields. + +## Capture a frame trace + +Flash does not log every frame in production: frame logs leak header and traffic metadata and add +work to the hottest connection loop. Reproduce against a verbose client instead: + +```bash +nghttp -nv https://host/path +curl --http2 -v https://host/path +``` + +`nghttp -nv` prints SETTINGS, HEADERS, DATA, WINDOW_UPDATE, RST_STREAM and GOAWAY in wire order. For +a server-side-only failure, capture the connection with an approved packet tool; TLS traffic must +be decrypted in a controlled environment. Never attach production header blocks or payloads to a +ticket without redacting credentials and personal data. + +## Common misconfiguration patterns + +1. **HTTP/2 switch disabled.** `http2Enabled` controls TLS ALPN and + `http2CleartextEnabled` controls prior knowledge independently. +2. **Wrong cleartext mode.** The client sends `Upgrade: h2c`; Flash expects the RFC 9113 prior- + knowledge preface on the shared plaintext listener. +3. **ALPN or certificate mismatch.** A custom `TlsConfig.ofContext` omits `h2`, or hostname + verification rejects the certificate before HTTP/2 starts. Inspect the TLS handshake first. + +If a connection closes under load rather than at startup, compare the observed rate and retained +stream count with [the security defaults](SECURITY.md), especially reset/stream creation budgets, +the 64 concurrent-stream setting, header assembly time and stream idle time. diff --git a/flash/docs/http2/WRITER.md b/flash/docs/http2/WRITER.md index 5af0729..010eba2 100644 --- a/flash/docs/http2/WRITER.md +++ b/flash/docs/http2/WRITER.md @@ -1,13 +1,10 @@ -# The Serialized Frame Writer (Phase 3 — GO/NO-GO gate) +# The serialized frame writer Audience: contributors. This is the design record and benchmark evidence for `dev.relism.flash.http2.frame.Http2FrameWriter`, the one component every HTTP/2 write in this -codebase passes through. Phase 3 of `IMPLEMENTATION-PLAN.md` treats this component as the -single genuinely novel architectural risk in the whole project — everything downstream (frames, -HPACK, flow control) is table-driven work with known cost, but nothing in Flash today -coordinates concurrent writers onto one socket. If this component could not deliver, the plan -says to stop here having spent one phase, not ten. It delivered: **GO**, see the gate table at -the end of this document. +codebase passes through. It was the central architectural risk: frames, HPACK and flow control are +table-driven, but multiplexed streams require concurrent producers to share one socket without +interleaving bytes or pinning carrier threads. The measured gate is recorded below. ## The problem, precisely @@ -27,7 +24,7 @@ has already built its complete frame — header, HPACK block, payload — into a writer never serializes anything; it holds the lock only for the duration of one bulk `sink.write(buffer, offset, length)` call, never for a sequence of small writes. This is why `EX-27` (collapsing `HttpServer.writeResponse`'s ~10 small writes into one) is a prerequisite for -h1 too, landing in Phase 6. +HTTP/1.1 too; `Http1ResponseWriter` now follows the same bulk-write discipline. **Layer 2 — `ReentrantLock`, never `synchronized`.** On Java 21, a virtual thread that blocks inside a `synchronized` block pins its carrier platform thread (JEP 491, which removes this, @@ -260,9 +257,8 @@ design's exclusive use of `ReentrantLock` (never `synchronized`) on every path t | 3 | No carrier pinning under `-Djdk.tracePinnedThreads=full` | none observed | **PASS** | | 4 | Stress test green at every N, 1000 iterations, incl. parallelism=1 | 10 000/10 000 | **PASS** | -**All four gate criteria are met. Verdict: GO.** `Http2FrameWriter` ships as designed — -`tryLock()` fast path, intrusive MPSC fallback — and Phase 4 may proceed. See `DECISIONS.md`, -`DEC-09`, for the decision-log entry recording this outcome alongside the plan's other decisions. +**All four gate criteria are met.** `Http2FrameWriter` ships as designed: a `tryLock()` fast path +with an intrusive MPSC fallback. See `DECISIONS.md` for the retained alternatives and evidence. ## What this design costs vs. what it saves diff --git a/flash/src/main/java/dev/relism/flash/bytes/ArrayBackedByteView.java b/flash/src/main/java/dev/relism/flash/bytes/ArrayBackedByteView.java index 018c9ee..bfed512 100644 --- a/flash/src/main/java/dev/relism/flash/bytes/ArrayBackedByteView.java +++ b/flash/src/main/java/dev/relism/flash/bytes/ArrayBackedByteView.java @@ -24,7 +24,7 @@ import dev.relism.fpr.core.ByteView; * to be copied. * * Code that only has a bare {@link ByteView} (e.g. because it received one across the - * {@link SegmentedByteView} boundary, from a future HPACK CONTINUATION-spanning block) keeps the + * {@link SegmentedByteView} boundary) keeps the * byte-at-a-time fallback — this interface is an opportunistic fast path, never a requirement. */ public interface ArrayBackedByteView extends ByteView { diff --git a/flash/src/main/java/dev/relism/flash/extension/AnnotationProcessor.java b/flash/src/main/java/dev/relism/flash/extension/AnnotationProcessor.java index b5ddc26..db38882 100644 --- a/flash/src/main/java/dev/relism/flash/extension/AnnotationProcessor.java +++ b/flash/src/main/java/dev/relism/flash/extension/AnnotationProcessor.java @@ -7,7 +7,7 @@ import java.util.List; /** * Inspects a handler class at registration time and returns zero or more - * {@link Middleware middlewares} to inject automatically. + * {@link MiddlewareNode middleware nodes} to inject automatically. * *

Processors are called once per register call, before * the handler is compiled into the router. Returning an empty list is always diff --git a/flash/src/main/java/dev/relism/flash/extension/PackageScanner.java b/flash/src/main/java/dev/relism/flash/extension/PackageScanner.java index ce378d4..7c1393f 100644 --- a/flash/src/main/java/dev/relism/flash/extension/PackageScanner.java +++ b/flash/src/main/java/dev/relism/flash/extension/PackageScanner.java @@ -2,8 +2,9 @@ package dev.relism.flash.extension; import dev.relism.flash.exceptions.InitializationException; import dev.relism.flash.models.RequestHandler; -import dev.relism.flash.routing.Ws; +import dev.relism.flash.routing.Route; import dev.relism.flash.routing.Routes; +import dev.relism.flash.routing.Ws; import dev.relism.flash.websocket.WebSocketEndpoint; import java.io.File; diff --git a/flash/src/main/java/dev/relism/flash/http/ContentType.java b/flash/src/main/java/dev/relism/flash/http/ContentType.java index 432b9e2..365e483 100644 --- a/flash/src/main/java/dev/relism/flash/http/ContentType.java +++ b/flash/src/main/java/dev/relism/flash/http/ContentType.java @@ -7,7 +7,7 @@ import java.util.Arrays; import lombok.Getter; /** - * Pre-compiled byte representations of common HTTP {@code Content-Type} values. {@link #getBytes()} + * Pre-compiled byte representations of common HTTP {@code Content-Type} values. {@code getBytes()} * returns the pre-computed array directly, never allocates. */ @Getter diff --git a/flash/src/main/java/dev/relism/flash/http2/Http2Limits.java b/flash/src/main/java/dev/relism/flash/http2/Http2Limits.java index abca22e..1fc31cd 100644 --- a/flash/src/main/java/dev/relism/flash/http2/Http2Limits.java +++ b/flash/src/main/java/dev/relism/flash/http2/Http2Limits.java @@ -200,7 +200,7 @@ public final class Http2Limits { /** * Maximum time, in milliseconds, {@code Http2FrameReader} may wait for a single frame's - * header and payload to fully arrive. Bounds the same slowloris-shaped hazard {@code + * header and payload to fully arrive. Bounds the same slowloris-shaped hazard: * without it, a peer that sends 9 header bytes and then never sends the declared payload * would hold this connection's frame reader waiting forever. */ diff --git a/flash/src/main/java/dev/relism/flash/models/RequestBody.java b/flash/src/main/java/dev/relism/flash/models/RequestBody.java index f60a225..1104273 100644 --- a/flash/src/main/java/dev/relism/flash/models/RequestBody.java +++ b/flash/src/main/java/dev/relism/flash/models/RequestBody.java @@ -11,7 +11,7 @@ import java.io.*; * bodies larger than 2 GB. *

  • {@link #stream()} — returns a bounded {@link InputStream} without upfront allocation. * into the already-buffered header bytes stitched to the socket; for chunked bodies it is - * the raw {@link dev.relism.ChunkedInputStream} that de-chunks on the fly.
  • + * the raw {@link dev.relism.flash.ChunkedInputStream} that de-chunks on the fly. * * *

    Mutual exclusivity: calling both {@code bytes()} and {@code stream()} on the same @@ -134,7 +134,7 @@ public class RequestBody { *

    For fixed-length bodies: a reused {@link BoundedBufferedInputStream} (see the class * view of the socket stream — zero allocation on a warm connection. * - *

    For chunked bodies: the raw {@link dev.relism.ChunkedInputStream} that de-chunks on + *

    For chunked bodies: the raw {@link dev.relism.flash.ChunkedInputStream} that de-chunks on * the fly; EOF signals the end of the logical body and leaves the socket positioned for * the next keep-alive request. *