feat(core): HTTP/2 support, correctness fixes, and doc reorganization #10
@@ -1,12 +1,13 @@
|
|||||||
# Flash
|
# 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
|
## Modules
|
||||||
|
|
||||||
| Module | Description |
|
| 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-jackson` | Jackson JSON integration |
|
||||||
| `flash-extensions/flash-ext-openapi` | OpenAPI 3.0 spec + Swagger UI |
|
| `flash-extensions/flash-ext-openapi` | OpenAPI 3.0 spec + Swagger UI |
|
||||||
| `flash-extensions/flash-ext-oidc` | OIDC Authorization Code + PKCE flow |
|
| `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) -> {
|
app.get("/users/{id}", (req, res) -> {
|
||||||
String id = req.pathParam("id");
|
String id = req.param("id");
|
||||||
return "user:" + 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. |
|
| `shutdownDrainTimeoutMs` | `15000` | How long graceful shutdown waits for in-flight requests before force-closing. |
|
||||||
| `http2Enabled` | `false` | Whether TLS listeners advertise HTTP/2 through ALPN. |
|
| `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. |
|
| `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. |
|
| `h2MaxResetStreamsPerInterval` | `200` | Rapid Reset budget per rolling interval. |
|
||||||
| `h2MaxStreamsCreatedPerInterval` | `400` | New-stream budget per rolling interval. |
|
| `h2MaxStreamsCreatedPerInterval` | `400` | New-stream budget per rolling interval. |
|
||||||
| `h2AbuseRateIntervalMs` | `10000` | Rolling interval for the two operator-tunable rate limits above. |
|
| `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. |
|
| `h2MaxBytesPerConnection` | `0` | Optional total wire-byte budget; `0` disables it. |
|
||||||
| `h2MaxConnectionLifetimeMs` | `0` | Optional connection lifetime; `0` disables it. |
|
| `h2MaxConnectionLifetimeMs` | `0` | Optional connection lifetime; `0` disables it. |
|
||||||
| `h2StreamIdleTimeoutMs` | `60000` | Inactive open-stream deadline. |
|
| `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
|
## WebSockets over HTTP/2
|
||||||
|
|
||||||
|
|||||||
+16
-17
@@ -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
|
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
|
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
|
│ ├── FastPathViews.StringByteView a String's UTF-8 bytes
|
||||||
│ └── PooledSlice the EX-05 reusable, pool-issued slice
|
│ └── PooledSlice the EX-05 reusable, pool-issued slice
|
||||||
└── (bare ByteView, not array-backed)
|
└── (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
|
└── 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
|
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.
|
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
|
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,
|
performing 6–10 lookups per request. `RequestParser` now populates the index while it validates
|
||||||
recording per-header `(nameOffset, nameLength, valueOffset, valueLength)` and a case-insensitive
|
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
|
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`
|
(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)`,
|
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
|
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
|
name comparison. Production therefore performs one combined validation/index pass rather than
|
||||||
once at parse time (`RequestParser`'s own validation pass) — two scans total, replacing "one scan
|
one parse-time pass plus one rescan per lookup. `forEach` uses the same index rather than keeping
|
||||||
at parse time plus one rescan per lookup" — strictly less work even for a single lookup, and much
|
an independent scanner.
|
||||||
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.
|
|
||||||
|
|
||||||
## `EX-05`: pooled slices
|
## `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
|
`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
|
(`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,
|
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
|
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:
|
different bytes. This is a real, demonstrated hazard, not a hypothetical one:
|
||||||
`SlicePoolTest#wraparoundAliasesThePreviouslyReturnedSlice` and the analogous tests in
|
`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.
|
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
|
`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
|
— 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
|
request (or, for `PathParams`'s `FastPathRouterImpl`-owned reusable instance, once per
|
||||||
connection) regardless of whether `view()` is ever invoked; an eager pool would add
|
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
|
`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 `HeaderMap` handles headers) and is
|
pool, by contrast, is unconditionally useful (every request's map handles headers) and is
|
||||||
constructed eagerly for simplicity.
|
constructed eagerly for simplicity.
|
||||||
|
|
||||||
**Two documented, deliberately-kept exceptions to "no `new ByteView()` remains"**: `QueryParams.view`
|
**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
|
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,
|
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
|
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.
|
— it is always buffer-backed by construction.
|
||||||
|
|
||||||
## `EX-19`/`EX-06` (router half): the `FastPathRouterImpl` scratch
|
## `EX-19`/`EX-06` (router half): the `FastPathRouterImpl` scratch
|
||||||
|
|||||||
@@ -1142,3 +1142,24 @@ makes no "unmatched" claim.
|
|||||||
noise floor if a profiler can distinguish harness allocation from benchmark allocation exactly.
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
+10
-10
@@ -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
|
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
|
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
|
├── 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
|
├── Padding RFC 9113 §6.1/§6.2 pad-length byte + trailing padding, DATA/HEADERS
|
||||||
├── FrameWriteBuffer beginFrame()/endFrame() length back-patching over a ByteWriter
|
├── FrameWriteBuffer beginFrame()/endFrame() length back-patching over a ByteWriter
|
||||||
├── Http2FrameWriter (Phase 3) the connection's single serialized writer — unchanged here
|
├── Http2FrameWriter the connection's single serialized writer
|
||||||
├── WriteIntent (Phase 3) unchanged
|
├── WriteIntent caller-owned serialized frame batch
|
||||||
└── IntrusiveMpscQueue (Phase 3) unchanged
|
└── IntrusiveMpscQueue allocation-free contended-write queue
|
||||||
```
|
```
|
||||||
|
|
||||||
## The validation table
|
## 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 |
|
| Type | Code | Length | Stream id | Notes / RFC |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| DATA | 0x0 | 0..MAX_FRAME_SIZE | required (≠0) | §6.1. Padding via `Padding.unpad`. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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
|
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
|
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`
|
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).
|
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
|
`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
|
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
|
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
|
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
|
*whole* payload counts against the window, not just the data) is applied by
|
||||||
locates the data range, it performs no window bookkeeping itself.
|
`Http2FlowController`; `Padding` only locates the data range.
|
||||||
|
|
||||||
## Writing: `FrameWriteBuffer`'s back-patching
|
## 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
|
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
|
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
|
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
|
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
|
produced — streaming would need the length upfront, which back-patching deliberately avoids
|
||||||
needing.
|
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
|
`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`
|
unit tests and an unconditional `socket.setSoTimeout(...)` call that NPE'd against the `null`
|
||||||
|
|||||||
@@ -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
|
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
|
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` |
|
| `MAX_TRAILER_COUNT` | 50 | `431` |
|
||||||
| A chunk's data not followed by `\r\n`, or a malformed chunk-size/trailer terminator | — | `400` |
|
| 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
|
Trailers are consumed within the bounds above and exposed through `Request.trailers()` on both
|
||||||
handler, on HTTP/1.1 today — exposing them via `Request.trailers()` on both protocols is Phase
|
HTTP/1.1 and HTTP/2.
|
||||||
12 scope.
|
|
||||||
|
|
||||||
## Timeouts (`FlashConfiguration`)
|
## 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. |
|
| `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. |
|
| `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. |
|
| `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
|
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
|
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
|
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
|
(`TlsConfig.TLS12_H2_BLOCKED_CIPHERS`, ~280 entries). TLS 1.3 is never affected — none of its
|
||||||
cipher suites are on that list.
|
cipher suites are on that list.
|
||||||
- HTTP/2 itself is not yet served in this phase (lands in Phase 8): a connection that negotiates
|
- `FlashConfiguration.http2Enabled` advertises `h2` on TLS listeners. The independent
|
||||||
`h2` via ALPN, or that opens with the h2c prior-knowledge preface while
|
`http2CleartextEnabled` switch accepts the h2c prior-knowledge preface on plaintext listeners.
|
||||||
`FlashConfiguration.http2Enabled` is set, is currently closed cleanly rather than served.
|
|
||||||
|
|||||||
@@ -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. |
|
| 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. |
|
| 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. |
|
| 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
|
`TCP_NODELAY` on both cleartext and TLS sockets before protocol exchange. A socket-option
|
||||||
regression test covers the shared configuration method. **Phase**: 17.
|
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
|
# 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.
|
that confidently states something false is worse than no comment.
|
||||||
|
|
||||||
### DoD
|
### DoD
|
||||||
- [ ] Every document listed above exists and is accurate.
|
- [x] Every document listed above exists, local Markdown links resolve, and stale future-tense
|
||||||
- [ ] `mvn javadoc:javadoc` produces no warnings.
|
descriptions were reconciled with the implemented architecture.
|
||||||
- [ ] A reader who knows HTTP/1.1 and nothing about HTTP/2 can read `flash/docs/http2/README.md` and
|
- [x] A clean `mvn -pl flash -am clean javadoc:javadoc` produces no warnings.
|
||||||
understand the architecture. (Verify by having someone who did not implement it read it.)
|
- [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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
Audience: contributors. This is the design record for `dev.relism.flash.models`'s shared
|
||||||
object model after Phase 6's refactor — what is pooled, what that pooling actually means for code
|
request/response model: what is pooled, what that pooling means for callers, and how HTTP/1.1 and
|
||||||
that touches these objects, and the allocation fixes (`EX-20`–`EX-24`, `EX-27`, `EX-28`, `EX-29`,
|
HTTP/2 retain the same public contract.
|
||||||
`EX-42`) that got the h1 request/response cycle to the zero-alloc contract Phase 4 (`DEC-20`) left
|
|
||||||
open.
|
|
||||||
|
|
||||||
## Why this exists
|
## 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
|
serialized, so mixing `header(String,String)` and `header(byte[])` calls on the same response still
|
||||||
produces headers in the order they were added.
|
produces headers in the order they were added.
|
||||||
|
|
||||||
`PreEncodedHeader` precomputes a header's name+value ASCII bytes once (e.g. for a constant response
|
`PreEncodedHeader` precomputes a header's name and value ASCII bytes once (for example, a constant
|
||||||
header set at boot) — deliberately does **not** yet expose HPACK-encoded bytes, since HPACK does
|
response header set at boot). Preserving the boundary lets HTTP/1.1 render a field line and HTTP/2
|
||||||
not exist until Phase 9; that scope boundary is recorded in the class's own Javadoc rather than
|
encode the same pair through HPACK without a second public header type.
|
||||||
building untested, speculative API surface now.
|
|
||||||
|
|
||||||
`ResponseSerializer.forEachField(Response, FieldConsumer)` is the **one source of truth for what
|
`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
|
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
|
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
|
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
|
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).
|
`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)
|
`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
|
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
|
`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
|
interface; `Http2HeaderMap` is the HPACK-backed second implementation without requiring a
|
||||||
implementation, not a `Request`/`RequestLine` API change.
|
`Request` or `RequestLine` API split.
|
||||||
|
|
||||||
## `ByteTemplate` (`EX-28`)
|
## `ByteTemplate` (`EX-28`)
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -40,9 +40,9 @@ while dispatch is pending.
|
|||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
- Phase 11 clean Maven build with JMH sources: 633 tests, no failures.
|
- The complete clean Maven/JMH suite is recorded in `COMPLIANCE.md` and `PERFORMANCE.md`.
|
||||||
- h2spec sections 5 and 8: 39/39 after request DATA byte accounting landed.
|
- 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.
|
- 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.
|
- curl prior-knowledge h2c receives a valid `200` response and body.
|
||||||
- Current JMH pooled lifecycle (HPACK decode, request assembly, response write and release):
|
- The pooled lifecycle (HPACK decode, request assembly, response write and release) remains a
|
||||||
483.571 ns/op, 0.003 B/op, no GC.
|
zero-GC CI gate; current percentile and allocation baselines live in `BASELINES.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
|
Audience: contributors. This document describes the shared listener and connection layer behind
|
||||||
connection state machine behind the seam described here.
|
the HTTP/1.1 and HTTP/2 implementations.
|
||||||
|
|
||||||
## Why this exists
|
## 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
|
upgrade detection, WebSocket handshake, the WebSocket session loop, keep-alive detection, HTTP
|
||||||
response serialization, chunked encoding, hex encoding, and decimal encoding — eleven reasons to
|
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
|
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
|
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`).
|
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`
|
The current design replaces it with named, single-responsibility components and a
|
||||||
seam HTTP/2 will plug into starting Phase 8.
|
`ConnectionProtocol` seam implemented by both wire protocols.
|
||||||
|
|
||||||
## Package layout
|
## Package layout
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ dev.relism.flash.websocket (existing package, extended)
|
|||||||
```
|
```
|
||||||
TransportFactory.create(configuration, router, wsRouter)
|
TransportFactory.create(configuration, router, wsRouter)
|
||||||
binds every configured listener (ListenerBinder)
|
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)
|
returns a ServerLifecycle (implements ServerHandle)
|
||||||
|
|
||||||
ServerLifecycle.start()
|
ServerLifecycle.start()
|
||||||
@@ -71,8 +71,8 @@ ConnectionRunner.handle(socket, stopped)
|
|||||||
if SSLSocket: force startHandshake() under headerReadTimeoutMs (EX-30)
|
if SSLSocket: force startHandshake() under headerReadTimeoutMs (EX-30)
|
||||||
wrap streams: BufferedByteSource in, buffered OutputStream out, raw OutputStream rawOut
|
wrap streams: BufferedByteSource in, buffered OutputStream out, raw OutputStream rawOut
|
||||||
negotiated = negotiateProtocol(socket, in) # ALPN or h2c preface
|
negotiated = negotiateProtocol(socket, in) # ALPN or h2c preface
|
||||||
if negotiated == H2: return # no Http2Connection yet (Phase 8) -- close cleanly
|
build ConnectionContext
|
||||||
build ConnectionContext, dispatch to http1Protocol.run(ctx)
|
dispatch to http1Protocol.run(ctx) or http2Protocol.run(ctx)
|
||||||
finally:
|
finally:
|
||||||
activeSockets.remove(socket); scratchPool.release(scratch)
|
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
|
scratch is simply dropped for the garbage collector rather than queued, so an unusually large
|
||||||
burst of connections cannot grow it without limit.
|
burst of connections cannot grow it without limit.
|
||||||
|
|
||||||
The router's own `ThreadLocal`s (`FastPathRouterImpl`, `FastPathWsRouterImpl`) are **not**
|
The routers use an explicit per-connection scratch passed through `AbstractRouter.route`; neither
|
||||||
removed in this phase — `EX-06`'s registry entry explicitly phases that part of the fix to
|
`FastPathRouterImpl` nor `FastPathWsRouterImpl` retains connection state in a `ThreadLocal`.
|
||||||
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 `ConnectionProtocol` seam (R1 / `DEC-02`)
|
## 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
|
`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
|
dispatches to `Http1Connection` or `Http2Connection`. Neither implementation is aware the other
|
||||||
(there is no `Http2Connection` to hand off to until Phase 8). Neither implementation is aware
|
exists — `dev.relism.flash.http1` and `dev.relism.flash.http2` do not import each other, enforced
|
||||||
the other exists — `dev.relism.flash.http1` and `dev.relism.flash.http2` do not import each other,
|
by `PackageBoundaryTest`.
|
||||||
enforced by `PackageBoundaryTest`.
|
|
||||||
|
|
||||||
## Graceful shutdown (`EX-32`)
|
## Graceful shutdown (`EX-32`)
|
||||||
|
|
||||||
@@ -130,7 +125,8 @@ Two stages, driven by `ServerLifecycle.stop()`:
|
|||||||
(the common case). `ServerLifecycle.stop()` polls `activeSockets` for up to
|
(the common case). `ServerLifecycle.stop()` polls `activeSockets` for up to
|
||||||
`shutdownDrainTimeoutMs`, then force-closes whatever remains and shuts down the executor.
|
`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`)
|
## What changed for WebSocket (`EX-01`, `EX-11`, `EX-12`, `EX-13`)
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
|
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
|
`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
|
codebase passes through. It was the central architectural risk: frames, HPACK and flow control are
|
||||||
single genuinely novel architectural risk in the whole project — everything downstream (frames,
|
table-driven, but multiplexed streams require concurrent producers to share one socket without
|
||||||
HPACK, flow control) is table-driven work with known cost, but nothing in Flash today
|
interleaving bytes or pinning carrier threads. The measured gate is recorded below.
|
||||||
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.
|
|
||||||
|
|
||||||
## The problem, precisely
|
## 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
|
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
|
`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
|
`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
|
**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,
|
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** |
|
| 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** |
|
| 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 —
|
**All four gate criteria are met.** `Http2FrameWriter` ships as designed: a `tryLock()` fast path
|
||||||
`tryLock()` fast path, intrusive MPSC fallback — and Phase 4 may proceed. See `DECISIONS.md`,
|
with an intrusive MPSC fallback. See `DECISIONS.md` for the retained alternatives and evidence.
|
||||||
`DEC-09`, for the decision-log entry recording this outcome alongside the plan's other decisions.
|
|
||||||
|
|
||||||
## What this design costs vs. what it saves
|
## What this design costs vs. what it saves
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import dev.relism.fpr.core.ByteView;
|
|||||||
* to be copied.</li>
|
* to be copied.</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* Code that only has a bare {@link ByteView} (e.g. because it received one across the
|
* 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.
|
* byte-at-a-time fallback — this interface is an opportunistic fast path, never a requirement.
|
||||||
*/
|
*/
|
||||||
public interface ArrayBackedByteView extends ByteView {
|
public interface ArrayBackedByteView extends ByteView {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import java.util.List;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Inspects a handler class at registration time and returns zero or more
|
* 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.
|
||||||
*
|
*
|
||||||
* <p>Processors are called once per register call, before
|
* <p>Processors are called once per register call, before
|
||||||
* the handler is compiled into the router. Returning an empty list is always
|
* the handler is compiled into the router. Returning an empty list is always
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package dev.relism.flash.extension;
|
|||||||
|
|
||||||
import dev.relism.flash.exceptions.InitializationException;
|
import dev.relism.flash.exceptions.InitializationException;
|
||||||
import dev.relism.flash.models.RequestHandler;
|
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.Routes;
|
||||||
|
import dev.relism.flash.routing.Ws;
|
||||||
import dev.relism.flash.websocket.WebSocketEndpoint;
|
import dev.relism.flash.websocket.WebSocketEndpoint;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import java.util.Arrays;
|
|||||||
import lombok.Getter;
|
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.
|
* returns the pre-computed array directly, never allocates.
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ public final class Http2Limits {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum time, in milliseconds, {@code Http2FrameReader} may wait for a single frame's
|
* 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
|
* 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.
|
* would hold this connection's frame reader waiting forever.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import java.io.*;
|
|||||||
* bodies larger than 2 GB.</li>
|
* bodies larger than 2 GB.</li>
|
||||||
* <li>{@link #stream()} — returns a bounded {@link InputStream} without upfront allocation.
|
* <li>{@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
|
* 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.</li>
|
* the raw {@link dev.relism.flash.ChunkedInputStream} that de-chunks on the fly.</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* <p><b>Mutual exclusivity:</b> calling both {@code bytes()} and {@code stream()} on the same
|
* <p><b>Mutual exclusivity:</b> calling both {@code bytes()} and {@code stream()} on the same
|
||||||
@@ -134,7 +134,7 @@ public class RequestBody {
|
|||||||
* <p>For fixed-length bodies: a reused {@link BoundedBufferedInputStream} (see the class
|
* <p>For fixed-length bodies: a reused {@link BoundedBufferedInputStream} (see the class
|
||||||
* view of the socket stream — zero allocation on a warm connection.
|
* view of the socket stream — zero allocation on a warm connection.
|
||||||
*
|
*
|
||||||
* <p>For chunked bodies: the raw {@link dev.relism.ChunkedInputStream} that de-chunks on
|
* <p>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 fly; EOF signals the end of the logical body and leaves the socket positioned for
|
||||||
* the next keep-alive request.
|
* the next keep-alive request.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user