docs(core): document HTTP/2 operation and architecture
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
+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
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
+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
|
||||
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`
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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`)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
- 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`.
|
||||
|
||||
@@ -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`)
|
||||
|
||||
|
||||
@@ -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
|
||||
`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
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import dev.relism.fpr.core.ByteView;
|
||||
* to be copied.</li>
|
||||
* </ul>
|
||||
* 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 {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Processors are called once per register call, before
|
||||
* 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.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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -11,7 +11,7 @@ import java.io.*;
|
||||
* bodies larger than 2 GB.</li>
|
||||
* <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
|
||||
* 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>
|
||||
*
|
||||
* <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
|
||||
* 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 next keep-alive request.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user