From 885c450f6ba113f8a077721b28ca451cc3e9e163 Mon Sep 17 00:00:00 2001 From: Zakaria El Orche Date: Thu, 13 Aug 2026 16:24:23 +0000 Subject: [PATCH] refactor(core): unify HTTP protocol package boundaries --- flash/docs/http2/DECISIONS.md | 10 +-- flash/docs/http2/FRAMES.md | 4 +- flash/docs/http2/IMPLEMENTATION-PLAN.md | 52 ++++++------- flash/docs/http2/TRANSPORT.md | 2 +- flash/docs/http2/WRITER.md | 4 +- .../frame/FrameLayerBenchmark.java | 2 +- .../frame/FrameWriterBenchmark.java | 2 +- .../dev/relism/flash/ChunkedInputStream.java | 2 - .../java/dev/relism/flash/RequestParser.java | 15 ---- .../java/dev/relism/flash/ServerHandle.java | 1 - .../relism/flash/api/multipart/Multipart.java | 5 -- .../flash/bytes/ArrayBackedByteView.java | 1 - .../java/dev/relism/flash/bytes/ByteScan.java | 7 -- .../dev/relism/flash/bytes/ByteWriter.java | 21 ++---- .../dev/relism/flash/bytes/PooledSlice.java | 1 - .../relism/flash/bytes/SegmentedByteView.java | 1 - .../dev/relism/flash/bytes/SlicePool.java | 1 - .../exceptions/MalformedRequestException.java | 2 - .../flash/extension/FlashConfiguration.java | 6 -- .../dev/relism/flash/h2/package-info.java | 73 ------------------- .../dev/relism/flash/http/DateHeader.java | 3 - .../dev/relism/flash/http/Http1Limits.java | 15 +--- .../dev/relism/flash/http/HttpStatus.java | 1 - .../relism/flash/http1/Http1Connection.java | 6 -- .../relism/flash/http1/Http1KeepAlive.java | 1 - .../flash/http1/Http1ResponseWriter.java | 6 -- .../flash/{h2 => http2}/Http2ErrorCode.java | 3 +- .../flash/{h2 => http2}/Http2Exception.java | 5 +- .../flash/{h2 => http2}/Http2Limits.java | 21 ++---- .../{h2 => http2}/Http2StreamException.java | 5 +- .../flash/{h2 => http2}/frame/FrameFlags.java | 2 +- .../{h2 => http2}/frame/FrameHeader.java | 3 +- .../flash/{h2 => http2}/frame/FrameType.java | 4 +- .../{h2 => http2}/frame/FrameValidator.java | 10 +-- .../{h2 => http2}/frame/FrameWriteBuffer.java | 3 +- .../{h2 => http2}/frame/Http2FrameReader.java | 8 +- .../{h2 => http2}/frame/Http2FrameWriter.java | 13 +--- .../frame/IntrusiveMpscQueue.java | 2 +- .../flash/{h2 => http2}/frame/Padding.java | 7 +- .../{h2 => http2}/frame/WriteIntent.java | 2 +- .../dev/relism/flash/models/HeaderView.java | 16 ++-- .../relism/flash/models/Http1HeaderMap.java | 9 --- .../dev/relism/flash/models/PathParams.java | 5 -- .../relism/flash/models/PreEncodedHeader.java | 4 +- .../dev/relism/flash/models/QueryParams.java | 3 - .../java/dev/relism/flash/models/Request.java | 5 -- .../dev/relism/flash/models/RequestBody.java | 9 --- .../dev/relism/flash/models/RequestLine.java | 1 - .../dev/relism/flash/models/Response.java | 15 +--- .../flash/models/ResponseSerializer.java | 6 +- .../relism/flash/routing/AbstractRouter.java | 2 - .../flash/routing/AbstractWsRouter.java | 1 - .../fastpathrouter/FastPathRouterImpl.java | 3 - .../routers/fastpathrouter/FastPathViews.java | 8 -- .../fastpathrouter/FastPathWsRouterImpl.java | 6 +- .../relism/flash/template/ByteTemplate.java | 1 - .../java/dev/relism/flash/tls/TlsConfig.java | 5 -- .../flash/transport/BufferedByteSource.java | 4 - .../flash/transport/ConnectionContext.java | 1 - .../flash/transport/ConnectionProtocol.java | 1 - .../flash/transport/ConnectionRunner.java | 6 +- .../flash/transport/ConnectionScratch.java | 5 -- .../flash/transport/NegotiatedProtocol.java | 3 +- .../flash/transport/ProtocolNegotiator.java | 14 ++-- .../flash/transport/ServerLifecycle.java | 2 - .../flash/transport/TransportFactory.java | 1 - .../relism/flash/websocket/WebSocketLoop.java | 2 - .../flash/websocket/WebSocketSession.java | 4 +- .../flash/websocket/WebSocketUpgrade.java | 2 - .../relism/flash/ChunkedInputStreamTest.java | 2 - .../relism/flash/HttpServerTimeoutTest.java | 2 - .../flash/RequestParserSecurityTest.java | 7 -- .../dev/relism/flash/RequestParserTest.java | 5 -- .../flash/api/multipart/MultipartTest.java | 1 - .../architecture/PackageBoundaryTest.java | 22 ++---- .../relism/flash/bytes/ByteScanFuzzTest.java | 1 - .../dev/relism/flash/http/HttpStatusTest.java | 1 - .../flash/http1/Http1ResponseWriterTest.java | 5 -- .../{h2 => http2}/Http2ErrorCodeTest.java | 3 +- .../{h2 => http2}/Http2ExceptionTest.java | 2 +- .../flash/{h2 => http2}/Http2LimitsTest.java | 2 +- .../Http2StreamExceptionTest.java | 2 +- .../frame/FrameValidatorTest.java | 8 +- .../frame/Http2FrameReaderFuzzTest.java | 5 +- .../frame/Http2FrameReaderTest.java | 10 +-- .../frame/Http2FrameWriterStressTest.java | 3 +- .../frame/Http2FrameWriterTest.java | 2 +- .../{h2 => http2}/frame/PaddingTest.java | 6 +- .../flash/models/Http1HeaderMapIndexTest.java | 3 - .../relism/flash/models/PathParamsTest.java | 1 - .../flash/models/QueryParamsFastPathTest.java | 3 - .../relism/flash/models/RequestBodyTest.java | 2 - .../flash/models/RequestPoolingTest.java | 2 - .../flash/models/RequestRecycleGuardTest.java | 1 - .../flash/models/ResponsePoolingTest.java | 1 - .../models/ResponseRecycleGuardTest.java | 1 - .../dev/relism/flash/models/ResponseTest.java | 1 - .../FastPathRouterImplTest.java | 1 - .../FastPathViewsLongAtTest.java | 2 - .../fastpathrouter/FastPathViewsTest.java | 1 - .../flash/template/ByteTemplateTest.java | 1 - .../dev/relism/flash/tls/TlsConfigTest.java | 1 - .../transport/BufferedByteSourceTest.java | 4 - .../flash/transport/ConnectionRunnerTest.java | 1 - .../transport/ProtocolNegotiatorTest.java | 7 +- .../ServerLifecycleGracefulShutdownTest.java | 2 - ...bSocketFragmentationAndValidationTest.java | 6 -- 107 files changed, 122 insertions(+), 484 deletions(-) rename flash/src/jmh/java/dev/relism/flash/{h2 => http2}/frame/FrameLayerBenchmark.java (99%) rename flash/src/jmh/java/dev/relism/flash/{h2 => http2}/frame/FrameWriterBenchmark.java (99%) delete mode 100644 flash/src/main/java/dev/relism/flash/h2/package-info.java rename flash/src/main/java/dev/relism/flash/{h2 => http2}/Http2ErrorCode.java (96%) rename flash/src/main/java/dev/relism/flash/{h2 => http2}/Http2Exception.java (92%) rename flash/src/main/java/dev/relism/flash/{h2 => http2}/Http2Limits.java (87%) rename flash/src/main/java/dev/relism/flash/{h2 => http2}/Http2StreamException.java (85%) rename flash/src/main/java/dev/relism/flash/{h2 => http2}/frame/FrameFlags.java (98%) rename flash/src/main/java/dev/relism/flash/{h2 => http2}/frame/FrameHeader.java (96%) rename flash/src/main/java/dev/relism/flash/{h2 => http2}/frame/FrameType.java (95%) rename flash/src/main/java/dev/relism/flash/{h2 => http2}/frame/FrameValidator.java (92%) rename flash/src/main/java/dev/relism/flash/{h2 => http2}/frame/FrameWriteBuffer.java (96%) rename flash/src/main/java/dev/relism/flash/{h2 => http2}/frame/Http2FrameReader.java (94%) rename flash/src/main/java/dev/relism/flash/{h2 => http2}/frame/Http2FrameWriter.java (94%) rename flash/src/main/java/dev/relism/flash/{h2 => http2}/frame/IntrusiveMpscQueue.java (99%) rename flash/src/main/java/dev/relism/flash/{h2 => http2}/frame/Padding.java (93%) rename flash/src/main/java/dev/relism/flash/{h2 => http2}/frame/WriteIntent.java (98%) rename flash/src/test/java/dev/relism/flash/{h2 => http2}/Http2ErrorCodeTest.java (95%) rename flash/src/test/java/dev/relism/flash/{h2 => http2}/Http2ExceptionTest.java (97%) rename flash/src/test/java/dev/relism/flash/{h2 => http2}/Http2LimitsTest.java (98%) rename flash/src/test/java/dev/relism/flash/{h2 => http2}/Http2StreamExceptionTest.java (96%) rename flash/src/test/java/dev/relism/flash/{h2 => http2}/frame/FrameValidatorTest.java (96%) rename flash/src/test/java/dev/relism/flash/{h2 => http2}/frame/Http2FrameReaderFuzzTest.java (93%) rename flash/src/test/java/dev/relism/flash/{h2 => http2}/frame/Http2FrameReaderTest.java (96%) rename flash/src/test/java/dev/relism/flash/{h2 => http2}/frame/Http2FrameWriterStressTest.java (98%) rename flash/src/test/java/dev/relism/flash/{h2 => http2}/frame/Http2FrameWriterTest.java (98%) rename flash/src/test/java/dev/relism/flash/{h2 => http2}/frame/PaddingTest.java (95%) diff --git a/flash/docs/http2/DECISIONS.md b/flash/docs/http2/DECISIONS.md index fd37880..1fca653 100644 --- a/flash/docs/http2/DECISIONS.md +++ b/flash/docs/http2/DECISIONS.md @@ -14,7 +14,7 @@ that supersedes the earlier one and says so explicitly. --- -## DEC-01 — HTTP/2 lives in `flash` core, package `dev.relism.flash.h2`, not an extension +## DEC-01 — HTTP/2 lives in `flash` core, package `dev.relism.flash.http2`, not an extension **Context.** Flash has an extension mechanism (`flash-ext-*` modules) for optional functionality. HTTP/2 could in principle be shipped as `flash-ext-h2`. @@ -53,7 +53,7 @@ existing HTTP/1.1 code paths. extracted upward into protocol-neutral components (`dev.relism.flash.bytes`, `ResponseSerializer`), never pushed sideways with a protocol flag. This is enforced by an architecture test (Phase 2) asserting `dev.relism.flash.http1` never references -`dev.relism.flash.h2` and vice versa. The cost is more up-front extraction work in Phase 2 and +`dev.relism.flash.http2` and vice versa. The cost is more up-front extraction work in Phase 2 and Phase 6; the benefit is that h1 throughput cannot regress from an `if` that the JIT fails to eliminate, and that either implementation can be read in isolation. @@ -318,7 +318,7 @@ identified. Not anticipated. **Consequence.** All HTTP/2 commits use `feat(core): ...` / `fix(core): ...` / `refactor(core): ...`, consistent with the branch name (`feature/core/http2`) and with `DEC-01` (HTTP/2 is core, not a separate concern). A reader can still find every h2-related commit via -the file paths touched (`dev.relism.flash.h2/**`, `flash/docs/http2/**`) or via the commit body, +the file paths touched (`dev.relism.flash.http2/**`, `flash/docs/http2/**`) or via the commit body, which is no worse than a scope label and avoids growing the scope list for what is, by `DEC-01`, not actually a separate module. @@ -484,7 +484,7 @@ instead of a speculative one. ## DEC-17 — `FrameWriterBenchmark` lives in `src/jmh/java`, a source root registered only inside the `jmh` profile, not in `src/test/java` **Context.** The Phase 3 JMH benchmark (`FrameWriterBenchmark`) was first placed directly in -`src/test/java/dev/relism/flash/h2/frame/`, on the theory recorded in `flash/pom.xml`'s comment +`src/test/java/dev/relism/flash/http2/frame/`, on the theory recorded in `flash/pom.xml`'s comment at the time: since the class carries only `@Benchmark`/JMH annotations and no JUnit annotations, Surefire's JUnit-Jupiter engine would simply not select it as a test, so a plain `mvn test` (no `-Pjmh`) would harmlessly ignore it. Verifying this assumption (`mvn -pl flash -am clean @@ -512,7 +512,7 @@ catch, just in the build graph rather than the source graph. profile's ``. With the profile inactive, the file is not handed to the compiler at all, under any goal — not `test-compile`, not IDE indexing driven by the effective POM. This is also what the plan itself already suggested (Phase 3's Files list: `flash/src/jmh/ - java/dev/relism/flash/h2/FrameWriterBenchmark.java (or a flash-bench submodule...)`) — the + java/dev/relism/flash/http2/FrameWriterBenchmark.java (or a flash-bench submodule...)`) — the prior session's placement in `src/test/java` was itself a deviation from the plan's own suggested layout, not a considered alternative. 3. A separate `flash-bench` submodule, depending on `flash` and always pulling in JMH. The diff --git a/flash/docs/http2/FRAMES.md b/flash/docs/http2/FRAMES.md index d84b488..e212fa9 100644 --- a/flash/docs/http2/FRAMES.md +++ b/flash/docs/http2/FRAMES.md @@ -1,6 +1,6 @@ # The Frame Layer (Phase 5) -Audience: contributors. This is the design record for `dev.relism.flash.h2.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 semantics, no streams, and no HPACK above it. @@ -32,7 +32,7 @@ out of `streamId()` once, so no caller has to remember to. ## Package layout ``` -dev.relism.flash.h2.frame +dev.relism.flash.http2.frame ├── FrameType the 10 known types + per-type validation descriptor (min/max length, stream-id rule) ├── FrameFlags END_STREAM/ACK/END_HEADERS/PADDED/PRIORITY bit constants + predicates ├── FrameHeader flyweight over a read buffer: length/type/flags/streamId/payloadOffset diff --git a/flash/docs/http2/IMPLEMENTATION-PLAN.md b/flash/docs/http2/IMPLEMENTATION-PLAN.md index 1cb2422..a0bf75d 100644 --- a/flash/docs/http2/IMPLEMENTATION-PLAN.md +++ b/flash/docs/http2/IMPLEMENTATION-PLAN.md @@ -1,10 +1,10 @@ # Flash — HTTP/2 Implementation Plan -> **Status**: design document, not yet implemented. +> **Status**: working implementation ledger; it is not product documentation or an API contract. > **Target branch**: `feature/core/http2` > **Target module**: `flash` (core). HTTP/2 is a transport concern and must live where > `HttpServer` lives; it cannot be an extension. -> **Target package root**: `dev.relism.flash.h2` +> **Target package root**: `dev.relism.flash.http2` > **Java baseline**: 21 (`maven.compiler.source/target=21` in the root `pom.xml`). Every > decision in this document assumes Java 21 semantics, in particular that > **`synchronized` pins the carrier thread of a virtual thread** (JEP 491, which removes @@ -61,7 +61,7 @@ Status values: `not started` / `in progress` / `blocked` / `done`. | Phase | Status | Branch/PR | Notes | |---|---|---|---| -| 0 — Groundwork | done | `feature/core/http2` | Package skeleton, `Http2Limits`, `Http1Limits`, `Http2ErrorCode`, `Http2Exception`/`Http2StreamException`, `DECISIONS.md` (`DEC-01`…`DEC-11`), `package-info.java`. 226/226 tests green. | +| 0 — Groundwork | done | `feature/core/http2` | Package skeleton, `Http2Limits`, `Http1Limits`, `Http2ErrorCode`, `Http2Exception`/`Http2StreamException`, and `DECISIONS.md`. 226/226 tests green. | | 1 — HTTP/1.1 hardening + ALPN/preface | done | `feature/core/http2` | EX-02/03/07/08/10/17/18/30/31 fixed; EX-35/36 found+fixed. `BufferedByteSource`, `ProtocolNegotiator`, `MalformedRequestException` added (plan corrected, DEC-12). 277/277 tests green (run twice). h1 benchmark check deferred — no JMH harness until Phase 3 (documented in DoD). | | 2 — Transport decomposition | done | `feature/core/http2` | `HttpServer.java` deleted; `transport`/`http1` packages + WS extraction (EX-01/06/11/12/13/14/15/16/32/34) done. Router `ThreadLocal` (EX-06 router half) deliberately deferred to Phase 4 per DEC-15. 311/311 tests green (run 3×). h1 benchmark check deferred — no JMH harness until Phase 3. | | 3 — Serialized frame writer (GO/NO-GO gate) | done | `feature/core/http2` | `Http2FrameWriter`/`WriteIntent`/`IntrusiveMpscQueue` + `Http2FrameWriterTest`/`Http2FrameWriterStressTest` + `FrameWriterBenchmark` (JMH, `-Pjmh`, `src/jmh/java` — moved there from `src/test/java` after it broke default `mvn test`; see `DEC-17`). All 4 gate criteria met: N=1 0 B/op & 42.6 ns overhead (≤50 ns budget); N=64 65.5% throughput retention (≥60%) & 11.8–14.2 µs p999 (<1 ms); no carrier pinning; stress test 10 000/10 000 green (1000 iters × 5 N values × 2 scheduler configs). Full numbers in `WRITER.md`, `DEC-09`. 321/321 non-JMH tests green. | @@ -782,11 +782,10 @@ prevents three different naming schemes for the same idea. ### Files created ``` -flash/src/main/java/dev/relism/flash/h2/package-info.java -flash/src/main/java/dev/relism/flash/h2/Http2Limits.java -flash/src/main/java/dev/relism/flash/h2/Http2ErrorCode.java -flash/src/main/java/dev/relism/flash/h2/Http2Exception.java -flash/src/main/java/dev/relism/flash/h2/Http2StreamException.java +flash/src/main/java/dev/relism/flash/http2/Http2Limits.java +flash/src/main/java/dev/relism/flash/http2/Http2ErrorCode.java +flash/src/main/java/dev/relism/flash/http2/Http2Exception.java +flash/src/main/java/dev/relism/flash/http2/Http2StreamException.java flash/src/main/java/dev/relism/flash/http/Http1Limits.java flash/docs/http2/IMPLEMENTATION-PLAN.md (this file) flash/docs/http2/DECISIONS.md (decision log, see below) @@ -795,8 +794,7 @@ flash/docs/http2/DECISIONS.md (decision log, see below) ### Package layout (final; later phases fill it in) ``` -dev.relism.flash.h2 -├── package-info.java module-level Javadoc: the whole architecture in one page +dev.relism.flash.http2 ├── Http2Limits.java every bound, every default, each with its attack rationale ├── Http2ErrorCode.java the 14 RFC 9113 §7 codes, with pre-encoded 4-byte forms ├── Http2Exception.java connection error → GOAWAY @@ -867,11 +865,7 @@ dev.relism.flash.bytes (new, Phase 4 — protocol-neutral byte ut (`DEC-01` … `DEC-08`, listed in Part VI). Every subsequent non-obvious choice appends an entry: context, options, decision, consequence. This is how the next agent understands why the encoder has no dynamic table. -2. Write `dev/relism/flash/h2/package-info.java` containing the one-page architecture - description: the demux loop, the virtual-thread-per-stream model, the writer discipline, the - arena strategy, and the explicit list of what Flash does not implement (server push, - priority scheduling) with the RFC citation permitting it. -3. Write `Http2ErrorCode` as an enum of the 14 RFC 9113 §7 codes with `code()` and a +2. Write `Http2ErrorCode` as an enum of the 14 RFC 9113 §7 codes with `code()` and a **pre-encoded 4-byte big-endian `byte[]`** per constant (used in RST_STREAM and GOAWAY payloads without formatting). 4. Write `Http2Limits` with every bound this plan will need. Each field gets a Javadoc naming @@ -906,7 +900,7 @@ positive and internally consistent, e.g. `MAX_FRAME_SIZE_LOCAL` within RFC bound 16384..16777215). ### Docs -`flash/docs/http2/DECISIONS.md` created. `package-info.java` written. +`flash/docs/http2/DECISIONS.md` created. ### DoD - [x] Package skeleton compiles (empty classes are acceptable only for classes whose phase has @@ -1241,7 +1235,7 @@ nothing but stops syscalling per byte. No new steady-state allocation is introdu immediately. - [x] `PackageBoundaryTest` — a source-scan architecture test (decision recorded in the test's own Javadoc: no ArchUnit dependency yet, and one import check per package pair does not - need one): `dev.relism.flash.http1` must not import `dev.relism.flash.h2` and vice versa. + need one): `dev.relism.flash.http1` must not import `dev.relism.flash.http2` and vice versa. ### Docs - `README.md` architecture section (lines 257-274) rewritten to reflect the new component @@ -1337,16 +1331,16 @@ race documented in the Javadoc, and verified by a dedicated stress test. ### Files Created: -- `flash/src/main/java/dev/relism/flash/h2/frame/Http2FrameWriter.java` -- `flash/src/main/java/dev/relism/flash/h2/frame/WriteIntent.java` — the interface a stream +- `flash/src/main/java/dev/relism/flash/http2/frame/Http2FrameWriter.java` +- `flash/src/main/java/dev/relism/flash/http2/frame/WriteIntent.java` — the interface a stream implements to describe "serialize yourself into this buffer". Implemented by `Http2Stream` and by connection-level singletons (SETTINGS ACK, PING ACK, GOAWAY, WINDOW_UPDATE) so that connection frames use the same path as stream frames — one writer, no exceptions. -- `flash/src/main/java/dev/relism/flash/h2/frame/IntrusiveMpscQueue.java` — the Vyukov queue, +- `flash/src/main/java/dev/relism/flash/http2/frame/IntrusiveMpscQueue.java` — the Vyukov queue, operating on a `Node` interface that `Http2Stream` implements. -- `flash/src/test/java/dev/relism/flash/h2/frame/Http2FrameWriterTest.java` -- `flash/src/test/java/dev/relism/flash/h2/frame/Http2FrameWriterStressTest.java` -- `flash/src/jmh/java/dev/relism/flash/h2/FrameWriterBenchmark.java` (or a `flash-bench` +- `flash/src/test/java/dev/relism/flash/http2/frame/Http2FrameWriterTest.java` +- `flash/src/test/java/dev/relism/flash/http2/frame/Http2FrameWriterStressTest.java` +- `flash/src/jmh/java/dev/relism/flash/http2/FrameWriterBenchmark.java` (or a `flash-bench` submodule — decide and record in `DECISIONS.md`; a `jmh` profile on the `flash` module is simplest and avoids a new artifact). @@ -2801,7 +2795,7 @@ speak h2 as a **client** so Pathway can proxy. the same frame reader/writer, the same HPACK codec (the encoder now needs `:method`, `:scheme`, `:authority`, `:path` — all static-table entries), the same stream machine with the roles inverted. New: connection pooling, `:status` handling, and response assembly. - Keep it in `dev.relism.flash.h2.client` and keep it honest about scope: it exists to serve + Keep it in `dev.relism.flash.http2.client` and keep it honest about scope: it exists to serve the proxy use case, not to be a general-purpose HTTP client. 4. **Trailer relay.** A proxy must forward trailers in both directions, and must forward them *as trailers*, not fold them into headers. Getting this wrong is the single most common @@ -2918,7 +2912,7 @@ defines the h2 mechanism. - A soak test: 10 minutes of sustained mixed traffic (GET, POST, streaming, RST, PING) with heap and pool-size assertions at the end. Tagged for nightly, not per-PR. 6. **Regression corpus.** Every bug found during implementation gets a test with the exact - frame bytes that triggered it, checked in under `src/test/resources/h2/regressions/`. + frame bytes that triggered it, checked in under `src/test/resources/http2/regressions/`. ### Docs `flash/docs/http2/COMPLIANCE.md` — the `h2spec` result table, the interop matrix with versions, the @@ -3044,8 +3038,7 @@ be traceable to a number in this file. orientation for someone opening the package for the first time. **Javadoc:** -- Every public type in `dev.relism.flash.h2` and the new `transport`/`http1`/`bytes` packages. -- `package-info.java` for each new package. +- Every public type in `dev.relism.flash.http2` and the new `transport`/`http1`/`bytes` packages. - The release workflow publishes Javadoc to GitHub Pages (`release.yml`); verify the new packages render correctly and that no `@link` is broken. @@ -3080,7 +3073,7 @@ be traceable to a number in this file. | Concurrency | 1000 streams, stress, leak, pinning | `*ConcurrencyTest`, `*LeakTest` | | Allocation | 0 B/op gates | JMH `-prof gc` in CI | | Performance | Throughput and latency baselines | JMH + `h2load` | -| Regression | Every bug ever found, by its exact bytes | `src/test/resources/h2/regressions/` | +| Regression | Every bug ever found, by its exact bytes | `src/test/resources/http2/regressions/` | ## Rules @@ -3141,7 +3134,7 @@ an entry in the same format: **Context / Options / Decision / Consequence / Revi | Id | Decision | One-line rationale | |---|---|---| -| `DEC-01` | HTTP/2 lives in `flash` core, package `dev.relism.flash.h2`, not an extension | The protocol branch must sit where the transport sits; `HttpServer` is package-private | +| `DEC-01` | HTTP/2 lives in `flash` core, package `dev.relism.flash.http2`, not an extension | The protocol branch must sit where the transport sits; `HttpServer` is package-private | | `DEC-02` | h1 and h2 are peers behind a `ConnectionProtocol` seam, never flags in shared code | `R1`; protects h1 performance and both implementations' readability | | `DEC-03` | `ReentrantLock` everywhere, never `synchronized` around blocking I/O | Java 21 pins carriers on `synchronized`; JEP 491 is JDK 24+ | | `DEC-04` | The HPACK **encoder** uses the static table only; no dynamic table | Removes all shared mutable state from the write path, at a cost of a few bytes on the wire | @@ -3267,4 +3260,3 @@ pressure and it is the one the project owner asked for most explicitly: > description. Do not open a TODO, do not defer it, and do not work around it. > > The registry in Part II came from reading the codebase once. It is a floor, not a ceiling. - diff --git a/flash/docs/http2/TRANSPORT.md b/flash/docs/http2/TRANSPORT.md index 6be8abe..fef5cdc 100644 --- a/flash/docs/http2/TRANSPORT.md +++ b/flash/docs/http2/TRANSPORT.md @@ -114,7 +114,7 @@ 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.h2` do not import each other, +the other exists — `dev.relism.flash.http1` and `dev.relism.flash.http2` do not import each other, enforced by `PackageBoundaryTest`. ## Graceful shutdown (`EX-32`) diff --git a/flash/docs/http2/WRITER.md b/flash/docs/http2/WRITER.md index a0afa7c..5af0729 100644 --- a/flash/docs/http2/WRITER.md +++ b/flash/docs/http2/WRITER.md @@ -1,7 +1,7 @@ # The Serialized Frame Writer (Phase 3 — GO/NO-GO gate) Audience: contributors. This is the design record and benchmark evidence for -`dev.relism.flash.h2.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 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 @@ -139,7 +139,7 @@ from the path this document's gate criteria are strictest about. ## Benchmark methodology -`flash/src/jmh/java/dev/relism/flash/h2/frame/FrameWriterBenchmark.java` (a JMH source root +`flash/src/jmh/java/dev/relism/flash/http2/frame/FrameWriterBenchmark.java` (a JMH source root registered only under the `jmh` Maven profile — see `DECISIONS.md`, `DEC-17`, for why it does not live in `src/test/java`) compares four harnesses at `threads` ∈ {1, 2, 4, 8, 16, 64}: diff --git a/flash/src/jmh/java/dev/relism/flash/h2/frame/FrameLayerBenchmark.java b/flash/src/jmh/java/dev/relism/flash/http2/frame/FrameLayerBenchmark.java similarity index 99% rename from flash/src/jmh/java/dev/relism/flash/h2/frame/FrameLayerBenchmark.java rename to flash/src/jmh/java/dev/relism/flash/http2/frame/FrameLayerBenchmark.java index f49e16e..7fe0cae 100644 --- a/flash/src/jmh/java/dev/relism/flash/h2/frame/FrameLayerBenchmark.java +++ b/flash/src/jmh/java/dev/relism/flash/http2/frame/FrameLayerBenchmark.java @@ -1,4 +1,4 @@ -package dev.relism.flash.h2.frame; +package dev.relism.flash.http2.frame; import dev.relism.flash.bytes.ByteWriter; import dev.relism.flash.transport.BufferedByteSource; diff --git a/flash/src/jmh/java/dev/relism/flash/h2/frame/FrameWriterBenchmark.java b/flash/src/jmh/java/dev/relism/flash/http2/frame/FrameWriterBenchmark.java similarity index 99% rename from flash/src/jmh/java/dev/relism/flash/h2/frame/FrameWriterBenchmark.java rename to flash/src/jmh/java/dev/relism/flash/http2/frame/FrameWriterBenchmark.java index a79292c..4d93c37 100644 --- a/flash/src/jmh/java/dev/relism/flash/h2/frame/FrameWriterBenchmark.java +++ b/flash/src/jmh/java/dev/relism/flash/http2/frame/FrameWriterBenchmark.java @@ -1,4 +1,4 @@ -package dev.relism.flash.h2.frame; +package dev.relism.flash.http2.frame; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; diff --git a/flash/src/main/java/dev/relism/flash/ChunkedInputStream.java b/flash/src/main/java/dev/relism/flash/ChunkedInputStream.java index bb89eeb..d3d6ec9 100644 --- a/flash/src/main/java/dev/relism/flash/ChunkedInputStream.java +++ b/flash/src/main/java/dev/relism/flash/ChunkedInputStream.java @@ -12,7 +12,6 @@ import java.io.InputStream; * Handles pre-buffered bytes from the header read-ahead, chunk framing, and trailer consumption. * Returns -1 at end of the final chunk; the underlying socket is left positioned for the next request. * - *

{@code EX-10}: reads through the connection's shared {@link BufferedByteSource} * instead of the raw, unbuffered socket stream. Chunk-size digits, the trailing CRLF after each * chunk, and trailer lines are all read one byte at a time by design (the framing is * byte-oriented) — that used to mean one {@code read(2)} syscall per byte on the raw socket; @@ -124,7 +123,6 @@ final class ChunkedInputStream extends InputStream { * (RFC 9112 §7.1.2). Bounded by {@link Http1Limits#MAX_TRAILER_COUNT} and * {@link Http1Limits#MAX_HEADER_VALUE_LENGTH} — without a bound, a peer could follow the * final chunk with an unbounded trailer section purely to waste CPU discarding it. Trailers - * are discarded, not exposed to the handler; exposing them is Phase 12 scope * ({@code Request.trailers()}). */ private void consumeTrailers() throws IOException { diff --git a/flash/src/main/java/dev/relism/flash/RequestParser.java b/flash/src/main/java/dev/relism/flash/RequestParser.java index ae91915..e50f8c9 100644 --- a/flash/src/main/java/dev/relism/flash/RequestParser.java +++ b/flash/src/main/java/dev/relism/flash/RequestParser.java @@ -40,7 +40,6 @@ import java.util.Arrays; * and reset to {@code 0/0} before any work begins, so an exception thrown mid-parse * leaves the fields clean rather than pointing at stale data from a previous request. * - *

Rejection model (RFC 9112 §6.1, {@code EX-02}/{@code EX-03}/{@code EX-08}/{@code EX-18})

* Anything wrong with the request itself — smuggling-relevant ambiguity, an over-limit * header, a malformed byte where the grammar forbids one — is reported as a * {@link MalformedRequestException} carrying the exact status the caller must respond with. @@ -58,13 +57,10 @@ public class RequestParser { private final InetSocketAddress remoteAddress; private final SSLSocket sslSocket; private final Http1HeaderMap headerMap = new Http1HeaderMap(); - // EX-22: one Request/RequestLine per connection, repositioned (never reallocated) per // request — same idiom as headerMap above. private final RequestLine requestLine = new RequestLine(); private final Request request = new Request(); private final RequestBody requestBody = new RequestBody(); - // EX-42: one pooled RequestByteView per role, repositioned (never reallocated) per request — - // closes the last per-request allocation left after EX-20..EX-24 pooled Request/RequestBody/ // RequestLine/Response themselves. queryView is only reset and used when a query string is // actually present; RequestLine.getQuery() must keep returning null otherwise (see reset()). private final FastPathViews.RequestByteView pathView = new FastPathViews.RequestByteView(null, 0, 0); @@ -174,7 +170,6 @@ public class RequestParser { int protocolEnd = ByteScan.indexOf(buffer, protocolStart, headerEndIdx, (byte) '\r'); if (protocolEnd == -1) throw new MalformedRequestException(400, "Invalid request line (protocol)"); - // EX-08: the request line itself (method SP target SP version) is bounded separately // from the overall header-block size, so an oversized request line gets its own, // specific rejection rather than being folded into the generic "headers too large" case. if (protocolEnd - base > Http1Limits.MAX_REQUEST_LINE_LENGTH) { @@ -194,7 +189,6 @@ public class RequestParser { int headerCount = 0; while (current < headerEndIdx) { - // EX-18 (obs-fold): a header line MUST NOT begin with whitespace. RFC 9112 §5.2 // deprecates line folding and treating a folded continuation as part of the // previous header's value is a known request-smuggling vector. byte first = buffer[current]; @@ -205,7 +199,6 @@ public class RequestParser { int lineEnd = ByteScan.indexOf(buffer, current, headerEndIdx + 1, (byte) '\r'); if (lineEnd == -1 || lineEnd == current) break; - // EX-18: verify the '\r' is immediately followed by '\n' instead of blindly // advancing past two bytes — a bare '\r' not followed by '\n' desynchronizes the // parse and is a known bare-CR smuggling surface. Safe to read lineEnd+1: lineEnd // is at most headerEndIdx, and findEndOfHeader already guaranteed 4 readable bytes @@ -238,11 +231,9 @@ public class RequestParser { } if (ByteScan.equalsIgnoreCaseAscii(buffer, current, colon, "content-length")) { - // EX-03: strict, overflow-safe parsing — replaces the old digit-skipping // parseLong, which silently accepted "5abc" as 5 and "-1" as 1. long parsed = parseContentLengthStrict(buffer, valueStart, lineEnd); // Multiple Content-Length lines with differing values is itself a smuggling - // primitive (EX-02); identical repeated values are tolerated (RFC 9110 §8.6 // permits a recipient to treat that as one value). if (contentLengthSeen && parsed != contentLength) { throw new MalformedRequestException(400, "Conflicting Content-Length values"); @@ -251,8 +242,6 @@ public class RequestParser { contentLengthSeen = true; } else if (ByteScan.equalsIgnoreCaseAscii(buffer, current, colon, "transfer-encoding")) { transferEncodingSeen = true; - // Correctness fix found while implementing EX-02 in this exact code path - // (registered as EX-35): the old check required the WHOLE value to equal // "chunked", so "gzip, chunked" — valid per RFC 9112 §6.1, where chunked need // only be the FINAL coding — was silently treated as not chunked at all, // corrupting the message boundary. Fixed by inspecting only the last token. @@ -261,7 +250,6 @@ public class RequestParser { current = lineEnd + 2; } - // EX-02 (RFC 9112 §6.1): a request with both Content-Length and Transfer-Encoding // MUST be treated as an error by an origin server — this is the canonical CL.TE/TE.CL // smuggling vector. Checked once both headers are known, regardless of the order they // appeared in, so ordering games cannot bypass it. @@ -301,7 +289,6 @@ public class RequestParser { requestLine.reset(method, pathView, queryMark != -1 ? queryView : null, protocolView, headerMap); - // EX-22: requestBody is this connection's single pooled instance (see its own class // Javadoc) -- reset() repositions it for the fixed-length/empty case (contentLength == 0 // is handled by the same call: preBufLen is already forced to 0 for it above) or the // chunked case, never reallocated. @@ -314,7 +301,6 @@ public class RequestParser { } /** - * Strict, overflow-safe {@code Content-Length} parsing ({@code EX-03}). Rejects: an empty * value, any non-digit byte (including a leading {@code +}/{@code -}, which are not * digits), more than 19 digits (the longest possible {@code Long.MAX_VALUE}), arithmetic * overflow past {@code Long.MAX_VALUE}, and a value above @@ -349,7 +335,6 @@ public class RequestParser { * ({@code "gzip, chunked"}), {@code chunked} MUST be the final one for the message to be * self-delimiting. Returns whether the last comma-separated token in {@code [start, end)} * is exactly {@code "chunked"} (case-insensitive), ignoring surrounding whitespace around - * that token. Registered as {@code EX-35}: the previous whole-value comparison silently * misclassified any multi-coding value as non-chunked. */ private static boolean isFinalCodingChunked(byte[] buf, int start, int end) { diff --git a/flash/src/main/java/dev/relism/flash/ServerHandle.java b/flash/src/main/java/dev/relism/flash/ServerHandle.java index a4926c5..e03e8ce 100644 --- a/flash/src/main/java/dev/relism/flash/ServerHandle.java +++ b/flash/src/main/java/dev/relism/flash/ServerHandle.java @@ -13,7 +13,6 @@ import java.util.concurrent.CompletableFuture; * Public handle to the underlying HTTP transport. Returned by {@link #create} * so that {@link FlashApp} can start and stop the server * without holding a direct reference to the transport's internal composition - * ({@link TransportFactory}, {@code EX-34}). */ public interface ServerHandle { diff --git a/flash/src/main/java/dev/relism/flash/api/multipart/Multipart.java b/flash/src/main/java/dev/relism/flash/api/multipart/Multipart.java index 4c751aa..81703f9 100644 --- a/flash/src/main/java/dev/relism/flash/api/multipart/Multipart.java +++ b/flash/src/main/java/dev/relism/flash/api/multipart/Multipart.java @@ -57,7 +57,6 @@ public final class Multipart { private final List scanned = new ArrayList<>(); private PartBodyStream active = null; // open file stream; must be drained before next scan - private int partCount = 0; // EX-29: bounds Http1Limits.MAX_MULTIPART_PARTS // ------------------------------------------------------------------------- // Factory @@ -159,7 +158,6 @@ public final class Multipart { Map headers = readPartHeaders(); if (headers == null) { done = true; return null; } - // EX-29: without this bound, a peer sending an unbounded number of minimal parts forces // unbounded growth of `scanned` and unbounded cumulative header-parsing work. if (++partCount > Http1Limits.MAX_MULTIPART_PARTS) { throw new IOException("multipart body exceeds max part count (" + Http1Limits.MAX_MULTIPART_PARTS + ")"); @@ -177,7 +175,6 @@ public final class Multipart { // File part — expose streaming body; not cached (stream is consumed once) p = Part.streaming(name, filename, ct, active); } else { - // Text part, or full-scan path: buffer body now. EX-29: bounded, not // InputStream.readAllBytes() — an unbounded field/file body would otherwise let a // hostile peer force an arbitrarily large single heap allocation. byte[] body = readBoundedBody(active); @@ -302,7 +299,6 @@ public final class Multipart { while (true) { String line = readLine(); if (line == null || line.isEmpty()) break; - // EX-29: without this bound a peer can send an effectively unlimited number of // header lines before the blank line that ends a part's header block. if (++count > Http1Limits.MAX_MULTIPART_PART_HEADER_COUNT) { throw new IOException("multipart part exceeds max header count (" @@ -338,7 +334,6 @@ public final class Multipart { sb.append(new String(win, wPos, append, StandardCharsets.UTF_8)); wPos += append; wLen -= append; } - // EX-29: without this bound, a peer that never sends \r\n keeps this StringBuilder // growing for as long as it keeps streaming bytes — the multipart-header analogue of // RequestParser's Http1Limits.MAX_HEADER_VALUE_LENGTH check, which does not apply // here since these header lines live inside the body, not the top-level HTTP headers. diff --git a/flash/src/main/java/dev/relism/flash/bytes/ArrayBackedByteView.java b/flash/src/main/java/dev/relism/flash/bytes/ArrayBackedByteView.java index 8aeec16..018c9ee 100644 --- a/flash/src/main/java/dev/relism/flash/bytes/ArrayBackedByteView.java +++ b/flash/src/main/java/dev/relism/flash/bytes/ArrayBackedByteView.java @@ -20,7 +20,6 @@ import dev.relism.fpr.core.ByteView; *
  • Single-allocation {@code String} construction — * {@code new String(view.array(), view.offset(), view.length(), UTF_8)} instead of a * byte-at-a-time copy into a scratch {@code byte[]} followed by a second allocation for - * the {@code String} itself ({@code EX-25}).
  • *
  • A single {@code System.arraycopy} instead of a manual loop wherever a view's bytes need * to be copied.
  • * diff --git a/flash/src/main/java/dev/relism/flash/bytes/ByteScan.java b/flash/src/main/java/dev/relism/flash/bytes/ByteScan.java index b3b2a49..2cf56aa 100644 --- a/flash/src/main/java/dev/relism/flash/bytes/ByteScan.java +++ b/flash/src/main/java/dev/relism/flash/bytes/ByteScan.java @@ -11,15 +11,12 @@ import java.nio.ByteOrder; * {@code \r\n\r\n} header-terminator search (SWAR-accelerated), case-insensitive comparison, * comma-separated token-list scanning ({@code Connection: a, b, c}), RFC 9110 {@code tchar} * validation, and the case-insensitive header-name hash {@link dev.relism.flash.models.Http1HeaderMap}'s - * index uses ({@code EX-09}). * *

    Every method here is {@code static} and allocates nothing. Every SWAR method has a plain * scalar counterpart ({@code *Scalar}) that exists for two reasons: it is what the tests use as * the correctness oracle (property-tested against the SWAR version on randomized inputs — see * {@code ByteScanTest}/{@code ByteScanFuzzTest}), and it is the documented fallback if a future * measurement ever shows the SWAR path is not worth its complexity on some path (none has been - * found not worth it so far — see {@code DECISIONS.md} for the one path that {@em was} - * measured and kept, {@code EX-33}). * *

    The SWAR technique used throughout

    * Both {@link #indexOf} and {@link #indexOfCrLfCrLf} use the classic "does this word contain @@ -60,7 +57,6 @@ public final class ByteScan { /** * RFC 9110 §5.6.2 {@code tchar} set, table-driven so validation is a single array read per - * byte (R4/R5) rather than a chain of range comparisons. Indexed directly by byte value; * only the ASCII range a valid header-name character can ever occupy is populated. */ private static final boolean[] TCHAR = new boolean[128]; @@ -119,7 +115,6 @@ public final class ByteScan { * Index of the first {@code "\r\n\r\n"} in {@code buf[from, to)}, or {@code -1}. SWAR * pre-filter (find a candidate {@code CR} byte 8 at a time) plus a cheap scalar 3-byte * verify at each candidate — see the class Javadoc for the technique and - * {@code RequestParser}, {@code EX-33}, for why this replaced a fully byte-at-a-time scan. */ public static int indexOfCrLfCrLf(byte[] buf, int from, int to) { int limit = to - 4; // last index at which a 4-byte match can start @@ -216,7 +211,6 @@ public final class ByteScan { /** * Whether the comma-separated, OWS-tolerant token list {@code view} contains {@code token} * (case-insensitive). The shared scanner behind both {@code Http1KeepAlive.isKeepAlive} and - * the {@code Connection: Upgrade} check ({@code EX-13}) — a single home so the two can never * drift apart the way a whole-value {@code equals} check once did. */ public static boolean tokenListContains(ByteView view, String token) { @@ -238,7 +232,6 @@ public final class ByteScan { return equalsIgnoreCase(view, start, start + wlen, token); } - // ── Header-name hash (EX-09) ───────────────────────────────────────────── /** * Case-insensitive (ASCII fold) 32-bit FNV-1a hash of {@code buf[start, start + len)}. Used diff --git a/flash/src/main/java/dev/relism/flash/bytes/ByteWriter.java b/flash/src/main/java/dev/relism/flash/bytes/ByteWriter.java index cf2fd99..3a646ba 100644 --- a/flash/src/main/java/dev/relism/flash/bytes/ByteWriter.java +++ b/flash/src/main/java/dev/relism/flash/bytes/ByteWriter.java @@ -8,12 +8,8 @@ import java.nio.charset.StandardCharsets; * on an already-warm buffer (the steady-state case: the buffer has already grown to the * connection's high-water mark), no method here allocates. * - *

    This is the infrastructure {@code EX-27} (Phase 6, collapsing {@code Http1ResponseWriter}'s - * ~10 small writes into one) and the Phase 5 frame layer serialize into: build a complete - * message into a {@code ByteWriter}-backed scratch buffer, then issue one bulk - * {@code write(buffer, 0, length())} — the same "serialize outside the lock, one bulk write" - * discipline {@link dev.relism.flash.h2.frame.Http2FrameWriter} already established for the h2 - * writer (see its Javadoc's "Layer 1"), extended to the byte layer both protocols share. + * Callers build a complete message in a {@code ByteWriter}-backed scratch buffer and then issue + * one bulk {@code write(buffer, 0, length())}. The same writer is shared by HTTP/1.1 and HTTP/2. * *

    Lifetime and thread-safety contract

    * Not thread-safe — exactly one writer at a time, matching every other per-connection scratch @@ -24,6 +20,7 @@ import java.nio.charset.StandardCharsets; */ public final class ByteWriter { private byte[] buf; + private final byte[] digits = new byte[20]; private int len; public ByteWriter(int initialCapacity) { @@ -80,9 +77,8 @@ public final class ByteWriter { writeByte((byte) '0'); return; } - // Digits emerge least-significant-first; stage them in a small fixed buffer (at most 20 - // digits for any long) and copy in reverse — avoids a second pass to compute digit count. - byte[] digits = new byte[20]; + // Digits emerge least-significant-first. The reusable field holds every possible long + // representation, so decimal rendering does not allocate on a warm writer. int n = 0; long v = value; while (v > 0) { @@ -101,7 +97,6 @@ public final class ByteWriter { writeByte((byte) '0'); return; } - byte[] digits = new byte[8]; int n = 0; int v = value; while (v != 0) { @@ -125,10 +120,8 @@ public final class ByteWriter { /** * Writes {@code s}'s ASCII bytes, case preserved. {@code s} must be ASCII-only. Unlike - * {@code new String(...).getBytes(UTF_8)}, writes each character directly into this - * writer's buffer — no intermediate {@code byte[]} ({@code EX-20}: this is what lets - * {@code Response.header(String, String)} avoid the {@code StringBuilder}+concat+ - * {@code getBytes} allocation chain it used to pay per call). + * {@code new String(...).getBytes(UTF_8)}, writes each character directly into this buffer + * and avoids an intermediate {@code byte[]}. */ public void writeAscii(String s) { int n = s.length(); diff --git a/flash/src/main/java/dev/relism/flash/bytes/PooledSlice.java b/flash/src/main/java/dev/relism/flash/bytes/PooledSlice.java index cff5635..ff771e9 100644 --- a/flash/src/main/java/dev/relism/flash/bytes/PooledSlice.java +++ b/flash/src/main/java/dev/relism/flash/bytes/PooledSlice.java @@ -1,7 +1,6 @@ package dev.relism.flash.bytes; /** - * A mutable, reusable {@link ArrayBackedByteView} — the {@code EX-05} fix. Replaces the * per-call {@code new ByteView() { ... }} anonymous-class allocation that used to live in * {@code Http1HeaderMap.view}, {@code QueryParams.view}, and {@code PathParams.view}: instead of * allocating a fresh view object (plus its capturing instance) on every call, a small diff --git a/flash/src/main/java/dev/relism/flash/bytes/SegmentedByteView.java b/flash/src/main/java/dev/relism/flash/bytes/SegmentedByteView.java index 459bcee..a3389ed 100644 --- a/flash/src/main/java/dev/relism/flash/bytes/SegmentedByteView.java +++ b/flash/src/main/java/dev/relism/flash/bytes/SegmentedByteView.java @@ -12,7 +12,6 @@ import dev.relism.fpr.core.ByteView; *

    Deliberately not array-backed

    * This does not implement {@link ArrayBackedByteView} — there is no single {@code (array, * offset)} pair that describes it — and {@link #supportsLong()} returns {@code false} - * unconditionally rather than attempting a cross-segment 8-byte read ({@code EX-04}'s word-at-a- * time path is only sound for a genuinely contiguous backing array; see * {@code FastPathViews.MethodPathByteView} for the other deliberately-segmented view in this * codebase, which makes the same choice for the same reason). diff --git a/flash/src/main/java/dev/relism/flash/bytes/SlicePool.java b/flash/src/main/java/dev/relism/flash/bytes/SlicePool.java index 12e9c68..20b07d4 100644 --- a/flash/src/main/java/dev/relism/flash/bytes/SlicePool.java +++ b/flash/src/main/java/dev/relism/flash/bytes/SlicePool.java @@ -2,7 +2,6 @@ package dev.relism.flash.bytes; /** * A small, fixed-size ring of {@link PooledSlice} instances — one per {@code ConnectionScratch}- - * held call site that used to allocate a fresh {@code ByteView} per call ({@code EX-05}: * {@code Http1HeaderMap.view}, {@code QueryParams.view}, {@code PathParams.view}). * *

    Why a ring, not a single reused slice

    diff --git a/flash/src/main/java/dev/relism/flash/exceptions/MalformedRequestException.java b/flash/src/main/java/dev/relism/flash/exceptions/MalformedRequestException.java index c15b67d..18197e1 100644 --- a/flash/src/main/java/dev/relism/flash/exceptions/MalformedRequestException.java +++ b/flash/src/main/java/dev/relism/flash/exceptions/MalformedRequestException.java @@ -3,8 +3,6 @@ package dev.relism.flash.exceptions; /** * Thrown by the HTTP/1.1 parser when a request violates a protocol rule that must be rejected * outright — most importantly the request-smuggling defenses of RFC 9112 §6.1 (see - * {@code EX-02}/{@code EX-03} in {@code flash/docs/http2/IMPLEMENTATION-PLAN.md}) and the hard - * safety limits in {@code Http1Limits} (see {@code EX-08}). * *

    Distinct from {@link HttpException}, which a handler throws to describe an * application-level failure and which is routed through the user's configured exception diff --git a/flash/src/main/java/dev/relism/flash/extension/FlashConfiguration.java b/flash/src/main/java/dev/relism/flash/extension/FlashConfiguration.java index 31d14cd..12dd8d3 100644 --- a/flash/src/main/java/dev/relism/flash/extension/FlashConfiguration.java +++ b/flash/src/main/java/dev/relism/flash/extension/FlashConfiguration.java @@ -64,7 +64,6 @@ public class FlashConfiguration { * (see {@code dev.relism.flash.transport.BufferedByteSource}), not merely a per-read socket * timeout — a per-read timeout alone never trips as long as each individual read succeeds * within the window, no matter how long the overall header block takes. Default: 10 000 - * ({@code EX-07}). */ @Builder.Default int headerReadTimeoutMs = 10_000; @@ -74,7 +73,6 @@ public class FlashConfiguration { * request before being closed. More generous than {@link #headerReadTimeoutMs} because an * idle keep-alive connection is normal, expected behaviour, not an attack in progress — the * tighter bound applies only once bytes have actually started arriving. Default: 60 000 - * ({@code EX-07}). */ @Builder.Default int idleKeepAliveTimeoutMs = 60_000; @@ -82,7 +80,6 @@ public class FlashConfiguration { /** * Maximum time, in milliseconds, a request's body may take to be fully read (by the handler * or by the automatic drain after it returns) once headers are parsed. Default: 30 000 - * ({@code EX-07}). */ @Builder.Default int bodyReadTimeoutMs = 30_000; @@ -90,15 +87,12 @@ public class FlashConfiguration { /** * Maximum time, in milliseconds, {@link dev.relism.flash.ServerHandle#stop()} waits for * in-flight requests to finish after it stops accepting new connections, before force- - * closing whatever remains. Default: 15 000 ({@code EX-32} — the graceful two-stage - * shutdown this bounds is wired up starting Phase 2). */ @Builder.Default int shutdownDrainTimeoutMs = 15_000; /** * Whether this server will ever negotiate HTTP/2. Default {@code false}: until the h2 - * connection state machine exists (Phase 8) there is nothing to negotiate into, so this * flag currently only gates the h2c cleartext-preface detection * ({@code dev.relism.flash.transport.ProtocolNegotiator}) — skipping it entirely keeps * plaintext connections byte-for-byte identical to pre-HTTP/2 Flash when left at its diff --git a/flash/src/main/java/dev/relism/flash/h2/package-info.java b/flash/src/main/java/dev/relism/flash/h2/package-info.java deleted file mode 100644 index bfd7397..0000000 --- a/flash/src/main/java/dev/relism/flash/h2/package-info.java +++ /dev/null @@ -1,73 +0,0 @@ -/** - * HTTP/2 (RFC 9113) and HPACK (RFC 7541), as a peer transport to HTTP/1.1 — not a special case - * bolted onto it. This package lives in {@code flash} core, not an extension, because the - * protocol decision is made at the transport layer, where {@code HttpServer}'s replacement - * lives (see {@code DEC-01} in {@code flash/docs/http2/DECISIONS.md}). - * - *

    Architecture in one page

    - * - *

    The demux loop

    - * One virtual thread per connection reads and dispatches frames - * ({@code Http2Connection}, Phase 8): read a 9-byte frame header, validate it against the - * per-type table ({@code FrameValidator}, Phase 5), dispatch by type. This loop never blocks - * on application work — a slow handler must never stall frame processing for other streams - * on the same connection, which is the entire point of multiplexing. The only things the demux - * thread itself does synchronously are protocol bookkeeping: SETTINGS/PING/WINDOW_UPDATE - * accounting, HPACK decode, and stream-table updates. - * - *

    Virtual-thread-per-stream dispatch

    - * Once a request's headers (and, for small bodies, its body) are fully assembled, the demux - * thread submits a task to the shared virtual-thread executor and returns immediately to - * reading frames. Routing, middleware, and the user's handler run on that stream's own virtual - * thread — identical to the HTTP/1.1 dispatch model, so a handler written for h1 works - * unmodified over h2 (verified in Phase 10). - * - *

    The writer discipline

    - * N stream threads share one socket. {@code Http2FrameWriter} (Phase 3) is the single - * serialization point: a stream serializes its complete frame (header + HPACK block + payload) - * into a reusable per-stream scratch buffer, then takes a connection-wide {@link - * java.util.concurrent.locks.ReentrantLock} — never {@code synchronized}, which pins a virtual - * thread's carrier on Java 21 (see {@code DEC-03}) — and issues one bulk write. The uncontended - * path costs one CAS ({@code tryLock()}); contention falls back to an intrusive, allocation-free - * MPSC queue rather than blocking every writer on the lock. This is the project's single - * largest architectural risk and is proven or falsified by Phase 3's benchmark gate before any - * frame-layer code is written. - * - *

    The arena strategy

    - * HPACK is stateful compression: header bytes that enter the dynamic table must outlive the - * connection read buffer, and Huffman-coded values must be decoded somewhere. Flash copies each - * decoded header into a per-stream arena, not a shared one ({@code DEC-06}). This is not - * the minimal-copy design — a refcounted shared dynamic table would copy less — but it is the - * only design that is correct by construction under concurrent multiplexing: the demux thread - * can decode another stream's HEADERS, evicting dynamic-table entries, while a handler on a - * different virtual thread is still reading a view into a previous decode. A per-stream arena - * makes that race impossible without any cross-thread coordination on the hot path. See - * {@code flash/docs/http2/HPACK.md} (Phase 7) for the worked example. - * - *

    What this package deliberately does not implement

    - *
      - *
    • Server push ({@code PUSH_PROMISE}). Flash never sends it and rejects any - * {@code PUSH_PROMISE} received from a client as a connection error, since only servers may - * send it (RFC 9113 §8.4). Flash advertises {@code SETTINGS_ENABLE_PUSH = 0}. Justification: - * push is widely disabled by browsers and its cache-coherency benefits are better served by - * {@code 103 Early Hints} or resource hints, which do not require protocol-level state.
    • - *
    • Priority scheduling ({@code PRIORITY} frames, and the deprecated priority fields on - * {@code HEADERS}). RFC 9113 §5.3.2 itself says endpoints "SHOULD ignore" priority - * signalling — it was deprecated in the same RFC that (re)defined HTTP/2. Flash parses and - * discards {@code PRIORITY} frames (they must still be consumed, not rejected) and never acts - * on the priority fields.
    • - *
    • {@code Upgrade: h2c}. RFC 9113 §3.1 removed the HTTP/1.1 upgrade mechanism that - * RFC 7540 §3.2 defined. Cleartext HTTP/2 is reached only via prior knowledge (RFC 9113 §3.4), - * which is what every modern h2c client (notably gRPC) actually uses. See {@code DEC-10}.
    • - *
    - * - *

    Package layout

    - * This package is filled in incrementally, phase by phase — see - * {@code flash/docs/http2/IMPLEMENTATION-PLAN.md} Part III for the full schedule. As of Phase 0 - * it contains only the error model ({@link dev.relism.flash.h2.Http2ErrorCode}, - * {@link dev.relism.flash.h2.Http2Exception}, {@link dev.relism.flash.h2.Http2StreamException}) - * and the limits registry ({@link dev.relism.flash.h2.Http2Limits}). Subpackages - * {@code frame}, {@code hpack}, {@code stream}, {@code message}, and {@code upgrade} are added - * by Phases 3, 5, 7–9, and 14–15 respectively. - */ -package dev.relism.flash.h2; diff --git a/flash/src/main/java/dev/relism/flash/http/DateHeader.java b/flash/src/main/java/dev/relism/flash/http/DateHeader.java index 679c703..ddf10b9 100644 --- a/flash/src/main/java/dev/relism/flash/http/DateHeader.java +++ b/flash/src/main/java/dev/relism/flash/http/DateHeader.java @@ -6,14 +6,11 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; /** - * {@code EX-16}: RFC 9110 §6.6.1 — an origin server with a clock SHOULD send {@code Date}. * Flash never emitted it. Rather than formatting a timestamp on every response, a single * daemon thread refreshes a pre-encoded {@code "Date: ...\r\n"} field line once per second into * a {@code volatile byte[]}; {@code Http1ResponseWriter} writes it with one * {@code OutputStream#write(byte[])} — the cost per response is one volatile read and one - * write, never a format call (R4). * - *

    The parallel HPACK-encoded rendering for HTTP/2 responses is added in Phase 9. */ public final class DateHeader { diff --git a/flash/src/main/java/dev/relism/flash/http/Http1Limits.java b/flash/src/main/java/dev/relism/flash/http/Http1Limits.java index 661e80d..b921f94 100644 --- a/flash/src/main/java/dev/relism/flash/http/Http1Limits.java +++ b/flash/src/main/java/dev/relism/flash/http/Http1Limits.java @@ -4,14 +4,10 @@ package dev.relism.flash.http; * Bounds the HTTP/1.1 parser ({@code RequestParser}, {@code ChunkedInputStream}) enforces * against a peer's input, in one place. * - *

    Per R8, any code that reads a length, an index, a count, or a size off the wire checks it * against a named constant here — never against an ad-hoc literal, and never by letting the * underlying buffer throw on overrun. Each field's Javadoc names the specific attack it bounds. * - *

    Seeded in Phase 0 with the bounds required by {@code EX-03} (strict {@code Content-Length} - * parsing) and {@code EX-08} (header count/size limits); extended in Phase 1 with the chunked- - * transfer bounds ({@code EX-10}) and again in later phases as new h1 surfaces need a limit. - * Compare {@code dev.relism.flash.h2.Http2Limits}, the HTTP/2 equivalent. + * Compare {@code dev.relism.flash.http2.Http2Limits}, the HTTP/2 equivalent. */ public final class Http1Limits { @@ -30,7 +26,6 @@ public final class Http1Limits { * peer to a finite, known-in-advance number rather than the effectively unbounded * {@code Long.MAX_VALUE} the parser accepted before this limit existed. Comfortably above * {@code Integer.MAX_VALUE} (~2.1 billion) so legitimate very-large declared lengths are - * not confused with the int-overflow bug this same fix (EX-03) also closes. */ public static final long MAX_CONTENT_LENGTH = 4L * 1024 * 1024 * 1024; @@ -39,7 +34,6 @@ public final class Http1Limits { * request with tens of thousands of one-byte headers passes the total header-block size * check ({@code maxHeaderBufferSize}) while still forcing every subsequent * {@code Http1HeaderMap} lookup to scan all of them — turning a small request into quadratic CPU - * work per middleware that reads a header ({@code EX-08}, {@code EX-09}). */ public static final int MAX_HEADER_COUNT = 100; @@ -98,19 +92,16 @@ public final class Http1Limits { public static final int MAX_TRAILER_COUNT = 50; /** - * {@code EX-27}: response bodies at or below this size are copied into the same scratch * buffer as the response head (status line + headers) and written with it in a single * {@code OutputStream.write} call; larger bodies are written in a second {@code write} right * after the head, since copying a large body into the head buffer first would cost more * (an extra full-body memcpy) than the syscall it saves. 8 KiB — matches this codebase's * other "one socket-buffer's worth" constants ({@code ConnectionScratch.RELAY_BUFFER_SIZE}, * {@code BufferedByteSource.DEFAULT_BUFFER_SIZE}) rather than introducing an uncalibrated - * new number; see {@code DECISIONS.md} for the measurement that confirmed this default. */ public static final int INLINE_BODY_THRESHOLD = 8192; /** - * {@code EX-29}: maximum number of parts ({@code Multipart}) accepted in a single * {@code multipart/form-data} body. Without this bound, a peer can send an unbounded number * of minimal parts — each cheap individually but forcing unbounded growth of the parser's * {@code scanned} list and unbounded per-part header-parsing work, the multipart analogue of @@ -119,7 +110,6 @@ public final class Http1Limits { public static final int MAX_MULTIPART_PARTS = 1_000; /** - * {@code EX-29}: maximum number of header lines ({@code Content-Disposition}, * {@code Content-Type}, …) accepted per multipart part. Real clients send at most two or * three; without a bound a peer could send an effectively unlimited number before the blank * line that ends a part's header block, forcing unbounded {@code HashMap} growth per part. @@ -127,7 +117,6 @@ public final class Http1Limits { public static final int MAX_MULTIPART_PART_HEADER_COUNT = 20; /** - * {@code EX-29}: maximum length, in bytes, of a single header line within a multipart part's * header block. {@code Multipart.readLine} otherwise has no bound of its own to fall back * on — unlike the top-level HTTP headers (bounded by {@link #MAX_HEADER_VALUE_LENGTH} in * {@code RequestParser}), a line here with no {@code \r\n} would grow its {@code StringBuilder} @@ -136,7 +125,6 @@ public final class Http1Limits { public static final int MAX_MULTIPART_HEADER_LINE_LENGTH = 8_192; /** - * {@code EX-29}: maximum size, in bytes, of a single multipart part body that {@code Multipart} * buffers eagerly into a {@code byte[]} — text fields (always buffered) and, during a full * {@code parts()}/{@code parts(String)} scan, file bodies too. {@link #MAX_CONTENT_LENGTH} * bounds the whole request body, but at 4 GiB (and effectively unbounded for a chunked body, @@ -155,7 +143,6 @@ public final class Http1Limits { * hostile peer — a handler that calls {@code header(...)} in an unbounded loop (e.g. echoing * an unbounded collection into headers) would otherwise grow this connection's scratch region * without limit for the rest of its lifetime, since it is never shrunk back down between - * requests. Phase 6's zero-alloc DoD names this bound explicitly. */ public static final int MAX_RESPONSE_HEADER_BYTES = 65_536; diff --git a/flash/src/main/java/dev/relism/flash/http/HttpStatus.java b/flash/src/main/java/dev/relism/flash/http/HttpStatus.java index bb502c4..8e3d4ed 100644 --- a/flash/src/main/java/dev/relism/flash/http/HttpStatus.java +++ b/flash/src/main/java/dev/relism/flash/http/HttpStatus.java @@ -58,7 +58,6 @@ public enum HttpStatus { INSUFFICIENT_STORAGE (507, "Insufficient Storage"), NETWORK_AUTHENTICATION_REQUIRED (511, "Network Authentication Required"); - // EX-17: the bound used to be the hand-maintained constant 504, which silently threw // ArrayIndexOutOfBoundsException from this static initializer the moment any constant // above it (421, 431, 505, 507, 511 — several of which HTTP/2 needs, see MISDIRECTED_REQUEST // and REQUEST_HEADER_FIELDS_TOO_LARGE above) was added. Computed from values() instead, so diff --git a/flash/src/main/java/dev/relism/flash/http1/Http1Connection.java b/flash/src/main/java/dev/relism/flash/http1/Http1Connection.java index 143de76..2d70da1 100644 --- a/flash/src/main/java/dev/relism/flash/http1/Http1Connection.java +++ b/flash/src/main/java/dev/relism/flash/http1/Http1Connection.java @@ -39,16 +39,13 @@ public final class Http1Connection implements ConnectionProtocol { OutputStream out = ctx.out(); byte[] idleProbe = new byte[1]; - // EX-06 (router half): created once per connection, exactly like `parser` above, and // reused across every request on this connection — see AbstractRouter#newScratch. Object routeScratch = ctx.router().newScratch(); Object wsRouteScratch = ctx.wsRouter().newScratch(); - // EX-21: one Response per connection, repositioned (never reallocated) per request. Response pooledResponse = new Response(200, ContentType.TEXT_PLAIN); while (!ctx.stopped().getAsBoolean()) { - // EX-07: wait for the next request to begin, bounded by the generous // idle-keep-alive timeout — sitting idle between keep-alive requests is normal, not // an attack. Skipped when the parser already has bytes buffered from a previous // read (HTTP pipelining): the next request has, by definition, already started, so @@ -72,7 +69,6 @@ public final class Http1Connection implements ConnectionProtocol { try { request = parser.parse(in); } catch (MalformedRequestException e) { - // EX-02/03/08/18: a fixed, minimal, non-customizable rejection — never routed // through a handler or the user's exception handler — and the connection is // always closed afterwards, never kept alive. Response rejection = new Response(e.status(), e.getMessage(), ContentType.TEXT_PLAIN); @@ -124,7 +120,6 @@ public final class Http1Connection implements ConnectionProtocol { else if (result != null) response.setBody(result); } - // EX-32: re-checked here, not just before dispatch — a shutdown that begins while // this handler was running (the common case: draining connections mid-request) must // still force this response to Connection: close, not whatever was decided before // the handler ran. @@ -132,7 +127,6 @@ public final class Http1Connection implements ConnectionProtocol { Http1ResponseWriter.writeResponse(out, response, request.method(), actuallyKeepAlive, ctx.configuration().isSendDate(), ctx.scratch()); request.drain(); - // EX-22/EX-21: these instances are about to be repositioned over the next request (or // dropped, if the connection closes) — poison them in dev mode so any reference the // handler improperly retained (a captured field, an async callback) fails loudly on // its next access instead of silently reading whatever comes next. Only the pooled diff --git a/flash/src/main/java/dev/relism/flash/http1/Http1KeepAlive.java b/flash/src/main/java/dev/relism/flash/http1/Http1KeepAlive.java index 30857e9..f6765e7 100644 --- a/flash/src/main/java/dev/relism/flash/http1/Http1KeepAlive.java +++ b/flash/src/main/java/dev/relism/flash/http1/Http1KeepAlive.java @@ -7,7 +7,6 @@ import dev.relism.fpr.core.ByteView; * HTTP/1.1 keep-alive decision (RFC 9110 §7.6.1) and the shared {@code Connection} header * token-list scanner both it and WebSocket upgrade detection need. * - *

    {@code EX-13}: {@code Connection} is a comma-separated token list * (e.g. {@code "Connection: keep-alive, Upgrade"}), not a single value — a whole-value compare * against {@code "close"} misses exactly that case. {@link #tokenListContains} is the one * scanner both this class's {@link #isKeepAlive} and {@code WebSocketUpgrade}'s diff --git a/flash/src/main/java/dev/relism/flash/http1/Http1ResponseWriter.java b/flash/src/main/java/dev/relism/flash/http1/Http1ResponseWriter.java index 2b268ae..c68332a 100644 --- a/flash/src/main/java/dev/relism/flash/http1/Http1ResponseWriter.java +++ b/flash/src/main/java/dev/relism/flash/http1/Http1ResponseWriter.java @@ -18,7 +18,6 @@ import java.nio.charset.StandardCharsets; * serialization — routing, handler dispatch, and the request loop live in * {@link Http1Connection}. * - *

    {@code EX-27}: one bulk write, not ~10 small ones

    * The status line, {@code Content-Type}, {@code Date}, every custom header, and * {@code Content-Length}/{@code Connection} are all serialized into * {@link ConnectionScratch#responseHead} (a reused {@link ByteWriter}) before a single @@ -58,7 +57,6 @@ public final class Http1ResponseWriter { boolean keepAlive, boolean sendDate, ConnectionScratch scratch) throws IOException { int statusCode = response.getStatusCode(); // RFC 9110 §8.6/§15: 204, 304 and all 1xx responses MUST NOT carry Content-Length or a - // body at all — not "an empty one", none (EX-15). A HEAD response (RFC 9110 §9.3.2) // still reports the Content-Length GET would have, but never writes body bytes. boolean noContentAllowed = statusCode == 204 || statusCode == 304 || (statusCode >= 100 && statusCode < 200); boolean suppressBody = noContentAllowed || method == HttpMethod.HEAD; @@ -71,7 +69,6 @@ public final class Http1ResponseWriter { else writeStatusPhrase(head, statusCode); head.writeBytes(CRLF); - // EX-15: a Content-Type of ContentType.NONE (empty byte[]) used to still emit the line // "Content-Type: \r\n" — a header with no value. Skip the line entirely instead. byte[] contentType = response.getContentType(); if (contentType != null && contentType.length > 0) { @@ -80,7 +77,6 @@ public final class Http1ResponseWriter { head.writeBytes(CRLF); } - // EX-16: precomputed once per second by a shared daemon thread — one volatile read, // one write into the scratch, never a per-response format call. if (sendDate) head.writeBytes(DateHeader.bytes()); @@ -99,11 +95,9 @@ public final class Http1ResponseWriter { head.writeBytes(keepAlive ? CONNECTION_KEEPALIVE : CONNECTION_CLOSE); head.writeBytes(CRLF); - // EX-14: HEAD reports the Content-Length GET would have (above) but never writes // the body itself. boolean writeBody = body != null && !suppressBody; if (writeBody && len <= Http1Limits.INLINE_BODY_THRESHOLD) { - // EX-27: small body folded into the same scratch buffer — head + body leave in // one syscall. head.writeBytes(body); out.write(head.array(), 0, head.length()); diff --git a/flash/src/main/java/dev/relism/flash/h2/Http2ErrorCode.java b/flash/src/main/java/dev/relism/flash/http2/Http2ErrorCode.java similarity index 96% rename from flash/src/main/java/dev/relism/flash/h2/Http2ErrorCode.java rename to flash/src/main/java/dev/relism/flash/http2/Http2ErrorCode.java index d4125f9..917babe 100644 --- a/flash/src/main/java/dev/relism/flash/h2/Http2ErrorCode.java +++ b/flash/src/main/java/dev/relism/flash/http2/Http2ErrorCode.java @@ -1,4 +1,4 @@ -package dev.relism.flash.h2; +package dev.relism.flash.http2; /** * The 14 HTTP/2 error codes defined by RFC 9113 §7. @@ -6,7 +6,6 @@ package dev.relism.flash.h2; *

    Each constant carries its 4-byte big-endian wire encoding, precomputed once at class * load (RFC 9113 §6.4 {@code RST_STREAM} and §6.8 {@code GOAWAY} both carry the error code as * a raw 32-bit field — there is no framing around it to build). Callers write - * {@link #bytes()} directly into a frame payload; nothing is formatted at request time (R4). * *

    {@code Http2ErrorCode} is used to reject a peer and to interpret what a peer * sends us: {@link #fromCode(int)} decodes a received 32-bit value. RFC 9113 does not reserve diff --git a/flash/src/main/java/dev/relism/flash/h2/Http2Exception.java b/flash/src/main/java/dev/relism/flash/http2/Http2Exception.java similarity index 92% rename from flash/src/main/java/dev/relism/flash/h2/Http2Exception.java rename to flash/src/main/java/dev/relism/flash/http2/Http2Exception.java index cdc0d86..94efaf9 100644 --- a/flash/src/main/java/dev/relism/flash/h2/Http2Exception.java +++ b/flash/src/main/java/dev/relism/flash/http2/Http2Exception.java @@ -1,11 +1,10 @@ -package dev.relism.flash.h2; +package dev.relism.flash.http2; /** * A connection-level HTTP/2 error. Thrown anywhere a peer's frame, HPACK block, or * SETTINGS value violates the protocol in a way that leaves the connection's state (the HPACK * dynamic table, a flow-control window, the stream table) unrecoverable. * - *

    The connection demux loop ({@code Http2Connection}, Phase 8) catches this exception at a * single site: it sends {@code GOAWAY} with {@link #errorCode()} and closes the connection. * Compare {@link Http2StreamException}, whose scope is one stream and which results in * {@code RST_STREAM} while the connection survives. @@ -47,8 +46,6 @@ public final class Http2Exception extends RuntimeException { /** * Builds a connection error carrying a caller-supplied debug message. Allocates a new - * instance — acceptable per R2, since this exception always terminates the connection and - * R2 exempts error paths that terminate the connection. Use this overload whenever the * message carries information specific to this occurrence (e.g. the offending stream id or * a decoded value); use one of the preallocated singletons below when it does not. */ diff --git a/flash/src/main/java/dev/relism/flash/h2/Http2Limits.java b/flash/src/main/java/dev/relism/flash/http2/Http2Limits.java similarity index 87% rename from flash/src/main/java/dev/relism/flash/h2/Http2Limits.java rename to flash/src/main/java/dev/relism/flash/http2/Http2Limits.java index d6d5582..31cb188 100644 --- a/flash/src/main/java/dev/relism/flash/h2/Http2Limits.java +++ b/flash/src/main/java/dev/relism/flash/http2/Http2Limits.java @@ -1,22 +1,18 @@ -package dev.relism.flash.h2; +package dev.relism.flash.http2; /** * Every bound the HTTP/2 implementation enforces against a peer's input, in one place. * - *

    Per R8, any code that reads a length, an index, a count, or a size off the wire checks it - * against a named constant here — never against an ad-hoc literal, and never by letting the + * Every wire-derived length, index, count, or size is checked against a named constant here — + * never against an ad-hoc literal, and never by letting the * underlying array or buffer throw on overrun. Each field's Javadoc names the specific attack * or resource it bounds and, where one exists, the CVE. * - *

    These are compile-time defaults, not runtime configuration. The operationally-relevant - * subset is promoted to {@code FlashConfiguration} in Phase 13 task 10, once the whole surface - * has been exercised and it is clear which knobs operators actually need. Until then, changing - * a limit means changing this file. + *

    These are compile-time defaults, not runtime configuration. A limit becomes configurable + * only when the operational need and its safe range are established. * - *

    This class is added to incrementally: later phases add fields as the feature that needs - * them lands (e.g. {@code WRITE_TIMEOUT_MS} in Phase 3, {@code FRAME_READ_TIMEOUT_MS} in - * Phase 5). Phase 0 seeds the set called out explicitly by its task list; nothing here is a - * forward-declared placeholder — every field is already used by the phase that introduces it. + *

    Each field is introduced with the feature that enforces it; this class contains no unused + * placeholders. */ public final class Http2Limits { @@ -101,7 +97,6 @@ public final class Http2Limits { /** * The value of {@code SETTINGS_INITIAL_WINDOW_SIZE} Flash advertises for every new stream: * deliberately large (1 MiB, versus the RFC default of 65535) so that a normal-sized - * request or response body never blocks on a WINDOW_UPDATE round trip. See Phase 11 task 1. */ public static final int INITIAL_WINDOW_SIZE_LOCAL = 1_048_576; @@ -118,7 +113,6 @@ public final class Http2Limits { /** * The HPACK dynamic table size Flash's decoder honours, in bytes of RFC 7541 §4.1 * accounting. RFC 7541's protocol default. The encoder never uses a dynamic table at all - * (DEC-04), so this bound applies only to headers we receive. */ public static final int HPACK_DYNAMIC_TABLE_SIZE_LOCAL = 4_096; @@ -160,7 +154,6 @@ 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 - * BufferedByteSource}'s deadline mechanism already defends h1 against ({@code EX-07}): * without it, a peer that sends 9 header bytes and then never sends the declared payload * would hold this connection's frame reader waiting forever. */ diff --git a/flash/src/main/java/dev/relism/flash/h2/Http2StreamException.java b/flash/src/main/java/dev/relism/flash/http2/Http2StreamException.java similarity index 85% rename from flash/src/main/java/dev/relism/flash/h2/Http2StreamException.java rename to flash/src/main/java/dev/relism/flash/http2/Http2StreamException.java index a8826dd..d8dab3b 100644 --- a/flash/src/main/java/dev/relism/flash/h2/Http2StreamException.java +++ b/flash/src/main/java/dev/relism/flash/http2/Http2StreamException.java @@ -1,4 +1,4 @@ -package dev.relism.flash.h2; +package dev.relism.flash.http2; /** * A stream-level HTTP/2 error, scoped to one stream id. Results in an {@code RST_STREAM} @@ -12,11 +12,8 @@ package dev.relism.flash.h2; * *

    Why this allocates, unlike {@code Http2Exception}'s singletons

    * Every instance carries a distinct {@link #streamId()}, so it cannot be a shared singleton the - * way {@code Http2Exception}'s message-less constants are. This is still acceptable under R2: - * {@code RST_STREAM} generation is an error path, not the steady-state request path, and R2 * exempts error paths. The scenario where this matters most — a peer opening and resetting * thousands of streams per second (the Rapid Reset pattern, CVE-2023-44487) — is bounded by - * rate limits (Phase 13), not by making the rejection itself allocation-free; a hostile peer * that can force RST_STREAM generation fast enough for GC pressure to matter has already * tripped {@code Http2Limits.MAX_RESET_STREAMS_PER_INTERVAL} and the connection is being torn * down anyway. diff --git a/flash/src/main/java/dev/relism/flash/h2/frame/FrameFlags.java b/flash/src/main/java/dev/relism/flash/http2/frame/FrameFlags.java similarity index 98% rename from flash/src/main/java/dev/relism/flash/h2/frame/FrameFlags.java rename to flash/src/main/java/dev/relism/flash/http2/frame/FrameFlags.java index c92f35e..b61c4e6 100644 --- a/flash/src/main/java/dev/relism/flash/h2/frame/FrameFlags.java +++ b/flash/src/main/java/dev/relism/flash/http2/frame/FrameFlags.java @@ -1,4 +1,4 @@ -package dev.relism.flash.h2.frame; +package dev.relism.flash.http2.frame; /** * The frame-header flag bits (RFC 9113 §6), as bitwise constants plus predicate helpers. diff --git a/flash/src/main/java/dev/relism/flash/h2/frame/FrameHeader.java b/flash/src/main/java/dev/relism/flash/http2/frame/FrameHeader.java similarity index 96% rename from flash/src/main/java/dev/relism/flash/h2/frame/FrameHeader.java rename to flash/src/main/java/dev/relism/flash/http2/frame/FrameHeader.java index dc8cb41..c7b646c 100644 --- a/flash/src/main/java/dev/relism/flash/h2/frame/FrameHeader.java +++ b/flash/src/main/java/dev/relism/flash/http2/frame/FrameHeader.java @@ -1,4 +1,4 @@ -package dev.relism.flash.h2.frame; +package dev.relism.flash.http2.frame; /** * A flyweight over one frame's 9-byte header plus its payload location, both still living @@ -11,7 +11,6 @@ package dev.relism.flash.h2.frame; * the same reader — same "do not retain past the handler" rule the rest of this codebase's * buffer-backed flyweights (`Http1HeaderMap`, `WebSocketFrame`) already document. The payload bytes * are also transient: whatever layer needs to retain a DATA frame's payload past this window - * must copy it out (R3 — the connection read buffer is shared, single-threaded, and reused). * *

    Reserved bit and unknown types

    * {@link #streamId()} has already had the wire's reserved high bit (RFC 9113 §4.1: "R: A diff --git a/flash/src/main/java/dev/relism/flash/h2/frame/FrameType.java b/flash/src/main/java/dev/relism/flash/http2/frame/FrameType.java similarity index 95% rename from flash/src/main/java/dev/relism/flash/h2/frame/FrameType.java rename to flash/src/main/java/dev/relism/flash/http2/frame/FrameType.java index f2215fd..8e8cf25 100644 --- a/flash/src/main/java/dev/relism/flash/h2/frame/FrameType.java +++ b/flash/src/main/java/dev/relism/flash/http2/frame/FrameType.java @@ -1,4 +1,4 @@ -package dev.relism.flash.h2.frame; +package dev.relism.flash.http2.frame; /** * The 10 HTTP/2 frame types (RFC 9113 §6), plus the shared per-type validation rules @@ -9,7 +9,6 @@ package dev.relism.flash.h2.frame; * {@code UNKNOWN} constant would misleadingly suggest "a recognised category of unrecognised * frame", when the correct handling is simply "not this table, skip it"). * - *

    Per-type validation, table-driven (R4)

    * Each constant carries the RFC-mandated payload length bounds, whether a zero stream id is * required/forbidden/either, and whether the frame counts toward the CONTINUATION-flood guard * ({@code EX}-style defence, {@code Http2Limits#MAX_CONTINUATION_FRAMES_PER_BLOCK}) — see @@ -20,7 +19,6 @@ public enum FrameType { DATA(0x0, 0, Integer.MAX_VALUE, StreamIdRule.REQUIRED), /** RFC 9113 §6.2. Header block fragment (HPACK). Stream id required. */ HEADERS(0x1, 0, Integer.MAX_VALUE, StreamIdRule.REQUIRED), - /** RFC 9113 §6.3. Deprecated priority signal — parsed and discarded, never acted on (DEC, Phase 5 task 7). */ PRIORITY(0x2, 5, 5, StreamIdRule.REQUIRED), /** RFC 9113 §6.4. Stream-level error. Exactly 4 bytes (the error code). Stream id required. */ RST_STREAM(0x3, 4, 4, StreamIdRule.REQUIRED), diff --git a/flash/src/main/java/dev/relism/flash/h2/frame/FrameValidator.java b/flash/src/main/java/dev/relism/flash/http2/frame/FrameValidator.java similarity index 92% rename from flash/src/main/java/dev/relism/flash/h2/frame/FrameValidator.java rename to flash/src/main/java/dev/relism/flash/http2/frame/FrameValidator.java index 4a96637..bb91a9f 100644 --- a/flash/src/main/java/dev/relism/flash/h2/frame/FrameValidator.java +++ b/flash/src/main/java/dev/relism/flash/http2/frame/FrameValidator.java @@ -1,14 +1,13 @@ -package dev.relism.flash.h2.frame; +package dev.relism.flash.http2.frame; -import dev.relism.flash.h2.Http2ErrorCode; -import dev.relism.flash.h2.Http2Exception; -import dev.relism.flash.h2.Http2Limits; +import dev.relism.flash.http2.Http2ErrorCode; +import dev.relism.flash.http2.Http2Exception; +import dev.relism.flash.http2.Http2Limits; /** * Table-driven RFC 9113 per-frame-type validation: length bounds, the stream-id * required/forbidden/either rule, and the two special-cased structural rules ({@code SETTINGS}' * multiple-of-6 length, {@code PUSH_PROMISE} always rejected from a client) that do not fit a - * generic min/max/stream-id table. Table itself lives on {@link FrameType}'s constants (R4); this * class is the code that reads it. * *

    The error code is not uniform — read the RFC per violation, not just per type. A @@ -80,7 +79,6 @@ public final class FrameValidator { case EITHER -> { /* WINDOW_UPDATE: 0 (connection window) or non-zero (stream window) both valid */ } } - // RFC 9113 §8.4 / this codebase's DEC-10: PUSH_PROMISE is a server-to-client-only frame // (Flash advertises SETTINGS_ENABLE_PUSH=0 and never sends one); receiving one at all // means the peer believes it is talking to a client, which is always a protocol error. if (type == FrameType.PUSH_PROMISE) { diff --git a/flash/src/main/java/dev/relism/flash/h2/frame/FrameWriteBuffer.java b/flash/src/main/java/dev/relism/flash/http2/frame/FrameWriteBuffer.java similarity index 96% rename from flash/src/main/java/dev/relism/flash/h2/frame/FrameWriteBuffer.java rename to flash/src/main/java/dev/relism/flash/http2/frame/FrameWriteBuffer.java index 221ccb0..00f7e31 100644 --- a/flash/src/main/java/dev/relism/flash/h2/frame/FrameWriteBuffer.java +++ b/flash/src/main/java/dev/relism/flash/http2/frame/FrameWriteBuffer.java @@ -1,4 +1,4 @@ -package dev.relism.flash.h2.frame; +package dev.relism.flash.http2.frame; import dev.relism.flash.bytes.ByteWriter; @@ -10,7 +10,6 @@ import dev.relism.flash.bytes.ByteWriter; * size is rarely known before it is serialized (an HPACK-encoded header block, in particular, * has no cheap way to be measured in advance). * - *

    This is the reason {@link Http2FrameWriter} (Phase 3) serializes a complete buffer and * issues one bulk {@code write}, rather than streaming bytes as they are produced: streaming * would require knowing the length before the first byte goes out, which back-patching * deliberately avoids needing. diff --git a/flash/src/main/java/dev/relism/flash/h2/frame/Http2FrameReader.java b/flash/src/main/java/dev/relism/flash/http2/frame/Http2FrameReader.java similarity index 94% rename from flash/src/main/java/dev/relism/flash/h2/frame/Http2FrameReader.java rename to flash/src/main/java/dev/relism/flash/http2/frame/Http2FrameReader.java index 305266c..be51f7f 100644 --- a/flash/src/main/java/dev/relism/flash/h2/frame/Http2FrameReader.java +++ b/flash/src/main/java/dev/relism/flash/http2/frame/Http2FrameReader.java @@ -1,7 +1,7 @@ -package dev.relism.flash.h2.frame; +package dev.relism.flash.http2.frame; -import dev.relism.flash.h2.Http2Exception; -import dev.relism.flash.h2.Http2Limits; +import dev.relism.flash.http2.Http2Exception; +import dev.relism.flash.http2.Http2Limits; import dev.relism.flash.transport.BufferedByteSource; import java.io.EOFException; @@ -18,7 +18,6 @@ import java.util.Arrays; * One growable {@code byte[]} per connection, reused across every frame — the same * compact-before-grow discipline {@code RequestParser}'s own buffer uses. A frame's declared * length is checked against {@link Http2Limits#MAX_FRAME_SIZE_LOCAL} before the buffer - * is ever grown to accommodate it (R8): a hostile 16 MB declared length is rejected at the * length-check, not after an allocation already paid for it. * *

    Usage

    @@ -72,7 +71,6 @@ public final class Http2FrameReader { return null; // clean EOF: nothing buffered yet, peer closed between frames } int declaredLength = decodeLength(buffer, base); - // R8: checked BEFORE any further buffer growth or read — a hostile declared length // never causes an oversized allocation, only a rejection. if (declaredLength > Http2Limits.MAX_FRAME_SIZE_LOCAL) { throw Http2Exception.FRAME_SIZE_ERROR; diff --git a/flash/src/main/java/dev/relism/flash/h2/frame/Http2FrameWriter.java b/flash/src/main/java/dev/relism/flash/http2/frame/Http2FrameWriter.java similarity index 94% rename from flash/src/main/java/dev/relism/flash/h2/frame/Http2FrameWriter.java rename to flash/src/main/java/dev/relism/flash/http2/frame/Http2FrameWriter.java index 66b604a..acdbd6a 100644 --- a/flash/src/main/java/dev/relism/flash/h2/frame/Http2FrameWriter.java +++ b/flash/src/main/java/dev/relism/flash/http2/frame/Http2FrameWriter.java @@ -1,6 +1,6 @@ -package dev.relism.flash.h2.frame; +package dev.relism.flash.http2.frame; -import dev.relism.flash.h2.Http2Limits; +import dev.relism.flash.http2.Http2Limits; import java.io.IOException; import java.io.InterruptedIOException; @@ -25,7 +25,6 @@ import java.util.concurrent.locks.ReentrantLock; *

    Layer 2 — {@link ReentrantLock}, never {@code synchronized}. On Java 21, a virtual * thread blocking inside {@code synchronized} pins its carrier platform thread; blocking on a * {@link ReentrantLock} unmounts it instead (JEP 491, which removes the {@code synchronized} - * pinning behaviour, only lands in JDK 24+ — see {@code EX-01}, {@code DEC-03}). * {@code ReentrantLock} is also load-bearing here for a second reason {@code synchronized} * cannot offer: {@link ReentrantLock#tryLock()}. * @@ -72,7 +71,6 @@ import java.util.concurrent.locks.ReentrantLock; * the connection. This is bounded by {@link Http2Limits#WRITE_TIMEOUT_MS}, enforced by a shared * background reaper ({@link WriteTimeoutReaper}) that interrupts the blocked thread past the * deadline — {@code Socket#setSoTimeout} bounds reads, not writes, so it cannot be used here. - * Registration happens once per writer (connection-setup cost, not per write — R2 exempts * connection setup), so arming/disarming the deadline for each individual write is two * {@code volatile} field writes, not an allocation. */ @@ -111,8 +109,7 @@ public final class Http2FrameWriter { * the lock for longer than that stream's own single bulk write. * *

    Why the fast path is gated on {@code !queue.hasWork()}, not just {@code tryLock()} - * (found by this phase's own stress test, at N=64/256 — exactly the kind of bug R10 exists - * to catch): writing {@code intent} immediately, before anything already queued, is only + * Writing {@code intent} immediately, before anything already queued, is only * safe when nothing is already queued. Without the {@code hasWork()} check, this sequence * is possible — and violates same-producer ordering, which the stress test asserts: a * producer's {@code write(a)} then {@code write(b)} contends and both get queued @@ -197,11 +194,9 @@ public final class Http2FrameWriter { * A single shared daemon thread scanning every registered {@link Http2FrameWriter} for a * blocking write that has overrun {@link Http2Limits#WRITE_TIMEOUT_MS}. One thread for the * whole process (like {@code DateHeader}'s refresher), not one per connection — registration - * is the only per-connection cost, and it is a connection-setup-time cost (R2-exempt), not a * per-write one. * *

    Deliberately does not ask each write to record a {@code System.nanoTime()} - * deadline — an earlier version did, and Phase 3's own benchmark measured that single * {@code nanoTime()} call (plus the extra volatile field it required) costing enough to miss * the N=1 gate's 50 ns-over-baseline budget (recorded in {@code WRITER.md}). Instead, the * reaper counts consecutive scans a given writer has been observed still blocked @@ -239,7 +234,7 @@ public final class Http2FrameWriter { } } } - }, "flash-h2-write-timeout-reaper"); + }, "flash-http2-write-timeout-reaper"); reaper.setDaemon(true); reaper.start(); } diff --git a/flash/src/main/java/dev/relism/flash/h2/frame/IntrusiveMpscQueue.java b/flash/src/main/java/dev/relism/flash/http2/frame/IntrusiveMpscQueue.java similarity index 99% rename from flash/src/main/java/dev/relism/flash/h2/frame/IntrusiveMpscQueue.java rename to flash/src/main/java/dev/relism/flash/http2/frame/IntrusiveMpscQueue.java index 11649a4..e1dd83e 100644 --- a/flash/src/main/java/dev/relism/flash/h2/frame/IntrusiveMpscQueue.java +++ b/flash/src/main/java/dev/relism/flash/http2/frame/IntrusiveMpscQueue.java @@ -1,4 +1,4 @@ -package dev.relism.flash.h2.frame; +package dev.relism.flash.http2.frame; import java.util.concurrent.atomic.AtomicReference; diff --git a/flash/src/main/java/dev/relism/flash/h2/frame/Padding.java b/flash/src/main/java/dev/relism/flash/http2/frame/Padding.java similarity index 93% rename from flash/src/main/java/dev/relism/flash/h2/frame/Padding.java rename to flash/src/main/java/dev/relism/flash/http2/frame/Padding.java index 9105171..9f2acff 100644 --- a/flash/src/main/java/dev/relism/flash/h2/frame/Padding.java +++ b/flash/src/main/java/dev/relism/flash/http2/frame/Padding.java @@ -1,8 +1,8 @@ -package dev.relism.flash.h2.frame; +package dev.relism.flash.http2.frame; import dev.relism.flash.bytes.Pairs; -import dev.relism.flash.h2.Http2ErrorCode; -import dev.relism.flash.h2.Http2Exception; +import dev.relism.flash.http2.Http2ErrorCode; +import dev.relism.flash.http2.Http2Exception; /** * RFC 9113 §6.1 (DATA) / §6.2 (HEADERS) padding. When {@link FrameFlags#PADDED} is set, a @@ -16,7 +16,6 @@ import dev.relism.flash.h2.Http2Exception; *

    Flow control (forward note, not implemented here)

    * RFC 9113 §6.9.1: padding bytes count against the DATA flow-control window even though they * carry no data — the whole frame payload (pad-length byte + data + padding) is what a - * future Phase 11 flow controller must subtract from the window, not just {@link * #dataLength(long)}. This class only locates the data range within the payload; it performs no * flow-control accounting itself. */ diff --git a/flash/src/main/java/dev/relism/flash/h2/frame/WriteIntent.java b/flash/src/main/java/dev/relism/flash/http2/frame/WriteIntent.java similarity index 98% rename from flash/src/main/java/dev/relism/flash/h2/frame/WriteIntent.java rename to flash/src/main/java/dev/relism/flash/http2/frame/WriteIntent.java index d802a24..6ed07b7 100644 --- a/flash/src/main/java/dev/relism/flash/h2/frame/WriteIntent.java +++ b/flash/src/main/java/dev/relism/flash/http2/frame/WriteIntent.java @@ -1,4 +1,4 @@ -package dev.relism.flash.h2.frame; +package dev.relism.flash.http2.frame; /** * "Serialize yourself, then hand me the finished bytes." The interface a stream (and, diff --git a/flash/src/main/java/dev/relism/flash/models/HeaderView.java b/flash/src/main/java/dev/relism/flash/models/HeaderView.java index f805aab..712ac18 100644 --- a/flash/src/main/java/dev/relism/flash/models/HeaderView.java +++ b/flash/src/main/java/dev/relism/flash/models/HeaderView.java @@ -5,19 +5,15 @@ import dev.relism.fpr.core.ByteView; import java.util.List; /** - * The read-side contract every header container implements, protocol-neutral: {@link - * Http1HeaderMap} backs it with an HTTP/1.1 byte-buffer range today; a Phase 10 - * {@code Http2HeaderMap} will back it with HPACK-decoded (name, value) pairs. Neither concrete - * shape leaks into this interface — there is no {@code reset(byte[], int, int)} here, since that - * signature only makes sense for a byte-range-backed implementation. + * The read-side, protocol-neutral contract every header container implements. HTTP/1.1 uses a + * byte-range-backed implementation; HTTP/2 uses HPACK-decoded name/value pairs. Neither concrete + * representation leaks into this interface. * - *

    {@link RequestLine#getHeaders()} is typed as this interface (not a concrete class), which - * is what lets Phase 10 hand a {@link Request} an HPACK-backed header container without touching - * a single line of {@code Request}'s own code — the entire point of this phase's refactor (R1: - * h1 and h2 are peers behind a shared abstraction, never one forking the other). + *

    {@link RequestLine#getHeaders()} exposes this interface rather than a protocol-specific + * implementation so request handling remains independent of the transport protocol. * *

    Lifetime contract

    - * Every implementation lives on the connection (h1) or the stream (h2), not per-request, and is + * Every implementation lives on the connection (HTTP/1.1) or the stream (HTTP/2), not per-request, and is * repositioned in place between requests — never retain an instance past the handler that * received it. {@code String} values returned by {@link #first}/{@link #all} are safe to retain * (independent heap copies); {@link ByteView}s returned by {@link #view} and passed to {@link diff --git a/flash/src/main/java/dev/relism/flash/models/Http1HeaderMap.java b/flash/src/main/java/dev/relism/flash/models/Http1HeaderMap.java index 7c55435..b2877b4 100644 --- a/flash/src/main/java/dev/relism/flash/models/Http1HeaderMap.java +++ b/flash/src/main/java/dev/relism/flash/models/Http1HeaderMap.java @@ -22,8 +22,6 @@ import java.util.List; * instance per connection) lives in the root {@code dev.relism.flash} package, and {@code http1} * already depends on root (via {@code Http1Connection}'s use of {@code RequestParser}) — placing * this class in {@code http1} would require root to import back from {@code http1}, the exact - * kind of package cycle {@code DEC-19} already found and avoided once in this codebase. See - * {@code DECISIONS.md}, {@code DEC-22}, for the full reasoning; this note exists so a future * reader does not "fix" the location back to what the plan's Files list originally suggested. * *

    Lifetime contract — read carefully

    @@ -45,7 +43,6 @@ import java.util.List; * {@code byte[]} before leaving the synchronous handler scope. * * - *

    {@code EX-09}: an index built once per {@link #reset}, not rescanned per lookup

    * {@link #reset} scans the header section exactly once and records, per header, its name/value * byte offsets and a case-insensitive 32-bit hash of the name — into {@code int[]} arrays grown * (never shrunk) to this connection's high-water mark. Every lookup method @@ -62,7 +59,6 @@ public class Http1HeaderMap implements HeaderView { private int sectionStart; private int sectionEnd; - // EX-09 index — grown (never shrunk) to this connection's high-water mark, rebuilt in place // by every reset() call. Entry i's name is buffer[nameOffsets[i], nameOffsets[i]+nameLengths[i]), // its value is buffer[valueOffsets[i], valueOffsets[i]+valueLengths[i]). private int headerCount; @@ -72,7 +68,6 @@ public class Http1HeaderMap implements HeaderView { private int[] valueLengths = new int[INITIAL_INDEX_CAPACITY]; private int[] nameHashes = new int[INITIAL_INDEX_CAPACITY]; - // EX-05: pooled, reused slices for view() — see its own Javadoc for the reuse window. private final SlicePool viewPool = new SlicePool(VIEW_POOL_SIZE); // forEach's own pair, reused across every header of every call — same idiom as viewPool, @@ -81,7 +76,6 @@ public class Http1HeaderMap implements HeaderView { private Slice nameSlice; private Slice valueSlice; - /** Resets this map to the header section {@code buffer[sectionStart, sectionEnd)}, rebuilding the {@code EX-09} index. */ public void reset(byte[] buffer, int sectionStart, int sectionEnd) { this.buffer = buffer; this.sectionStart = sectionStart; @@ -112,7 +106,6 @@ public class Http1HeaderMap implements HeaderView { private void ensureIndexCapacity(int needed) { if (needed <= nameOffsets.length) return; - // EX-08 (Http1Limits.MAX_HEADER_COUNT) already rejects any request with more headers // than this before it ever reaches reset() — this can only fire while growing toward // that ceiling, never past it. Asserted, not silently truncated: an index that silently // dropped headers past this point would be a correctness bug, not a capacity one. @@ -203,7 +196,6 @@ public class Http1HeaderMap implements HeaderView { /** * Returns a zero-copy {@link ByteView} over the first value of {@code name}, or {@code null}. * - *

    {@code EX-05}: pooled, not allocated per call

    * The returned view is drawn from a small internal {@link SlicePool} rather than allocated * fresh. It stays valid until either the request ends, or {@link #view} is called * {@value #VIEW_POOL_SIZE} more times on this same {@code Http1HeaderMap} — whichever comes @@ -218,7 +210,6 @@ public class Http1HeaderMap implements HeaderView { return viewPool.acquire(buffer, valueOffsets[i], valueLengths[i]); } - /** Index into the {@code EX-09} arrays of the first header named {@code name}, or {@code -1}. */ private int indexOfHeader(String name) { if (buffer == null) return -1; int hash = ByteScan.hashNameIgnoreCaseAscii(name); diff --git a/flash/src/main/java/dev/relism/flash/models/PathParams.java b/flash/src/main/java/dev/relism/flash/models/PathParams.java index 0794206..7239ab2 100644 --- a/flash/src/main/java/dev/relism/flash/models/PathParams.java +++ b/flash/src/main/java/dev/relism/flash/models/PathParams.java @@ -10,9 +10,7 @@ import java.nio.charset.StandardCharsets; /** * Path parameters captured during routing, stored as byte offsets into the path view. * {@link #get} allocates a {@code String} on call (in one allocation when {@link #source} is - * {@link ArrayBackedByteView} — {@code EX-25} — two otherwise); {@link #view} is zero-copy. * - *

    Reusable instances ({@code EX-19})

    * The public constructor below builds a one-shot, fixed-size instance (used by * {@code AbstractWsRouter} and by tests) — {@code names.length} is taken as the exact param * count. {@code FastPathRouterImpl}'s per-connection scratch instead owns a single long-lived @@ -38,7 +36,6 @@ public class PathParams { private final int[] lens; private int count; - // EX-05: created lazily, only if view() is ever actually called. private SlicePool viewPool; public PathParams(ByteView source, String[] names, int[] starts, int[] lens) { @@ -85,7 +82,6 @@ public class PathParams { int i = indexOf(name); if (i < 0) return null; int start = starts[i], len = lens[i]; - // EX-25: a single-copy String construction when the source is a contiguous array slice // (always true for h1 today) instead of a byte-at-a-time copy into a scratch array // followed by a second allocation for the String itself. if (source instanceof ArrayBackedByteView abv) { @@ -97,7 +93,6 @@ public class PathParams { } /** - * Returns a zero-copy view over path param {@code name}, or {@code null}. {@code EX-05}: * drawn from a small internal {@link SlicePool} when {@link #source} is array-backed (always * true for h1 today) — same reuse-window contract as {@link Http1HeaderMap#view}. Falls back to a * fresh (allocating) view otherwise — never exercised on the real request path. diff --git a/flash/src/main/java/dev/relism/flash/models/PreEncodedHeader.java b/flash/src/main/java/dev/relism/flash/models/PreEncodedHeader.java index e0a24f3..e8cb604 100644 --- a/flash/src/main/java/dev/relism/flash/models/PreEncodedHeader.java +++ b/flash/src/main/java/dev/relism/flash/models/PreEncodedHeader.java @@ -7,7 +7,6 @@ import java.util.Arrays; * A header name/value pair pre-encoded once (typically at boot, as a {@code static final} * constant) and reused across many responses via {@link Response#header(PreEncodedHeader)}. * - *

    {@code EX-20}: why this exists alongside {@link Response#header(byte[])}

    * The older {@code header(byte[])} overload takes an already-fully-rendered h1 field line * (e.g. {@code "X-RateLimit-Limit: 100\r\n"}) — fine for h1, but not valid HPACK: HPACK encodes * a header as a compressed (name, value) pair, never as a literal CRLF-terminated line, so a @@ -15,11 +14,10 @@ import java.util.Arrays; * PreEncodedHeader} instead precomputes the {@code name}/{@code value} bytes separately * (still once, still at boot) so either protocol's writer can render them in its own format — * {@link Response#header(byte[])} is kept, working, for h1-only callers, but is documented as - * ignored on a future h2 response path (there is no way to recover structured name/value data + * ignored on a future HTTP/2 response path (there is no way to recover structured name/value data * from an opaque pre-rendered line); prefer this class for any header a handler wants to send on * both protocols. * - *

    The HPACK-encoded rendering itself is Phase 9 scope (no HPACK encoder exists yet) — this * class stores the raw {@code name}/{@code value} bytes now, which is everything a future HPACK * encoder needs to produce its own rendering from; it does not yet expose a precomputed HPACK * byte form, since building one before HPACK exists would be speculative, untested API surface. diff --git a/flash/src/main/java/dev/relism/flash/models/QueryParams.java b/flash/src/main/java/dev/relism/flash/models/QueryParams.java index 26cfc4b..27ecb29 100644 --- a/flash/src/main/java/dev/relism/flash/models/QueryParams.java +++ b/flash/src/main/java/dev/relism/flash/models/QueryParams.java @@ -22,7 +22,6 @@ public class QueryParams { private final ByteView raw; - // EX-05: created lazily, only if view() is ever actually called — QueryParams itself is // recreated per request (see Request#resolveQueryParams), so an eagerly-constructed pool // would cost VIEW_POOL_SIZE allocations on every request that touches query params at all, // even the (currently: every) request that never calls view(). @@ -40,7 +39,6 @@ public class QueryParams { /** * Returns a view over the first raw (not percent-decoded) value of {@code name}, or - * {@code null}. {@code EX-05}: drawn from a small internal {@link SlicePool} when * {@link #raw} is array-backed (always true for h1 today) instead of allocated per call — * same reuse-window contract as {@link Http1HeaderMap#view}: valid until either the request ends * or {@link #view} is called {@value #VIEW_POOL_SIZE} more times on this instance, whichever @@ -116,7 +114,6 @@ public class QueryParams { * {@code %XX} triplets are decoded to their byte values; {@code +} decodes as space. * Invalid {@code %} sequences are passed through as-is. * - *

    {@code EX-26}: the overwhelmingly common query value contains neither {@code %} nor * {@code +} — scanned for first; when clean and {@link #raw} is array-backed, the * {@code String} is built directly from the backing array in one allocation, skipping the * scratch {@code byte[]} copy this method used to make unconditionally for every value. diff --git a/flash/src/main/java/dev/relism/flash/models/Request.java b/flash/src/main/java/dev/relism/flash/models/Request.java index 5809710..82ca3f0 100644 --- a/flash/src/main/java/dev/relism/flash/models/Request.java +++ b/flash/src/main/java/dev/relism/flash/models/Request.java @@ -29,7 +29,6 @@ import java.util.List; * }); * } * - *

    {@code EX-22}: pooled, not allocated per request

    * A {@code Request} instance is owned by its connection (HTTP/1.1) or its stream (HTTP/2) and is * recycled after the handler returns. Do not retain it — the same instance is repositioned * over the next request's data as soon as this one's handler returns. {@code equals}/ @@ -48,7 +47,6 @@ import java.util.List; * loudly, and at the exact call site that misused it — instead of silently reading whatever the * next (or a completely different) request happened to reset this instance to. In production * this check is a single {@code boolean} field read gated behind a {@code static final} flag the - * JIT treats as a trusted constant once the class is initialized — see {@code DECISIONS.md} for * the measured cost. */ public class Request { @@ -65,7 +63,6 @@ public class Request { private InetSocketAddress remoteAddress; private SSLSocket sslSocket; - // EX-22 dev-mode poisoning guard: true from reset() until recycle() marks this instance // unsafe to use further. Only consulted when poisoningEnabled is true (see checkActive()). private boolean active; @@ -137,7 +134,6 @@ public class Request { /** * Repositions {@code pooled} over a freshly-parsed request. {@code body} is already fully * configured by the caller ({@code RequestParser}, which owns and resets its own pooled - * {@link RequestBody} for the fixed-length/chunked/empty cases — see {@code EX-22}) — this * method's only job is wiring it, {@code requestLine}, and the connection identity fields * into {@code pooled}. */ @@ -160,7 +156,6 @@ public class Request { checkActive(); if (cachedPath != null) return cachedPath; ByteView v = requestLine.getPath(); - // EX-25: one allocation via a direct String(array, offset, length) construction when the // view is a contiguous array slice (always true for h1 today), instead of a byte-at-a-time // copy into a scratch array followed by a second allocation for the String itself. if (v instanceof ArrayBackedByteView abv) { diff --git a/flash/src/main/java/dev/relism/flash/models/RequestBody.java b/flash/src/main/java/dev/relism/flash/models/RequestBody.java index 1f71813..44895a6 100644 --- a/flash/src/main/java/dev/relism/flash/models/RequestBody.java +++ b/flash/src/main/java/dev/relism/flash/models/RequestBody.java @@ -10,7 +10,6 @@ import java.io.*; * Safe to call multiple times; the second call returns the cached array. Throws for * bodies larger than 2 GB. *
  • {@link #stream()} — returns a bounded {@link InputStream} without upfront allocation. - * For fixed-length bodies this is a reused, repositioned view (see {@code EX-23} below) * 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.
  • * @@ -21,13 +20,11 @@ import java.io.*; *

    Keep-alive: unread body bytes are discarded by {@link Request#drain()} after the * handler returns so the socket is correctly positioned for the next pipelined request. * - *

    {@code EX-22}: pooled, not allocated per request

    * One instance per connection (owned by {@code RequestParser}, repositioned via {@link #reset} * for every request), the same treatment {@link Request}/{@link RequestLine} get. The {@link * #of(byte[])} factory below remains for test/manual construction and returns a freestanding, * unpooled instance — exactly like {@link Request}'s own manual constructor. * - *

    {@code EX-23}/{@code EX-24}: the reusable bounded stream and drain buffer

    * {@link #stream()} used to allocate a {@link SequenceInputStream}, a {@link ByteArrayInputStream} * and an anonymous bounded {@link InputStream} on every call. It now hands out one persistent * {@link BoundedBufferedInputStream}, repositioned per request instead of reallocated. @@ -45,10 +42,8 @@ public class RequestBody { private byte[] resolved; private long socketConsumed; - // EX-23: created once, repositioned per request via reset()'s call into boundedStream.reset(...). private BoundedBufferedInputStream boundedStream; - // EX-24: created lazily on first chunked-body drain(), then reused for the life of the connection. private byte[] drainBuffer; /** Pooled instance, populated later via {@link #reset}. One per connection — see {@code RequestParser}. */ @@ -128,7 +123,6 @@ public class RequestBody { * Returns a bounded {@link InputStream} over the body without upfront allocation. * *

    For fixed-length bodies: a reused {@link BoundedBufferedInputStream} (see the class - * Javadoc, {@code EX-23}) serving any already-buffered header bytes followed by a bounded * view of the socket stream — zero allocation on a warm connection. * *

    For chunked bodies: the raw {@link dev.relism.ChunkedInputStream} that de-chunks on @@ -136,7 +130,6 @@ public class RequestBody { * the next keep-alive request. * *

    If {@link #bytes()} was called first, returns a fresh {@link java.io.ByteArrayInputStream} - * over the cached array — a rare dual-access pattern, not the hot path {@code EX-23} targets. */ public InputStream stream() { if (resolved != null) return new ByteArrayInputStream(resolved); @@ -152,7 +145,6 @@ public class RequestBody { void drain() { if (isEmpty() || resolved != null) return; if (contentLength < 0) { - // EX-24: InputStream.transferTo's default implementation allocates a fresh 8 KiB // byte[] on every call — replaced with a buffer this instance allocates once // (lazily, only if a chunked body is ever actually drained) and reuses thereafter. if (drainBuffer == null) drainBuffer = new byte[8192]; @@ -167,7 +159,6 @@ public class RequestBody { } /** - * {@code EX-23}: a reused, repositionable {@link InputStream} that serves bytes first from a * caller-owned pre-buffered array, then from the socket, bounded overall to a fixed length — * replacing the {@code SequenceInputStream}+{@code ByteArrayInputStream}+anonymous-bounded- * stream trio that used to be allocated fresh on every {@link #stream()} call. One instance diff --git a/flash/src/main/java/dev/relism/flash/models/RequestLine.java b/flash/src/main/java/dev/relism/flash/models/RequestLine.java index 3eee779..4a21c18 100644 --- a/flash/src/main/java/dev/relism/flash/models/RequestLine.java +++ b/flash/src/main/java/dev/relism/flash/models/RequestLine.java @@ -8,7 +8,6 @@ import dev.relism.flash.http.HttpMethod; * and the header container. Internal — reached via {@link Request#getRequestLine()}, not * user-facing API. * - *

    Pooled, like {@link Request} ({@code EX-22})

    * One instance per connection, repositioned via {@link #reset} for every request rather than * reallocated — {@code RequestParser} owns it exactly the way it owns {@link Http1HeaderMap}. * {@link #reset} is {@code public} rather than package-private — matching diff --git a/flash/src/main/java/dev/relism/flash/models/Response.java b/flash/src/main/java/dev/relism/flash/models/Response.java index c1d0375..a24144c 100644 --- a/flash/src/main/java/dev/relism/flash/models/Response.java +++ b/flash/src/main/java/dev/relism/flash/models/Response.java @@ -28,7 +28,6 @@ import java.util.List; * return new Response(200, ContentType.TEXT_PLAIN).chunked(source); * } * - *

    {@code EX-21}: pooled, not allocated per request

    * The connection driver (e.g. {@code Http1Connection}) owns one {@code Response} instance per * connection, reset before every handler call rather than reallocated — the same treatment * {@link Request} gets (see its Javadoc for the full pooling/dev-mode-guard rationale, which @@ -47,7 +46,6 @@ public class Response { private boolean chunked; private byte[] contentType; - // EX-20: custom headers stored as (name, value) byte pairs in one growable region, instead // of a List of fully-rendered "Name: Value\r\n" lines (which cost a StringBuilder + // char[] + String + getBytes() chain per header(String,String) call). Two backing stores, // unified into one insertion-ordered sequence via headerTags/headerRefs, since a fully @@ -63,7 +61,6 @@ public class Response { private int[] headerRefs; // one entry per header(), in call order: index into the tag's store private int headerCount; // total header() calls this response has recorded - // EX-21 dev-mode poisoning guard -- see Request's identical mechanism for the full rationale. private boolean active = true; private static volatile boolean poisoningEnabled = Flash.DEV; @@ -207,7 +204,6 @@ public class Response { } /** - * Adds a response header. {@code EX-20}: writes {@code name}/{@code value} directly into a * reused byte region (via {@link ByteWriter#writeAscii}) instead of building an intermediate * {@code String} and re-encoding it — zero allocation once the region has grown to this * connection's high-water mark. @@ -241,7 +237,7 @@ public class Response { /** * Adds a header from a {@link PreEncodedHeader} built once (typically at boot). Copies its * precomputed {@code name}/{@code value} bytes into this response's region — a memcpy, not a - * re-encode, and usable by a future h2 response path (unlike {@link #header(byte[])}) since + * re-encode, and usable by a future HTTP/2 response path (unlike {@link #header(byte[])}) since * the name/value structure survives. */ public Response header(PreEncodedHeader preEncoded) { @@ -277,7 +273,7 @@ public class Response { * *

    h1-only: a rendered {@code "Name: Value\r\n"} line carries no structured * name/value data an HPACK encoder could use, so this header is not representable on a - * future h2 response path — prefer {@link #header(PreEncodedHeader)} for anything that must + * future HTTP/2 response path — prefer {@link #header(PreEncodedHeader)} for anything that must * render correctly on both protocols. Kept for existing h1-only callers. */ public Response header(byte[] preEncoded) { @@ -289,11 +285,7 @@ public class Response { return this; } - /** - * {@code EX-nn}: bounds the response-side analogue of the request header limits — a handler - * that calls {@code header(...)} in an unbounded loop must not grow this connection's - * per-request scratch state without limit (Phase 6's zero-alloc DoD names this explicitly). - */ + /** Prevents an unbounded header loop from growing the connection's response scratch state. */ private void checkHeaderBudget() { if (headerCount >= Http1Limits.MAX_RESPONSE_HEADER_COUNT) { throw new IllegalStateException("response exceeds " + Http1Limits.MAX_RESPONSE_HEADER_COUNT @@ -397,7 +389,6 @@ public class Response { /** * Writes every custom header directly into {@code head} (a scratch {@link ByteWriter} — - * see {@code EX-27}), in call order. Zero-alloc when no headers are set or on a warm region. * This is what {@code Http1ResponseWriter} uses; {@link #writeHeaders(OutputStream)} below * (the {@code OutputStream} equivalent) exists for the streaming-body write paths that * cannot fold their whole write into one scratch buffer. diff --git a/flash/src/main/java/dev/relism/flash/models/ResponseSerializer.java b/flash/src/main/java/dev/relism/flash/models/ResponseSerializer.java index 775fb45..d8c3fa0 100644 --- a/flash/src/main/java/dev/relism/flash/models/ResponseSerializer.java +++ b/flash/src/main/java/dev/relism/flash/models/ResponseSerializer.java @@ -5,8 +5,7 @@ import java.nio.charset.StandardCharsets; /** * The protocol-neutral enumeration of a {@link Response}'s header fields — one source of truth * consumed by every protocol's own writer, so {@code Content-Type}/custom-header logic is never - * duplicated (and cannot drift) between {@code Http1ResponseWriter} and a future h2 encoder - * (Phase 9). {@code Http1ResponseWriter} renders each field as {@code "Name: Value\r\n"}; the h2 + * duplicated (and cannot drift) between {@code Http1ResponseWriter} and a future HTTP/2 encoder * encoder will render the same fields via HPACK. * *

    Scope: response-object fields only, not connection framing

    @@ -22,7 +21,7 @@ import java.nio.charset.StandardCharsets; * recoverable (name, value) structure — see that method's own Javadoc — so it cannot appear in * this enumeration. {@code Http1ResponseWriter} still renders it (via {@link * Response#writeHeaders}, which handles both structured and raw entries, in the original call - * order); a future h2 writer will not be able to. + * order); a future HTTP/2 writer will not be able to. */ public final class ResponseSerializer { private ResponseSerializer() {} @@ -37,7 +36,6 @@ public final class ResponseSerializer { /** * Enumerates {@code response}'s fields in a fixed, deterministic order: {@code Content-Type} - * first (if set to a non-empty value — {@code EX-15}: {@code ContentType.NONE} emits * nothing, never an empty-valued header line), then every {@code header(String,String)}/ * {@code header(PreEncodedHeader)}-added field in call order. Zero allocation: every byte * range handed to {@code consumer} is a slice of {@code response}'s own already-allocated diff --git a/flash/src/main/java/dev/relism/flash/routing/AbstractRouter.java b/flash/src/main/java/dev/relism/flash/routing/AbstractRouter.java index 58c9baa..94959d0 100644 --- a/flash/src/main/java/dev/relism/flash/routing/AbstractRouter.java +++ b/flash/src/main/java/dev/relism/flash/routing/AbstractRouter.java @@ -108,14 +108,12 @@ public abstract class AbstractRouter { * same "create once per connection, reuse across requests" shape already used there for * {@code RequestParser}. * - *

    {@code EX-06}'s router-half fix: a {@code ThreadLocal} here would mean "one per virtual * thread", which under this codebase's one-virtual-thread-per-connection model is "one per * connection with no upper bound and no pooling" — exactly the failure mode * {@code ConnectionScratch} already exists to avoid for every other per-connection buffer. * An explicit, caller-owned scratch object achieves the same per-connection reuse without * that unbounded-growth risk, and without requiring {@code routing} to depend on * {@code transport}'s {@code ConnectionScratch} type (this package has no such dependency - * today — see {@code DECISIONS.md}, {@code DEC-19}, for why that boundary was kept rather * than extending {@code ConnectionScratch} itself, which is what an earlier draft of this * fix assumed). */ diff --git a/flash/src/main/java/dev/relism/flash/routing/AbstractWsRouter.java b/flash/src/main/java/dev/relism/flash/routing/AbstractWsRouter.java index 86252bf..416ffd9 100644 --- a/flash/src/main/java/dev/relism/flash/routing/AbstractWsRouter.java +++ b/flash/src/main/java/dev/relism/flash/routing/AbstractWsRouter.java @@ -18,7 +18,6 @@ public abstract class AbstractWsRouter { /** * Creates a fresh per-connection scratch object for {@link #route}, or {@code null} if this * router keeps no reusable per-connection state — see {@link AbstractRouter#newScratch} for - * the full rationale ({@code EX-06}'s router-half fix), mirrored here for the WebSocket * router. */ public Object newScratch() { diff --git a/flash/src/main/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathRouterImpl.java b/flash/src/main/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathRouterImpl.java index fa712f6..12c821b 100644 --- a/flash/src/main/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathRouterImpl.java +++ b/flash/src/main/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathRouterImpl.java @@ -19,7 +19,6 @@ import java.util.Arrays; * virtual {@code METHOD + path} byte sequence in a single pass; the per-connection * {@link RouteScratch} ({@link #newScratch}) owns the reused {@link MatchResult}, * {@link FastPathViews.MethodPathByteView} and path-param arrays that would otherwise allocate - * (or, before {@code EX-06}'s router-half fix, sit in an unbounded {@code ThreadLocal}) on every * request. */ public class FastPathRouterImpl extends AbstractRouter { @@ -30,12 +29,10 @@ public class FastPathRouterImpl extends AbstractRouter { public FastPathRouterImpl() {} /** - * Per-connection reusable matching state — {@code EX-06}'s router half and {@code EX-19} * together. Created once per connection by {@link #newScratch} and threaded back into every * {@link #route} call for that connection's lifetime (see {@link AbstractRouter#newScratch} * for why this replaced the two {@code ThreadLocal}s this class used to hold). * - *

    {@code paramNames}/{@code paramStarts}/{@code paramLens} ({@code EX-19}) start small and * grow (doubling, via {@link #ensureParamCapacity}) to the connection's high-water mark — * the number of path params the most param-heavy route matched on this connection ever * needed — and are never shrunk back down or reallocated once warm, the same amortized policy diff --git a/flash/src/main/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathViews.java b/flash/src/main/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathViews.java index 13f5b18..2601fd7 100644 --- a/flash/src/main/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathViews.java +++ b/flash/src/main/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathViews.java @@ -14,7 +14,6 @@ import java.nio.charset.StandardCharsets; public final class FastPathViews { /** - * {@code EX-04}: {@code fpr-core}'s decompiled {@code ByteCompare} (its word-at-a-time * router-matching fast path — see {@code ByteCompare.equals}/{@code indexOf}) reads a * comparison word via {@code MethodHandles.byteArrayViewVarHandle(long[].class, * ByteOrder.LITTLE_ENDIAN)} and compares it bit-for-bit against whatever @@ -35,14 +34,12 @@ public final class FastPathViews { * {@link ByteView#longAt} implementation to. Caller-guaranteed contract (never asserted here * — {@code ByteCompare} itself never calls this without first checking {@code pos + 8 <= * length}, so a defensive check here would be dead code on every real call path; see - * {@code EX-04}'s registry entry): {@code pos + 8 <= array.length}. */ private static long longAtLittleEndian(byte[] array, int pos) { return (long) LONG_VIEW_LE.get(array, pos); } /** - * {@code EX-42}: not immutable — {@link #reset} repositions an existing instance over new * bounds instead of requiring a fresh allocation. {@code RequestParser} owns one pooled * instance per role (path/query/protocol) per connection and calls {@link #reset} on it for * every request, the same "do not retain past the handler" pooling contract every other @@ -91,7 +88,6 @@ public final class FastPathViews { return start; } - /** {@code EX-04}: array-backed and contiguous — the word-at-a-time router path applies. */ @Override public boolean supportsLong() { return true; @@ -110,9 +106,7 @@ public final class FastPathViews { /** * Mutable composite view: method bytes + path. Reused per connection, call {@link #reset} - * before use (see {@code FastPathRouterImpl}'s per-connection scratch, {@code EX-06}). * - *

    {@code EX-04}: deliberately not array-backed, {@code supportsLong()} stays {@code false}

    * Unlike every other view in this file, this one is a composite of two independent sources * (a raw {@code byte[]} for the method, and another {@link ByteView} — itself possibly * array-backed — for the path). There is no single backing array a word-at-a-time read could @@ -173,7 +167,6 @@ public final class FastPathViews { return 0; } - /** {@code EX-04}: array-backed and contiguous — the word-at-a-time router path applies. */ @Override public boolean supportsLong() { return true; @@ -212,7 +205,6 @@ public final class FastPathViews { return 0; } - /** {@code EX-04}: array-backed and contiguous — the word-at-a-time router path applies. */ @Override public boolean supportsLong() { return true; diff --git a/flash/src/main/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathWsRouterImpl.java b/flash/src/main/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathWsRouterImpl.java index ebd3f60..e8be90d 100644 --- a/flash/src/main/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathWsRouterImpl.java +++ b/flash/src/main/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathWsRouterImpl.java @@ -12,9 +12,7 @@ import dev.relism.flash.websocket.WebSocketHandler; /** * WebSocket-upgrade counterpart of {@link FastPathRouterImpl} — same {@code fpr-core} matching - * engine, same {@code EX-06} router-half fix (an explicit per-connection {@link RouteScratch} * via {@link #newScratch} in place of the {@code ThreadLocal}s this class used to hold). Unlike - * {@link FastPathRouterImpl}, its path-param extraction is not covered by {@code EX-19} (that * registry entry names {@code FastPathRouterImpl.route} specifically) and still allocates a * fresh {@code PathParams} per matched, parametric WebSocket upgrade — WebSocket upgrades are * inherently rare relative to ordinary requests (one per connection, not one per message), so @@ -26,9 +24,7 @@ public final class FastPathWsRouterImpl extends AbstractWsRouter { private volatile FastPathRouter router; private String[] cachedParamNames; - /** Per-connection reusable matching state — see {@link FastPathRouterImpl.RouteScratch}'s - * javadoc for the full {@code EX-06} rationale; this router's scratch is smaller since - * {@code EX-19}'s path-param reuse does not apply here (see the class Javadoc). */ + /** Per-connection reusable matching state. */ static final class RouteScratch { final MatchResult matchResult = new MatchResult<>(32, 128); final FastPathViews.MethodPathByteView combinedView = new FastPathViews.MethodPathByteView(); diff --git a/flash/src/main/java/dev/relism/flash/template/ByteTemplate.java b/flash/src/main/java/dev/relism/flash/template/ByteTemplate.java index 48762f7..3a85880 100644 --- a/flash/src/main/java/dev/relism/flash/template/ByteTemplate.java +++ b/flash/src/main/java/dev/relism/flash/template/ByteTemplate.java @@ -15,7 +15,6 @@ import java.util.Map; *

    * Layout: seg[0] slot[0] seg[1] slot[1] … seg[n-1] slot[n-1] seg[n] * - *

    {@code EX-28}: slot lookup is O(1) per key-value pair, not O(slots)

    * A slot name can appear more than once (e.g. {@code {{var}} == {{var}}}), so the map built at * construction maps each name to the (usually single-element) array of every slot index using * that name, instead of the nested "scan every slot for every pair" loop this used to do. diff --git a/flash/src/main/java/dev/relism/flash/tls/TlsConfig.java b/flash/src/main/java/dev/relism/flash/tls/TlsConfig.java index 5006019..e245773 100644 --- a/flash/src/main/java/dev/relism/flash/tls/TlsConfig.java +++ b/flash/src/main/java/dev/relism/flash/tls/TlsConfig.java @@ -54,7 +54,6 @@ public final class TlsConfig { private static final String[] SECURE_PROTOCOLS = { "TLSv1.3", "TLSv1.2" }; /** - * {@code EX-31}: RFC 9113 §9.2.2 requires that an HTTP/2 endpoint MUST NOT use any of these * cipher suites over TLS 1.2 (the list is unchanged from RFC 7540 Appendix A, which 9113 * carries forward verbatim), and that it MUST support at least * {@code TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}. TLS 1.3 is unaffected: none of its cipher @@ -65,11 +64,9 @@ public final class TlsConfig { *

    Transcribed from Go's {@code golang.org/x/net/http2} {@code isBadCipher} table * (BSD-licensed, itself an implementation of this exact RFC 9113 requirement, cross-checked * against the IANA TLS Cipher Suite registry) rather than by hand from the RFC text, for the - * same reason Appendix D of {@code flash/docs/http2/IMPLEMENTATION-PLAN.md} insists the HPACK * static table be transcribed from the RFC directly and verified: a transcription error in a * ~280-entry list is easy to make and easy to miss, and here the failure mode is silently * permitting a cipher suite RFC 9113 requires rejecting. Built once, in a static - * initializer (R4) — never reconstructed per connection. */ private static final Set TLS12_H2_BLOCKED_CIPHERS = Set.of( "TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", @@ -435,7 +432,6 @@ public final class TlsConfig { if (clientAuth == ClientAuth.REQUIRE) socket.setNeedClientAuth(true); else if (clientAuth == ClientAuth.OPTIONAL) socket.setWantClientAuth(true); - // EX-31: RFC 9113 §9.2.2 — when this listener can negotiate h2, the enabled cipher // suite list must exclude every suite on the TLS 1.2 blocklist. TLS 1.3 suites are // never in that list (see TLS12_H2_BLOCKED_CIPHERS's Javadoc) so this only narrows // which TLS 1.2 suites remain available; TLS 1.3 is unaffected either way. @@ -452,7 +448,6 @@ public final class TlsConfig { /** * Whether this listener's configured ALPN protocol list ({@link #applicationProtocols}) * includes {@code "h2"}. Lets a caller (the connection runner, for logging; {@link #applyTo} - * itself, for {@code EX-31}'s cipher filtering) know a listener's h2 capability without * duplicating the offered-protocols check. */ public boolean negotiatesH2() { diff --git a/flash/src/main/java/dev/relism/flash/transport/BufferedByteSource.java b/flash/src/main/java/dev/relism/flash/transport/BufferedByteSource.java index 6acc13d..a160c51 100644 --- a/flash/src/main/java/dev/relism/flash/transport/BufferedByteSource.java +++ b/flash/src/main/java/dev/relism/flash/transport/BufferedByteSource.java @@ -7,7 +7,6 @@ import java.net.SocketTimeoutException; /** * The single buffered view over one connection's inbound bytes, for the whole lifetime of the - * connection. Fixes {@code EX-10} (one syscall per byte in {@code ChunkedInputStream}) and * gives {@link dev.relism.flash.transport.ProtocolNegotiator} a way to inspect the first bytes * of a plaintext connection (the h2c preface) without consuming them. * @@ -30,7 +29,6 @@ import java.net.SocketTimeoutException; * absolute {@link System#nanoTime()} deadline; every underlying socket read computes the * remaining budget and hands exactly that to {@code setSoTimeout} before reading, so a * {@link SocketTimeoutException} from an underlying read unambiguously means the deadline — - * not just one read — has been exceeded. This is what {@code EX-07} requires: "implement that * deadline, do not rely on {@code setSoTimeout} alone." * *

    Thread-safety

    @@ -92,7 +90,6 @@ public final class BufferedByteSource extends InputStream { * ({@code SO_TIMEOUT = 0}). Must be called before any read the caller wants to be * unbounded (e.g. handing the connection off to a long-lived WebSocket session loop). * - *

    {@code EX-37}: a {@code null} socket (the constructor accepts one — every isolated unit * test in this codebase that constructs a {@code BufferedByteSource} directly over a * {@code ByteArrayInputStream} passes {@code null}, since there is no real connection to * bound) is treated as "no OS-level timeout to clear", not an error — only the deadline @@ -255,7 +252,6 @@ public final class BufferedByteSource extends InputStream { * before reading, so a {@link SocketTimeoutException} from {@code in.read} unambiguously * means the deadline — not merely one read — has elapsed; see the class Javadoc. * - *

    {@code EX-37}: the expiry check above (throwing once {@code remainingNanos <= 0}) runs * regardless of whether a real {@link Socket} is present; only the OS-level * {@code setSoTimeout} call — meaningless without a socket, and previously called * unconditionally, which NPE'd the instant any deadline-bounded read ran against a diff --git a/flash/src/main/java/dev/relism/flash/transport/ConnectionContext.java b/flash/src/main/java/dev/relism/flash/transport/ConnectionContext.java index f7024d3..e47b51f 100644 --- a/flash/src/main/java/dev/relism/flash/transport/ConnectionContext.java +++ b/flash/src/main/java/dev/relism/flash/transport/ConnectionContext.java @@ -26,7 +26,6 @@ import java.util.function.BooleanSupplier; * @param rawOut the unbuffered output stream — for WebSocket, whose writes are already * bulk (see {@code WebSocketSession}) * @param remoteAddress the client's address, or {@code null} if unavailable - * @param scratch this connection's reusable buffers ({@code EX-06}) * @param router the HTTP router * @param wsRouter the WebSocket router * @param configuration the server configuration (timeouts, limits, feature flags) diff --git a/flash/src/main/java/dev/relism/flash/transport/ConnectionProtocol.java b/flash/src/main/java/dev/relism/flash/transport/ConnectionProtocol.java index c4fe26c..1b6547d 100644 --- a/flash/src/main/java/dev/relism/flash/transport/ConnectionProtocol.java +++ b/flash/src/main/java/dev/relism/flash/transport/ConnectionProtocol.java @@ -3,7 +3,6 @@ package dev.relism.flash.transport; import java.io.IOException; /** - * The h1/h2 seam R1 requires: the protocol decision is made once, immediately after * ALPN/preface detection ({@link ConnectionRunner}), and dispatches to one implementation of * this interface. After that point neither implementation knows the other exists. */ diff --git a/flash/src/main/java/dev/relism/flash/transport/ConnectionRunner.java b/flash/src/main/java/dev/relism/flash/transport/ConnectionRunner.java index 056760e..e3ea388 100644 --- a/flash/src/main/java/dev/relism/flash/transport/ConnectionRunner.java +++ b/flash/src/main/java/dev/relism/flash/transport/ConnectionRunner.java @@ -21,14 +21,12 @@ import java.util.function.BooleanSupplier; /** * Owns one connection's socket lifecycle from accept to close: configures socket options, - * forces the TLS handshake if applicable ({@code EX-30}), negotiates the protocol, and * dispatches to the matching {@link ConnectionProtocol} — guaranteeing cleanup (scratch * release, active-socket tracking) regardless of how the protocol implementation exits. * *

    Sole responsibility: connection setup/teardown. It contains no HTTP semantics at all — * those live entirely inside whichever {@link ConnectionProtocol} it dispatches to (today, * always {@code Http1Connection}; an {@code H2} negotiation result is closed cleanly, since - * {@code Http2Connection} does not exist until Phase 8). */ @Slf4j public final class ConnectionRunner { @@ -77,7 +75,6 @@ public final class ConnectionRunner { SSLSocket sslSocket = socket instanceof SSLSocket ssl ? ssl : null; if (sslSocket != null) { - // EX-30: force the handshake explicitly, under a bounded timeout, before any // protocol decision — SSLSocket#getApplicationProtocol() (which // ProtocolNegotiator relies on) returns null until the handshake has run. socket.setSoTimeout(configuration.getHeaderReadTimeoutMs()); @@ -92,8 +89,7 @@ public final class ConnectionRunner { BufferedByteSource in = new BufferedByteSource(socket.getInputStream(), socket); NegotiatedProtocol negotiated = negotiateProtocol(socket, in); - if (negotiated == NegotiatedProtocol.H2) { - // No Http2Connection exists yet (lands in Phase 8) — close cleanly rather than + if (negotiated == NegotiatedProtocol.HTTP_2) { // attempt to speak a protocol this version cannot yet serve. return; } diff --git a/flash/src/main/java/dev/relism/flash/transport/ConnectionScratch.java b/flash/src/main/java/dev/relism/flash/transport/ConnectionScratch.java index 58b9ecf..5113d61 100644 --- a/flash/src/main/java/dev/relism/flash/transport/ConnectionScratch.java +++ b/flash/src/main/java/dev/relism/flash/transport/ConnectionScratch.java @@ -6,7 +6,6 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** - * The {@code EX-06} fix. Owns every per-connection reusable buffer that used to live in a * {@link ThreadLocal} on {@code HttpServer}: the decimal-formatting scratch, the streaming * relay buffer, and the WebSocket-handshake {@link MessageDigest}. * @@ -25,10 +24,7 @@ import java.security.NoSuchAlgorithmException; * pool when the connection closes. Never shared between two connections at once — there is no * synchronization here because none is needed. * - *

    {@code EX-06}'s router half (the {@code FastPathRouterImpl}/{@code FastPathWsRouterImpl} - * {@code ThreadLocal}s) is fixed in Phase 4, but deliberately not by extending this * class: {@code routing} has no dependency on {@code transport} today, and folding the router's - * scratch fields in here would have created one — see {@code DECISIONS.md}, {@code DEC-19}, for * the opaque-per-connection-object mechanism ({@code AbstractRouter#newScratch}) used instead. * This class gains HTTP/2 write/HPACK scratch in later phases, where {@code h2} already depends * on {@code transport} and no such boundary concern applies. @@ -45,7 +41,6 @@ public final class ConnectionScratch { public final byte[] relayBuffer = new byte[RELAY_BUFFER_SIZE]; /** - * {@code EX-27}: the scratch {@code Http1ResponseWriter} serializes a whole response head * (status line, {@code Content-Type}, {@code Date}, custom headers, {@code Content-Length}/ * {@code Connection}, and — for small fixed bodies — the body itself) into before issuing a * single bulk {@code write()}, instead of ~10 small {@code OutputStream.write} calls. diff --git a/flash/src/main/java/dev/relism/flash/transport/NegotiatedProtocol.java b/flash/src/main/java/dev/relism/flash/transport/NegotiatedProtocol.java index 7d477a4..00fd106 100644 --- a/flash/src/main/java/dev/relism/flash/transport/NegotiatedProtocol.java +++ b/flash/src/main/java/dev/relism/flash/transport/NegotiatedProtocol.java @@ -2,9 +2,8 @@ package dev.relism.flash.transport; /** * The result of {@link ProtocolNegotiator#negotiate}: which protocol a connection will speak, - * decided once, immediately after ALPN or the h2c preface is inspected, per R1. */ public enum NegotiatedProtocol { HTTP_1_1, - H2 + HTTP_2 } diff --git a/flash/src/main/java/dev/relism/flash/transport/ProtocolNegotiator.java b/flash/src/main/java/dev/relism/flash/transport/ProtocolNegotiator.java index ecc96f9..7286855 100644 --- a/flash/src/main/java/dev/relism/flash/transport/ProtocolNegotiator.java +++ b/flash/src/main/java/dev/relism/flash/transport/ProtocolNegotiator.java @@ -9,23 +9,21 @@ import java.util.Arrays; /** * Decides, once per connection and before any request is parsed, whether the connection speaks - * HTTP/1.1 or HTTP/2 — the single seam R1 requires ("the protocol decision is made once, * immediately after ALPN/preface detection"). * *

    Two independent signals, in order: *

      *
    1. ALPN (TLS connections). If the socket is an {@link SSLSocket} and the TLS * handshake already resolved {@code "h2"} as the application protocol, this connection is - * {@link NegotiatedProtocol#H2}. Anything else negotiated — {@code "http/1.1"}, no + * {@link NegotiatedProtocol#HTTP_2}. Anything else negotiated — {@code "http/1.1"}, no * protocol at all (a peer that doesn't speak ALPN), or an empty string — is * {@link NegotiatedProtocol#HTTP_1_1}. This costs nothing beyond a field read: ALPN is * resolved during the handshake, which must already have completed (see * {@code TlsConfig}'s Javadoc on why {@code startHandshake()} must be called explicitly - * before this method runs — {@code EX-30}).
    2. *
    3. h2c prior knowledge (plaintext connections, RFC 9113 §3.4). The first 24 bytes of * the connection are compared, without being consumed, against the client connection * preface {@code "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"}. A match is - * {@link NegotiatedProtocol#H2}; anything else — including a partial match followed by + * {@link NegotiatedProtocol#HTTP_2}; anything else — including a partial match followed by * EOF, or a preface look-alike that diverges partway through — is * {@link NegotiatedProtocol#HTTP_1_1}. This is why {@link BufferedByteSource#peek} exists: * the bytes must remain available for {@code RequestParser} if they turn out not to be an @@ -33,8 +31,7 @@ import java.util.Arrays; *
    * *

    This method reports the protocol accurately and unconditionally — it does not consult - * {@code FlashConfiguration.http2Enabled}. Gating whether an {@link NegotiatedProtocol#H2} - * result is honoured (versus cleanly rejected, which is all Phase 1 can do — there is no + * {@code FlashConfiguration.http2Enabled}. Gating whether an {@link NegotiatedProtocol#HTTP_2} * {@code Http2Connection} yet) and whether the h2c peek is even attempted for plaintext * connections are both the caller's responsibility, so that this class stays a pure, * directly-testable detector (see {@code ProtocolNegotiatorTest}). @@ -42,7 +39,6 @@ import java.util.Arrays; public final class ProtocolNegotiator { /** - * The HTTP/2 client connection preface (RFC 9113 §3.4) — precompiled once (R4), never * reconstructed per connection. */ private static final byte[] H2C_PREFACE = @@ -54,13 +50,13 @@ public final class ProtocolNegotiator { public static NegotiatedProtocol negotiate(Socket socket, BufferedByteSource source) throws IOException { if (socket instanceof SSLSocket ssl) { String applicationProtocol = ssl.getApplicationProtocol(); - return "h2".equals(applicationProtocol) ? NegotiatedProtocol.H2 : NegotiatedProtocol.HTTP_1_1; + return "h2".equals(applicationProtocol) ? NegotiatedProtocol.HTTP_2 : NegotiatedProtocol.HTTP_1_1; } byte[] probe = new byte[H2C_PREFACE.length]; int n = source.peek(probe, 0, probe.length); if (n == H2C_PREFACE.length && Arrays.equals(probe, H2C_PREFACE)) { - return NegotiatedProtocol.H2; + return NegotiatedProtocol.HTTP_2; } return NegotiatedProtocol.HTTP_1_1; } diff --git a/flash/src/main/java/dev/relism/flash/transport/ServerLifecycle.java b/flash/src/main/java/dev/relism/flash/transport/ServerLifecycle.java index 9235876..d0536dc 100644 --- a/flash/src/main/java/dev/relism/flash/transport/ServerLifecycle.java +++ b/flash/src/main/java/dev/relism/flash/transport/ServerLifecycle.java @@ -17,7 +17,6 @@ import java.util.concurrent.TimeUnit; /** * Owns the server's lifecycle: the accept threads (one per listener × * {@code TransportTuning.ACCEPT_THREADS}), the active-socket registry, and the two-stage - * graceful shutdown ({@code EX-32}) — stop accepting, let in-flight connections drain up to * {@code shutdownDrainTimeoutMs} (during which {@code Http1Connection} forces * {@code Connection: close} on the next response once it observes {@link #isStopped()}), then * force-close whatever remains. @@ -88,7 +87,6 @@ public final class ServerLifecycle implements ServerHandle { try { bl.socket().close(); } catch (IOException e) { log.error("Error closing server socket", e); } } - // EX-32: give in-flight connections a chance to finish their current response and // exit (Http1Connection forces Connection: close once it observes isStopped()) // before force-closing whatever is still open. long deadlineNanos = System.nanoTime() + configuration.getShutdownDrainTimeoutMs() * 1_000_000L; diff --git a/flash/src/main/java/dev/relism/flash/transport/TransportFactory.java b/flash/src/main/java/dev/relism/flash/transport/TransportFactory.java index eb2587b..14a9bcd 100644 --- a/flash/src/main/java/dev/relism/flash/transport/TransportFactory.java +++ b/flash/src/main/java/dev/relism/flash/transport/TransportFactory.java @@ -22,7 +22,6 @@ import java.util.concurrent.Executors; * and the h1 protocol, and returns the {@link ServerHandle} implementation * ({@link ServerLifecycle}) that {@link dev.relism.flash.ServerHandle#create} exposes publicly. * - *

    {@code EX-34}: this is the "composed transport rather than a god object" the registry * asked for — {@code ServerHandle.create} used to construct {@code HttpServer} directly, which * no longer exists. Package-private-in-spirit (public only because {@code ServerHandle} lives * in a different package and must call it) — user code has no reason to call this directly. diff --git a/flash/src/main/java/dev/relism/flash/websocket/WebSocketLoop.java b/flash/src/main/java/dev/relism/flash/websocket/WebSocketLoop.java index 56e8349..2c04c47 100644 --- a/flash/src/main/java/dev/relism/flash/websocket/WebSocketLoop.java +++ b/flash/src/main/java/dev/relism/flash/websocket/WebSocketLoop.java @@ -4,7 +4,6 @@ import java.io.IOException; /** * Drives one {@link WebSocketSession}'s read loop until the session closes, dispatching frames - * to the user's {@link WebSocketHandler}. Extracted from {@code HttpServer} (Phase 2) — its only * responsibility is this loop; the handshake and upgrade detection live in * {@link WebSocketUpgrade}. */ @@ -30,7 +29,6 @@ public final class WebSocketLoop { } } } catch (WebSocketProtocolException e) { - // EX-12: tell the peer why, with the correct close code, before tearing down. try { session.close(e.closeCode()); } catch (IOException ignored) { } handler.onError(session, e); } catch (IOException e) { diff --git a/flash/src/main/java/dev/relism/flash/websocket/WebSocketSession.java b/flash/src/main/java/dev/relism/flash/websocket/WebSocketSession.java index 1ca094f..dcf663d 100644 --- a/flash/src/main/java/dev/relism/flash/websocket/WebSocketSession.java +++ b/flash/src/main/java/dev/relism/flash/websocket/WebSocketSession.java @@ -36,7 +36,6 @@ import java.util.concurrent.locks.ReentrantLock; * *

    Thread safety

    * {@link #sendText}, {@link #send}, and {@link #close} are serialized on a - * {@link ReentrantLock} (never {@code synchronized} — see {@code EX-01}: a virtual thread * blocking inside {@code synchronized} pins its carrier platform thread on Java 21, and a * blocking socket write is exactly the kind of call that can block. {@link ReentrantLock} * unmounts the blocked virtual thread instead) and are safe to call from threads other than the @@ -352,8 +351,7 @@ public final class WebSocketSession { } } - /** Bulk-reads {@code len} bytes into {@link #hdrScratch} starting at offset 0 — the {@code - * EX-11} fix: the extended-length and mask-key bytes used to be read one at a time. */ + /** Bulk-reads {@code len} bytes into {@link #hdrScratch} starting at offset 0. */ private void readFullyHeader(int len) throws IOException { int remaining = len; while (remaining > 0) { diff --git a/flash/src/main/java/dev/relism/flash/websocket/WebSocketUpgrade.java b/flash/src/main/java/dev/relism/flash/websocket/WebSocketUpgrade.java index 6938c5b..beb38ac 100644 --- a/flash/src/main/java/dev/relism/flash/websocket/WebSocketUpgrade.java +++ b/flash/src/main/java/dev/relism/flash/websocket/WebSocketUpgrade.java @@ -13,7 +13,6 @@ import java.util.Base64; /** * WebSocket upgrade detection (RFC 6455 §4.2.1) and handshake response. Extracted from - * {@code HttpServer} (Phase 2) — its only responsibility is deciding whether a request is an * upgrade request and, if so, answering the {@code 101 Switching Protocols} handshake. The * session loop itself lives in {@link WebSocketLoop}. */ @@ -40,7 +39,6 @@ public final class WebSocketUpgrade { /** * Whether {@code request} is a WebSocket upgrade request: {@code Upgrade: websocket} and a - * {@code Connection} header whose token list includes {@code upgrade} ({@code EX-13} — the * shared token-list scanner in {@link Http1KeepAlive} is what fixed the whole-value compare * bug this check used to have too). */ diff --git a/flash/src/test/java/dev/relism/flash/ChunkedInputStreamTest.java b/flash/src/test/java/dev/relism/flash/ChunkedInputStreamTest.java index d906590..c2aa4f0 100644 --- a/flash/src/test/java/dev/relism/flash/ChunkedInputStreamTest.java +++ b/flash/src/test/java/dev/relism/flash/ChunkedInputStreamTest.java @@ -127,7 +127,6 @@ class ChunkedInputStreamTest { assertEquals("hi", new String(in.readAllBytes(), StandardCharsets.UTF_8)); } - // --- EX-10: no per-byte syscalls against the underlying stream ------------ /** Counts every {@code read} call that reaches the wrapped stream — i.e. every syscall. */ private static final class CountingInputStream extends ByteArrayInputStream { @@ -159,7 +158,6 @@ class ChunkedInputStreamTest { assertEquals(1, counting.reads); } - // --- EX-02/09 chunk safety limits ------------------------------------------ @Test void chunkSizeAboveLimit_rejected() { diff --git a/flash/src/test/java/dev/relism/flash/HttpServerTimeoutTest.java b/flash/src/test/java/dev/relism/flash/HttpServerTimeoutTest.java index f471736..a354a90 100644 --- a/flash/src/test/java/dev/relism/flash/HttpServerTimeoutTest.java +++ b/flash/src/test/java/dev/relism/flash/HttpServerTimeoutTest.java @@ -17,7 +17,6 @@ import java.nio.file.Path; import static org.junit.jupiter.api.Assertions.*; /** - * {@code EX-07}: a socket-level {@code SO_TIMEOUT} alone never trips against a peer that keeps * trickling bytes slower than the timeout window — each individual read still succeeds. These * tests prove the absolute deadline in {@code dev.relism.flash.transport.BufferedByteSource} * actually bounds the total time, not just each read. @@ -142,7 +141,6 @@ class HttpServerTimeoutTest { long start = System.nanoTime(); // A plain socket that never speaks TLS at all — the server's explicit - // startHandshake() (EX-30) blocks waiting for a ClientHello that is never coming, // and must be bounded by headerReadTimeoutMs rather than hanging forever. Whether the // JSSE implementation sends a TLS alert record before closing or just closes outright // is a JSSE implementation detail, not something this test should pin down — the diff --git a/flash/src/test/java/dev/relism/flash/RequestParserSecurityTest.java b/flash/src/test/java/dev/relism/flash/RequestParserSecurityTest.java index 045bac1..1a4dbe3 100644 --- a/flash/src/test/java/dev/relism/flash/RequestParserSecurityTest.java +++ b/flash/src/test/java/dev/relism/flash/RequestParserSecurityTest.java @@ -13,8 +13,6 @@ import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.*; /** - * One test per rejection rule added in HTTP/2 plan Phase 1 (EX-02, EX-03, EX-08, EX-18, EX-35, - * EX-36), each asserting the specific status code {@link MalformedRequestException} carries — * not merely that some exception was thrown. {@code Http1Connection}/{@code ConnectionRunner} always closes the connection * after any of these (never keep-alive); that behaviour is exercised at the integration level * by {@code HttpServerTest}. @@ -34,7 +32,6 @@ class RequestParserSecurityTest { return assertThrows(MalformedRequestException.class, () -> parse(raw)); } - // --- EX-02: Content-Length + Transfer-Encoding smuggling ------------------- @Test void contentLengthAndTransferEncodingBothPresent_rejected400() { @@ -79,7 +76,6 @@ class RequestParserSecurityTest { assertEquals(501, e.status()); } - // --- EX-03: strict Content-Length parsing ----------------------------------- @Test void contentLength_nonDigitSuffix_rejected400() { @@ -112,7 +108,6 @@ class RequestParserSecurityTest { assertEquals(413, expect("POST / HTTP/1.1\nHost: h\nContent-Length: " + tooLarge + "\n\n").status()); } - // --- EX-08: header/request-line limits -------------------------------------- @Test void tooManyHeaders_rejected431() { @@ -140,7 +135,6 @@ class RequestParserSecurityTest { assertEquals(431, expect("GET " + path + " HTTP/1.1\nHost: h\n\n").status()); } - // --- EX-18: bare CR / obs-fold ----------------------------------------------- @Test void bareLfInsteadOfCrlf_headerLine_rejected() { @@ -178,7 +172,6 @@ class RequestParserSecurityTest { assertThrows(MalformedRequestException.class, () -> new RequestParser().parse(source(raw))); } - // --- EX-36: header line missing ':' ------------------------------------------- @Test void headerLineMissingColon_rejected400() { diff --git a/flash/src/test/java/dev/relism/flash/RequestParserTest.java b/flash/src/test/java/dev/relism/flash/RequestParserTest.java index 0bbc424..89484df 100644 --- a/flash/src/test/java/dev/relism/flash/RequestParserTest.java +++ b/flash/src/test/java/dev/relism/flash/RequestParserTest.java @@ -56,7 +56,6 @@ class RequestParserTest { assertEquals("2", r.query("page")); } - // --- EX-42: pooled RequestByteViews (path/query/protocol) don't leak across requests --- @Test void samePooledParser_secondRequestWithoutQuery_doesNotLeakFirstRequestsQuery() throws IOException { @@ -135,8 +134,6 @@ class RequestParserTest { @Test void missingHeaderTerminator_throwsMalformedRequestException() { // Valid request line but stream ends before \r\n\r\n. Previously a generic IOException; - // now the same typed rejection EX-08's over-limit case uses, since both mean "the - // header block could never be completed within the allowed buffer" (EX-08). byte[] raw = "GET / HTTP/1.1\r\nHost: localhost\r\n".getBytes(StandardCharsets.UTF_8); assertThrows(MalformedRequestException.class, () -> new RequestParser().parse(source(raw))); } @@ -159,7 +156,6 @@ class RequestParserTest { @Test void headers_exceedingMaxBufferSize_throwsMalformedRequestException() { - // Feed more bytes than the configured cap with no \r\n\r\n : must throw 431 (EX-08). int cap = 16 * 1024; byte[] giant = new byte[cap + 1]; Arrays.fill(giant, (byte) 'A'); @@ -183,7 +179,6 @@ class RequestParserTest { @Test void transferEncoding_multiValueEndingInChunked_recognised() throws IOException { - // EX-35: "gzip, chunked" — chunked need only be the FINAL coding (RFC 9112 §6.1). The // old whole-value comparison misclassified this as not chunked at all. String raw = "POST / HTTP/1.1\r\n" + "Host: localhost\r\n" + diff --git a/flash/src/test/java/dev/relism/flash/api/multipart/MultipartTest.java b/flash/src/test/java/dev/relism/flash/api/multipart/MultipartTest.java index 7ed3cbd..7e77496 100644 --- a/flash/src/test/java/dev/relism/flash/api/multipart/MultipartTest.java +++ b/flash/src/test/java/dev/relism/flash/api/multipart/MultipartTest.java @@ -245,7 +245,6 @@ class MultipartTest { } // ------------------------------------------------------------------------- - // EX-29: resource-exhaustion bounds // ------------------------------------------------------------------------- @Test diff --git a/flash/src/test/java/dev/relism/flash/architecture/PackageBoundaryTest.java b/flash/src/test/java/dev/relism/flash/architecture/PackageBoundaryTest.java index 665e94b..d963914 100644 --- a/flash/src/test/java/dev/relism/flash/architecture/PackageBoundaryTest.java +++ b/flash/src/test/java/dev/relism/flash/architecture/PackageBoundaryTest.java @@ -10,31 +10,21 @@ import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.fail; -/** - * {@code R1}/{@code DEC-02}: HTTP/1.1 and HTTP/2 are peers behind the {@code ConnectionProtocol} - * seam, never coupled to each other directly. A lightweight source-scan rather than ArchUnit — - * this project has no bytecode-analysis test dependency yet, and one import-statement check per - * package pair does not need one; record the choice here rather than in {@code DECISIONS.md} - * since it is this test's own implementation detail, not a design decision affecting shipped - * code. - */ +/** Ensures that the HTTP/1.1 and HTTP/2 implementations remain independent peers. */ class PackageBoundaryTest { @Test - void http1DoesNotImportH2() throws IOException { - assertNoImportOfPackage("dev/relism/flash/http1", "dev.relism.flash.h2"); + void http1DoesNotImportHttp2() throws IOException { + assertNoImportOfPackage("dev/relism/flash/http1", "dev.relism.flash.http2"); } @Test - void h2DoesNotImportHttp1() throws IOException { - assertNoImportOfPackage("dev/relism/flash/h2", "dev.relism.flash.http1"); + void http2DoesNotImportHttp1() throws IOException { + assertNoImportOfPackage("dev/relism/flash/http2", "dev.relism.flash.http1"); } private static void assertNoImportOfPackage(String sourceDirRelative, String forbiddenImportPrefix) throws IOException { Path root = findSourceRoot(sourceDirRelative); - // Neither package boundary can be meaningfully checked before both packages exist; once - // dev.relism.flash.h2 gains real classes (Phase 3+) this stops being a no-op for the - // h2-side test. if (root == null) return; try (Stream files = Files.walk(root)) { @@ -45,7 +35,7 @@ class PackageBoundaryTest { if (trimmed.startsWith("import " + forbiddenImportPrefix + ".") || trimmed.startsWith("import " + forbiddenImportPrefix + ";")) { fail(file + " imports " + forbiddenImportPrefix - + " — violates the h1/h2 package boundary (R1/DEC-02): " + trimmed); + + " and violates the HTTP protocol package boundary: " + trimmed); } } } diff --git a/flash/src/test/java/dev/relism/flash/bytes/ByteScanFuzzTest.java b/flash/src/test/java/dev/relism/flash/bytes/ByteScanFuzzTest.java index b28626b..ff4b866 100644 --- a/flash/src/test/java/dev/relism/flash/bytes/ByteScanFuzzTest.java +++ b/flash/src/test/java/dev/relism/flash/bytes/ByteScanFuzzTest.java @@ -9,7 +9,6 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; /** * Randomized agreement testing for {@link ByteScan}'s SWAR methods against their scalar - * counterparts, per Phase 4's task 1 ("property-test SWAR against scalar on random inputs of * every length 0..256 ... including unaligned starts"). {@link ByteScanTest} already covers * every exact boundary deterministically; this class instead throws a large volume of fully * random bytes and random sub-ranges at both implementations, on a fixed seed for reproducible diff --git a/flash/src/test/java/dev/relism/flash/http/HttpStatusTest.java b/flash/src/test/java/dev/relism/flash/http/HttpStatusTest.java index efe9bdf..87a61b1 100644 --- a/flash/src/test/java/dev/relism/flash/http/HttpStatusTest.java +++ b/flash/src/test/java/dev/relism/flash/http/HttpStatusTest.java @@ -41,7 +41,6 @@ class HttpStatusTest { assertNull(HttpStatus.reasonForCode(0)); } - // --- EX-17: bound computed from values(), not a hand-maintained constant ----- @Test void statusesAboveThePreviousHandMaintainedBound_workCorrectly() { diff --git a/flash/src/test/java/dev/relism/flash/http1/Http1ResponseWriterTest.java b/flash/src/test/java/dev/relism/flash/http1/Http1ResponseWriterTest.java index 19e09cc..be0836d 100644 --- a/flash/src/test/java/dev/relism/flash/http1/Http1ResponseWriterTest.java +++ b/flash/src/test/java/dev/relism/flash/http1/Http1ResponseWriterTest.java @@ -26,7 +26,6 @@ class Http1ResponseWriterTest { return out.toString(StandardCharsets.UTF_8); } - // --- EX-14: HEAD ------------------------------------------------------------ @Test void head_reportsContentLengthButWritesNoBody() throws IOException { @@ -46,7 +45,6 @@ class Http1ResponseWriterTest { assertTrue(raw.endsWith("hello world"), raw); } - // --- EX-15: 204 / 304 / 1xx never carry Content-Length or a body ------------ @Test void status204_omitsContentLengthAndBody() throws IOException { @@ -85,7 +83,6 @@ class Http1ResponseWriterTest { assertTrue(raw.contains("Content-Length: 1\r\n"), raw); } - // --- EX-15: ContentType.NONE omits the Content-Type line entirely ----------- @Test void contentTypeNone_omitsContentTypeLine() throws IOException { @@ -101,7 +98,6 @@ class Http1ResponseWriterTest { assertTrue(raw.contains("Content-Type: text/plain\r\n"), raw); } - // --- EX-16: Date header ------------------------------------------------------- @Test void sendDateTrue_includesDateHeader() throws IOException { @@ -135,7 +131,6 @@ class Http1ResponseWriterTest { assertTrue(raw.contains("Connection: close\r\n"), raw); } - // --- EX-27: one bulk write for a small fixed body ----------------------------- /** Counts calls to {@code write(byte[], int, int)} — the only overload {@link Http1ResponseWriter} uses. */ private static final class CountingOutputStream extends java.io.OutputStream { diff --git a/flash/src/test/java/dev/relism/flash/h2/Http2ErrorCodeTest.java b/flash/src/test/java/dev/relism/flash/http2/Http2ErrorCodeTest.java similarity index 95% rename from flash/src/test/java/dev/relism/flash/h2/Http2ErrorCodeTest.java rename to flash/src/test/java/dev/relism/flash/http2/Http2ErrorCodeTest.java index d2e40e2..cad8af3 100644 --- a/flash/src/test/java/dev/relism/flash/h2/Http2ErrorCodeTest.java +++ b/flash/src/test/java/dev/relism/flash/http2/Http2ErrorCodeTest.java @@ -1,4 +1,4 @@ -package dev.relism.flash.h2; +package dev.relism.flash.http2; import org.junit.jupiter.api.Test; @@ -53,7 +53,6 @@ class Http2ErrorCodeTest { @Test void bytesInstanceIsStablePerConstant() { - // Precomputed at class init (R4) — must not be rebuilt per call. assertSame(Http2ErrorCode.PROTOCOL_ERROR.bytes(), Http2ErrorCode.PROTOCOL_ERROR.bytes()); } } diff --git a/flash/src/test/java/dev/relism/flash/h2/Http2ExceptionTest.java b/flash/src/test/java/dev/relism/flash/http2/Http2ExceptionTest.java similarity index 97% rename from flash/src/test/java/dev/relism/flash/h2/Http2ExceptionTest.java rename to flash/src/test/java/dev/relism/flash/http2/Http2ExceptionTest.java index 3457f89..ef8c06f 100644 --- a/flash/src/test/java/dev/relism/flash/h2/Http2ExceptionTest.java +++ b/flash/src/test/java/dev/relism/flash/http2/Http2ExceptionTest.java @@ -1,4 +1,4 @@ -package dev.relism.flash.h2; +package dev.relism.flash.http2; import org.junit.jupiter.api.Test; diff --git a/flash/src/test/java/dev/relism/flash/h2/Http2LimitsTest.java b/flash/src/test/java/dev/relism/flash/http2/Http2LimitsTest.java similarity index 98% rename from flash/src/test/java/dev/relism/flash/h2/Http2LimitsTest.java rename to flash/src/test/java/dev/relism/flash/http2/Http2LimitsTest.java index 2faab33..837ad23 100644 --- a/flash/src/test/java/dev/relism/flash/h2/Http2LimitsTest.java +++ b/flash/src/test/java/dev/relism/flash/http2/Http2LimitsTest.java @@ -1,4 +1,4 @@ -package dev.relism.flash.h2; +package dev.relism.flash.http2; import org.junit.jupiter.api.Test; diff --git a/flash/src/test/java/dev/relism/flash/h2/Http2StreamExceptionTest.java b/flash/src/test/java/dev/relism/flash/http2/Http2StreamExceptionTest.java similarity index 96% rename from flash/src/test/java/dev/relism/flash/h2/Http2StreamExceptionTest.java rename to flash/src/test/java/dev/relism/flash/http2/Http2StreamExceptionTest.java index b541226..741399c 100644 --- a/flash/src/test/java/dev/relism/flash/h2/Http2StreamExceptionTest.java +++ b/flash/src/test/java/dev/relism/flash/http2/Http2StreamExceptionTest.java @@ -1,4 +1,4 @@ -package dev.relism.flash.h2; +package dev.relism.flash.http2; import org.junit.jupiter.api.Test; diff --git a/flash/src/test/java/dev/relism/flash/h2/frame/FrameValidatorTest.java b/flash/src/test/java/dev/relism/flash/http2/frame/FrameValidatorTest.java similarity index 96% rename from flash/src/test/java/dev/relism/flash/h2/frame/FrameValidatorTest.java rename to flash/src/test/java/dev/relism/flash/http2/frame/FrameValidatorTest.java index 5c88fdc..68a67ad 100644 --- a/flash/src/test/java/dev/relism/flash/h2/frame/FrameValidatorTest.java +++ b/flash/src/test/java/dev/relism/flash/http2/frame/FrameValidatorTest.java @@ -1,7 +1,7 @@ -package dev.relism.flash.h2.frame; +package dev.relism.flash.http2.frame; -import dev.relism.flash.h2.Http2ErrorCode; -import dev.relism.flash.h2.Http2Exception; +import dev.relism.flash.http2.Http2ErrorCode; +import dev.relism.flash.http2.Http2Exception; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -175,7 +175,7 @@ class FrameValidatorTest { @Test void declaredLengthAboveMaxFrameSize_isFrameSizeError() { - FrameHeader h = headerOf(dev.relism.flash.h2.Http2Limits.MAX_FRAME_SIZE_LOCAL + 1, FrameType.DATA, 0, 1); + FrameHeader h = headerOf(dev.relism.flash.http2.Http2Limits.MAX_FRAME_SIZE_LOCAL + 1, FrameType.DATA, 0, 1); assertEquals(Http2ErrorCode.FRAME_SIZE_ERROR, codeOf(h, false)); } } diff --git a/flash/src/test/java/dev/relism/flash/h2/frame/Http2FrameReaderFuzzTest.java b/flash/src/test/java/dev/relism/flash/http2/frame/Http2FrameReaderFuzzTest.java similarity index 93% rename from flash/src/test/java/dev/relism/flash/h2/frame/Http2FrameReaderFuzzTest.java rename to flash/src/test/java/dev/relism/flash/http2/frame/Http2FrameReaderFuzzTest.java index 7b36774..e17c2f6 100644 --- a/flash/src/test/java/dev/relism/flash/h2/frame/Http2FrameReaderFuzzTest.java +++ b/flash/src/test/java/dev/relism/flash/http2/frame/Http2FrameReaderFuzzTest.java @@ -1,6 +1,6 @@ -package dev.relism.flash.h2.frame; +package dev.relism.flash.http2.frame; -import dev.relism.flash.h2.Http2Exception; +import dev.relism.flash.http2.Http2Exception; import dev.relism.flash.transport.BufferedByteSource; import org.junit.jupiter.api.Test; @@ -13,7 +13,6 @@ import java.util.Random; import static org.junit.jupiter.api.Assertions.fail; /** - * Phase 5's DoD: "Fuzz test green for 10 million random inputs." Throws fully random bytes at * {@link Http2FrameReader} and asserts that only a typed, expected outcome ever results: a * {@link Http2Exception} (a declared length exceeding {@code MAX_FRAME_SIZE_LOCAL} — the * overwhelmingly common outcome, since a random 24-bit length is astronomically likely to diff --git a/flash/src/test/java/dev/relism/flash/h2/frame/Http2FrameReaderTest.java b/flash/src/test/java/dev/relism/flash/http2/frame/Http2FrameReaderTest.java similarity index 96% rename from flash/src/test/java/dev/relism/flash/h2/frame/Http2FrameReaderTest.java rename to flash/src/test/java/dev/relism/flash/http2/frame/Http2FrameReaderTest.java index c15855e..b8df2a6 100644 --- a/flash/src/test/java/dev/relism/flash/h2/frame/Http2FrameReaderTest.java +++ b/flash/src/test/java/dev/relism/flash/http2/frame/Http2FrameReaderTest.java @@ -1,7 +1,7 @@ -package dev.relism.flash.h2.frame; +package dev.relism.flash.http2.frame; import dev.relism.flash.bytes.ByteWriter; -import dev.relism.flash.h2.Http2Exception; +import dev.relism.flash.http2.Http2Exception; import dev.relism.flash.transport.BufferedByteSource; import org.junit.jupiter.api.Test; @@ -70,9 +70,9 @@ class Http2FrameReaderTest { byte[] payload = new byte[len]; byte[] wire = buildFrame(FrameType.DATA, 0, 1, payload); Http2FrameReader reader = new Http2FrameReader(sourceOf(wire)); - if (len > dev.relism.flash.h2.Http2Limits.MAX_FRAME_SIZE_LOCAL) { + if (len > dev.relism.flash.http2.Http2Limits.MAX_FRAME_SIZE_LOCAL) { Http2Exception ex = assertThrows(Http2Exception.class, reader::readFrame); - assertEquals(dev.relism.flash.h2.Http2ErrorCode.FRAME_SIZE_ERROR, ex.errorCode()); + assertEquals(dev.relism.flash.http2.Http2ErrorCode.FRAME_SIZE_ERROR, ex.errorCode()); } else { FrameHeader header = reader.readFrame(); assertNotNull(header); @@ -151,7 +151,7 @@ class Http2FrameReaderTest { out.beginFrame(FrameType.PING, 0, 0); out.writer().writeBytes(new byte[]{1, 2, 3, 4, 5, 6, 7, 8}); out.endFrame(); - out.beginFrame(FrameType.PING, dev.relism.flash.h2.frame.FrameFlags.ACK, 0); + out.beginFrame(FrameType.PING, dev.relism.flash.http2.frame.FrameFlags.ACK, 0); out.writer().writeBytes(new byte[]{8, 7, 6, 5, 4, 3, 2, 1}); out.endFrame(); byte[] wire = new byte[w.length()]; diff --git a/flash/src/test/java/dev/relism/flash/h2/frame/Http2FrameWriterStressTest.java b/flash/src/test/java/dev/relism/flash/http2/frame/Http2FrameWriterStressTest.java similarity index 98% rename from flash/src/test/java/dev/relism/flash/h2/frame/Http2FrameWriterStressTest.java rename to flash/src/test/java/dev/relism/flash/http2/frame/Http2FrameWriterStressTest.java index f738b78..11f209d 100644 --- a/flash/src/test/java/dev/relism/flash/h2/frame/Http2FrameWriterStressTest.java +++ b/flash/src/test/java/dev/relism/flash/http2/frame/Http2FrameWriterStressTest.java @@ -1,4 +1,4 @@ -package dev.relism.flash.h2.frame; +package dev.relism.flash.http2.frame; import org.junit.jupiter.api.Test; @@ -26,7 +26,6 @@ import static org.junit.jupiter.api.Assertions.*; * full gate verification (1000 iterations per N, plus a * {@code -Djdk.virtualThreadScheduler.parallelism=1} run to surface pinning/lost-wakeup bugs * that only appear at parallelism 1) was run manually and is recorded, with its numbers, in - * {@code flash/docs/http2/WRITER.md} and {@code DECISIONS.md} (`DEC-09`). */ class Http2FrameWriterStressTest { diff --git a/flash/src/test/java/dev/relism/flash/h2/frame/Http2FrameWriterTest.java b/flash/src/test/java/dev/relism/flash/http2/frame/Http2FrameWriterTest.java similarity index 98% rename from flash/src/test/java/dev/relism/flash/h2/frame/Http2FrameWriterTest.java rename to flash/src/test/java/dev/relism/flash/http2/frame/Http2FrameWriterTest.java index 1c17a24..23a01fc 100644 --- a/flash/src/test/java/dev/relism/flash/h2/frame/Http2FrameWriterTest.java +++ b/flash/src/test/java/dev/relism/flash/http2/frame/Http2FrameWriterTest.java @@ -1,4 +1,4 @@ -package dev.relism.flash.h2.frame; +package dev.relism.flash.http2.frame; import org.junit.jupiter.api.Test; diff --git a/flash/src/test/java/dev/relism/flash/h2/frame/PaddingTest.java b/flash/src/test/java/dev/relism/flash/http2/frame/PaddingTest.java similarity index 95% rename from flash/src/test/java/dev/relism/flash/h2/frame/PaddingTest.java rename to flash/src/test/java/dev/relism/flash/http2/frame/PaddingTest.java index 0b63852..f810ce0 100644 --- a/flash/src/test/java/dev/relism/flash/h2/frame/PaddingTest.java +++ b/flash/src/test/java/dev/relism/flash/http2/frame/PaddingTest.java @@ -1,7 +1,7 @@ -package dev.relism.flash.h2.frame; +package dev.relism.flash.http2.frame; -import dev.relism.flash.h2.Http2ErrorCode; -import dev.relism.flash.h2.Http2Exception; +import dev.relism.flash.http2.Http2ErrorCode; +import dev.relism.flash.http2.Http2Exception; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; diff --git a/flash/src/test/java/dev/relism/flash/models/Http1HeaderMapIndexTest.java b/flash/src/test/java/dev/relism/flash/models/Http1HeaderMapIndexTest.java index cc5d0c7..bb47ea9 100644 --- a/flash/src/test/java/dev/relism/flash/models/Http1HeaderMapIndexTest.java +++ b/flash/src/test/java/dev/relism/flash/models/Http1HeaderMapIndexTest.java @@ -8,7 +8,6 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.*; /** - * {@code EX-09}: dedicated correctness coverage for {@link Http1HeaderMap}'s per-{@code reset()} * index — duplicate names, case variation, zero headers, and growth past the initial index * capacity up to {@code Http1Limits.MAX_HEADER_COUNT}. {@link HeaderMapTest} already covers the * ordinary lookup/forEach contract; this class targets the index machinery specifically. @@ -104,7 +103,6 @@ class Http1HeaderMapIndexTest { @Test void allocation_indexArraysAreNotReallocatedOnceWarm() { - // The rigorous 0 B/op verification is the Phase 17 JMH gate (-prof gc); this is a // unit-test-level structural guarantee that repeated first()/all()/view() lookups never // re-trigger index growth (Arrays.copyOf inside ensureIndexCapacity) after the first // reset() has already sized the arrays for this header count — asserted by identity: the @@ -124,7 +122,6 @@ class Http1HeaderMapIndexTest { @Test void view_poolWraparound_aliasesAnEarlierReturnedView() { - // EX-05's documented hazard, demonstrated through the actual public API: Http1HeaderMap's // view() pool is sized 4 (VIEW_POOL_SIZE); a 5th call in the same request wraps around // and silently repositions the object the 1st call returned. dev.relism.fpr.core.ByteView v1 = null; diff --git a/flash/src/test/java/dev/relism/flash/models/PathParamsTest.java b/flash/src/test/java/dev/relism/flash/models/PathParamsTest.java index 913f065..09f1386 100644 --- a/flash/src/test/java/dev/relism/flash/models/PathParamsTest.java +++ b/flash/src/test/java/dev/relism/flash/models/PathParamsTest.java @@ -68,7 +68,6 @@ class PathParamsTest { @Test void view_poolWraparound_aliasesAnEarlierReturnedView() { - // EX-05's pooled path only engages when `source` is array-backed (ArrayBackedByteView) — // unlike of()'s plain inline ByteView (which exercises the non-pooled fallback, still // correct but not the code path this test targets), use the same view type RequestParser // actually produces. diff --git a/flash/src/test/java/dev/relism/flash/models/QueryParamsFastPathTest.java b/flash/src/test/java/dev/relism/flash/models/QueryParamsFastPathTest.java index 60aa45b..13f2cc1 100644 --- a/flash/src/test/java/dev/relism/flash/models/QueryParamsFastPathTest.java +++ b/flash/src/test/java/dev/relism/flash/models/QueryParamsFastPathTest.java @@ -9,10 +9,8 @@ import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.*; /** - * {@code EX-26}: the clean-value (no {@code %}/{@code +}) fast path in {@code QueryParams.decode} * must produce byte-for-byte identical results to the percent-decoding slow path it bypasses — * verified here across clean values, values needing every kind of decoding, and the boundary - * between them. Also covers {@code EX-05}'s pooled {@code view()}. */ class QueryParamsFastPathTest { @@ -63,7 +61,6 @@ class QueryParamsFastPathTest { assertEquals("a b", qp.get("plussed")); } - // ── EX-05: pooled view() ──────────────────────────────────────────────── @Test void view_returnsRawUndecodedBytes() { diff --git a/flash/src/test/java/dev/relism/flash/models/RequestBodyTest.java b/flash/src/test/java/dev/relism/flash/models/RequestBodyTest.java index e3b4b4e..afd1f3b 100644 --- a/flash/src/test/java/dev/relism/flash/models/RequestBodyTest.java +++ b/flash/src/test/java/dev/relism/flash/models/RequestBodyTest.java @@ -162,7 +162,6 @@ class RequestBodyTest { assertEquals(0, socket.available()); } - // --- EX-22/EX-23: pooled instance, repositioned via reset() -------------------- @Test void reset_repositionsSamePooledInstance_overSuccessiveRequests() throws IOException { @@ -187,7 +186,6 @@ class RequestBodyTest { body.reset(new ByteArrayInputStream("two".getBytes(StandardCharsets.UTF_8)), 3, new byte[0], 0, 0); InputStream stream2 = body.stream(); - assertSame(stream1, stream2, "EX-23: stream() must reposition the one pooled BoundedBufferedInputStream, not allocate a new one per request"); assertEquals("two", new String(stream2.readAllBytes(), StandardCharsets.UTF_8)); } diff --git a/flash/src/test/java/dev/relism/flash/models/RequestPoolingTest.java b/flash/src/test/java/dev/relism/flash/models/RequestPoolingTest.java index 9634dd7..d2db875 100644 --- a/flash/src/test/java/dev/relism/flash/models/RequestPoolingTest.java +++ b/flash/src/test/java/dev/relism/flash/models/RequestPoolingTest.java @@ -9,13 +9,11 @@ import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.*; /** - * {@code EX-22}: {@link Request} is pooled per connection (one instance owned by * {@code RequestParser}, repositioned via {@link Request#forParsed} for every request on that * connection) — not via a shared cross-connection pool. The plan's own safety-check wording * ("connection A's {@code Authorization} header must never be visible on connection B") describes * a threat model that does not structurally apply to this design: two different connections * never share a {@code Request} instance at all (each owns its own {@code RequestParser}, hence - * its own {@code Request}) — see {@code DECISIONS.md} for the pooling-granularity decision this * follows from. The real, applicable threat this class actually tests: request N+1 on * the *same* keep-alive connection must never see stale data left over from request N, * since those two requests genuinely do share one {@code Request} instance. diff --git a/flash/src/test/java/dev/relism/flash/models/RequestRecycleGuardTest.java b/flash/src/test/java/dev/relism/flash/models/RequestRecycleGuardTest.java index b43533c..7e77181 100644 --- a/flash/src/test/java/dev/relism/flash/models/RequestRecycleGuardTest.java +++ b/flash/src/test/java/dev/relism/flash/models/RequestRecycleGuardTest.java @@ -10,7 +10,6 @@ import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.*; /** - * {@code EX-22}'s dev-mode use-after-recycle guard. Exercises the poisoning check directly via * {@code Request.setPoisoningEnabledForTesting} rather than the real {@code Flash.DEV} flag, * which is a {@code static final boolean} fixed once at JVM startup and cannot be toggled by an * individual test — see that field's own comment in {@code Request.java}. diff --git a/flash/src/test/java/dev/relism/flash/models/ResponsePoolingTest.java b/flash/src/test/java/dev/relism/flash/models/ResponsePoolingTest.java index c835788..1be6746 100644 --- a/flash/src/test/java/dev/relism/flash/models/ResponsePoolingTest.java +++ b/flash/src/test/java/dev/relism/flash/models/ResponsePoolingTest.java @@ -5,7 +5,6 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; -/** {@code EX-21}: mirrors {@code RequestPoolingTest} for {@link Response}. */ class ResponsePoolingTest { @Test diff --git a/flash/src/test/java/dev/relism/flash/models/ResponseRecycleGuardTest.java b/flash/src/test/java/dev/relism/flash/models/ResponseRecycleGuardTest.java index 48fbae4..ffa3e9d 100644 --- a/flash/src/test/java/dev/relism/flash/models/ResponseRecycleGuardTest.java +++ b/flash/src/test/java/dev/relism/flash/models/ResponseRecycleGuardTest.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; -/** {@code EX-21}'s dev-mode use-after-recycle guard — mirrors {@code RequestRecycleGuardTest}. */ class ResponseRecycleGuardTest { @AfterEach diff --git a/flash/src/test/java/dev/relism/flash/models/ResponseTest.java b/flash/src/test/java/dev/relism/flash/models/ResponseTest.java index 8278d5b..61218ed 100644 --- a/flash/src/test/java/dev/relism/flash/models/ResponseTest.java +++ b/flash/src/test/java/dev/relism/flash/models/ResponseTest.java @@ -135,7 +135,6 @@ class ResponseTest { assertTrue(new Response(200, new byte[0], ContentType.TEXT_PLAIN).getHeaders().isEmpty()); } - // --- EX-43: response header budget (Phase 6 zero-alloc DoD) --- @Test void header_exceedingMaxCount_throws() { diff --git a/flash/src/test/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathRouterImplTest.java b/flash/src/test/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathRouterImplTest.java index 3edd7f2..450d25d 100644 --- a/flash/src/test/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathRouterImplTest.java +++ b/flash/src/test/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathRouterImplTest.java @@ -72,7 +72,6 @@ class FastPathRouterImplTest { @Test void route_reusesScratchAcrossManyRequests_includingGrowingParamCapacity() throws Exception { - // EX-19: the same scratch, reused across a mix of param counts, must keep matching // correctly as its arrays grow past their initial size (8) and get reused afterward. FastPathRouterImpl router = new FastPathRouterImpl(); router.doRegister(HttpMethod.GET, "/a/{p1}/{p2}/{p3}/{p4}/{p5}/{p6}/{p7}/{p8}/{p9}/{p10}", diff --git a/flash/src/test/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathViewsLongAtTest.java b/flash/src/test/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathViewsLongAtTest.java index 47cbec9..469960a 100644 --- a/flash/src/test/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathViewsLongAtTest.java +++ b/flash/src/test/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathViewsLongAtTest.java @@ -12,12 +12,10 @@ import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.*; /** - * {@code EX-04}: verifies the {@code longAt()}/{@code supportsLong()} contract against * {@code fpr-core}'s own word-at-a-time comparison code — not merely against a hand-derived * expectation, per the plan's explicit instruction to verify by testing against {@code fpr-core} * directly rather than by reading its bytecode (bytecode-reading only informed which byte order * to use; this test is the actual verification). A wrong endianness or a wrong bounds assumption - * here produces silently mis-routed requests, the worst possible failure mode ({@code EX-04}'s * own registry entry) — so this covers both the raw word-read contract and an end-to-end router * match with the long path actually engaged. */ diff --git a/flash/src/test/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathViewsTest.java b/flash/src/test/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathViewsTest.java index 3d9c380..e5256be 100644 --- a/flash/src/test/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathViewsTest.java +++ b/flash/src/test/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathViewsTest.java @@ -28,7 +28,6 @@ class FastPathViewsTest { assertThrows(IndexOutOfBoundsException.class, () -> view.byteAt(10)); } - // --- EX-42: reset() repositions the same instance, zero allocation --------- @Test void requestByteView_reset_repositionsSameInstance() { diff --git a/flash/src/test/java/dev/relism/flash/template/ByteTemplateTest.java b/flash/src/test/java/dev/relism/flash/template/ByteTemplateTest.java index 3b7ee7d..62e16e4 100644 --- a/flash/src/test/java/dev/relism/flash/template/ByteTemplateTest.java +++ b/flash/src/test/java/dev/relism/flash/template/ByteTemplateTest.java @@ -56,7 +56,6 @@ class ByteTemplateTest { assertEquals("A12B", new String(result, StandardCharsets.UTF_8)); } - // --- EX-28: renderInto(buffer, offset, ...) ------------------------------------ @Test void renderInto_writesAtOffset_andReturnsLength() { diff --git a/flash/src/test/java/dev/relism/flash/tls/TlsConfigTest.java b/flash/src/test/java/dev/relism/flash/tls/TlsConfigTest.java index d5b5eec..bb4ecb8 100644 --- a/flash/src/test/java/dev/relism/flash/tls/TlsConfigTest.java +++ b/flash/src/test/java/dev/relism/flash/tls/TlsConfigTest.java @@ -121,7 +121,6 @@ class TlsConfigTest { } } - // --- EX-31: cipher suite filtering when h2 is offered ----------------------- @Test void negotiatesH2_trueOnlyWhenH2IsInTheOfferedList() throws Exception { diff --git a/flash/src/test/java/dev/relism/flash/transport/BufferedByteSourceTest.java b/flash/src/test/java/dev/relism/flash/transport/BufferedByteSourceTest.java index 33dbb57..b546e3c 100644 --- a/flash/src/test/java/dev/relism/flash/transport/BufferedByteSourceTest.java +++ b/flash/src/test/java/dev/relism/flash/transport/BufferedByteSourceTest.java @@ -10,11 +10,8 @@ import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.*; /** - * {@code EX-37}: this class previously had zero dedicated tests — its deadline mechanism (the - * actual {@code EX-07} slowloris fix) was exercised only indirectly through real-socket, * end-to-end tests, which never hit the {@code null}-socket path every isolated unit test in * this codebase actually uses. Found and fixed while building {@code Http2FrameReaderTest} - * (Phase 5); this class closes the gap. */ class BufferedByteSourceTest { @@ -100,7 +97,6 @@ class BufferedByteSourceTest { assertThrows(IllegalStateException.class, () -> src.prependOnce(a, 0, 1)); } - // ── Deadline mechanism, EX-37's actual regression coverage ────────────── @Test void clearDeadline_withNullSocket_doesNotThrow() throws IOException { diff --git a/flash/src/test/java/dev/relism/flash/transport/ConnectionRunnerTest.java b/flash/src/test/java/dev/relism/flash/transport/ConnectionRunnerTest.java index bdb0f4d..373b4fe 100644 --- a/flash/src/test/java/dev/relism/flash/transport/ConnectionRunnerTest.java +++ b/flash/src/test/java/dev/relism/flash/transport/ConnectionRunnerTest.java @@ -22,7 +22,6 @@ import static org.junit.jupiter.api.Assertions.*; /** * A scratch is always released — including on an exception path — and a socket is always * removed from {@code activeSockets}, regardless of how the dispatched - * {@link ConnectionProtocol} exits. This is a resource-leak safety property (Phase 2's Safety * checks list), verified here with a protocol implementation that deliberately throws. */ class ConnectionRunnerTest { diff --git a/flash/src/test/java/dev/relism/flash/transport/ProtocolNegotiatorTest.java b/flash/src/test/java/dev/relism/flash/transport/ProtocolNegotiatorTest.java index 404834f..0789967 100644 --- a/flash/src/test/java/dev/relism/flash/transport/ProtocolNegotiatorTest.java +++ b/flash/src/test/java/dev/relism/flash/transport/ProtocolNegotiatorTest.java @@ -36,13 +36,13 @@ class ProtocolNegotiatorTest { @Test void h2cPrefaceExact_negotiatesH2() throws IOException { BufferedByteSource src = plaintextSource("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"); - assertEquals(NegotiatedProtocol.H2, ProtocolNegotiator.negotiate(new Socket(), src)); + assertEquals(NegotiatedProtocol.HTTP_2, ProtocolNegotiator.negotiate(new Socket(), src)); } @Test void h2cPrefaceFollowedByMoreData_stillNegotiatesH2_andDoesNotConsume() throws IOException { BufferedByteSource src = plaintextSource("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\nEXTRA"); - assertEquals(NegotiatedProtocol.H2, ProtocolNegotiator.negotiate(new Socket(), src)); + assertEquals(NegotiatedProtocol.HTTP_2, ProtocolNegotiator.negotiate(new Socket(), src)); // peek() must not have consumed anything — the full 24-byte preface is still there for // whatever reads next (Http2Connection, once it exists). byte[] readBack = new byte[24]; @@ -85,7 +85,6 @@ class ProtocolNegotiatorTest { /** * Binds a real TLS listener offering {@code serverAlpn}, connects a client offering * {@code clientAlpn}, forces the handshake on both sides (mirroring {@code Http1Connection}/{@code ConnectionRunner}'s - * EX-30 fix), and hands the accepted server-side socket to {@code assertion}. */ private static void withNegotiatedAlpn(Path dir, String[] serverAlpn, String[] clientAlpn, ThrowingConsumer assertion) throws Exception { @@ -129,7 +128,7 @@ class ProtocolNegotiatorTest { void alpnH2_negotiatesH2(@TempDir Path dir) throws Exception { withNegotiatedAlpn(dir, new String[]{"h2", "http/1.1"}, new String[]{"h2", "http/1.1"}, server -> { assertEquals("h2", server.getApplicationProtocol()); - assertEquals(NegotiatedProtocol.H2, + assertEquals(NegotiatedProtocol.HTTP_2, ProtocolNegotiator.negotiate(server, new BufferedByteSource(server.getInputStream(), server))); }); } diff --git a/flash/src/test/java/dev/relism/flash/transport/ServerLifecycleGracefulShutdownTest.java b/flash/src/test/java/dev/relism/flash/transport/ServerLifecycleGracefulShutdownTest.java index 99e4173..e148835 100644 --- a/flash/src/test/java/dev/relism/flash/transport/ServerLifecycleGracefulShutdownTest.java +++ b/flash/src/test/java/dev/relism/flash/transport/ServerLifecycleGracefulShutdownTest.java @@ -16,7 +16,6 @@ import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.*; /** - * {@code EX-32}: the two-stage graceful shutdown — stop accepting, let an in-flight request * finish (forced to {@code Connection: close}), then force-close whatever remains after * {@code shutdownDrainTimeoutMs}. */ @@ -72,7 +71,6 @@ class ServerLifecycleGracefulShutdownTest { assertTrue(response.startsWith("HTTP/1.1 200 OK"), response); assertTrue(response.contains("done"), response); - // EX-32: the in-flight request is forced to close rather than keep-alive, even // though the client asked for HTTP/1.1's default keep-alive. assertTrue(response.contains("Connection: close"), response); diff --git a/flash/src/test/java/dev/relism/flash/websocket/WebSocketFragmentationAndValidationTest.java b/flash/src/test/java/dev/relism/flash/websocket/WebSocketFragmentationAndValidationTest.java index 2bda2dc..9d0c172 100644 --- a/flash/src/test/java/dev/relism/flash/websocket/WebSocketFragmentationAndValidationTest.java +++ b/flash/src/test/java/dev/relism/flash/websocket/WebSocketFragmentationAndValidationTest.java @@ -11,7 +11,6 @@ import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.*; /** - * {@code EX-11} (bulk header read) and {@code EX-12} (continuation reassembly, mandatory * masking, opcode validation, control-frame constraints, correct close codes) coverage for * {@link WebSocketSession#readFrame}. */ @@ -52,7 +51,6 @@ class WebSocketFragmentationAndValidationTest { return new WebSocketSession(new ByteArrayInputStream(raw), new ByteArrayOutputStream(), bufferSize); } - // --- EX-12: continuation reassembly ------------------------------------------ @Test void continuationFrames_reassembleIntoOneMessage() throws IOException { @@ -118,7 +116,6 @@ class WebSocketFragmentationAndValidationTest { assertEquals(1009, e.closeCode()); } - // --- EX-12: mandatory masking direction --------------------------------------- @Test void serverSession_unmaskedIncomingFrame_rejected1002() throws IOException { @@ -149,7 +146,6 @@ class WebSocketFragmentationAndValidationTest { assertEquals("hi", new String(frame.copyPayload(), StandardCharsets.UTF_8)); } - // --- EX-12: opcode validation -------------------------------------------------- @Test void reservedOpcode_rejected1002() throws IOException { @@ -160,7 +156,6 @@ class WebSocketFragmentationAndValidationTest { assertEquals(1002, e.closeCode()); } - // --- EX-12: control-frame constraints ------------------------------------------ @Test void fragmentedControlFrame_rejected1002() throws IOException { @@ -189,7 +184,6 @@ class WebSocketFragmentationAndValidationTest { assertEquals(125, frame.payloadLength()); } - // --- EX-11: bulk header read, not one syscall per byte ------------------------- private static final class CountingInputStream extends InputStream { private final InputStream delegate;