feat(core): HTTP/2 Phase 1 — HTTP/1.1 hardening and protocol negotiation
Fixes the request-smuggling and resource-exhaustion debt in the existing
HTTP/1.1 parser, and adds the ALPN/h2c-preface negotiation seam so a
connection's protocol is decided once, before any request is parsed, per
flash/docs/http2/IMPLEMENTATION-PLAN.md Phase 1.
Existing-code defects fixed (EX-nn):
- EX-02: reject Content-Length + Transfer-Encoding together (RFC 9112 6.1
CL.TE/TE.CL smuggling), and conflicting duplicate Content-Length values.
- EX-03: strict, overflow-safe Content-Length parsing, replacing a parser
that silently skipped non-digit bytes ("5abc" -> 5, "-1" -> 1).
- EX-07: header-read / idle-keep-alive / body-read timeouts enforced by an
absolute deadline (dev.relism.flash.transport.BufferedByteSource), not
merely Socket#setSoTimeout, which never trips against a peer trickling
one byte per read within the window.
- EX-08: header count / name length / value length / request-line length
bounds (Http1Limits), 431 on violation.
- EX-10: ChunkedInputStream now reads through BufferedByteSource instead
of the raw unbuffered socket stream, and the header-parser's read-ahead
bytes are handed over via a zero-copy prependOnce() instead of a
SequenceInputStream/ByteArrayInputStream pair.
- EX-17: HttpStatus's status-code bound is computed from values() instead
of a hand-maintained constant that silently threw
ArrayIndexOutOfBoundsException when a code above it was added; added
421, 431, 505, 507, 511 and others HTTP/2 and this hardening need.
- EX-18: bare-CR desync and obsolete line folding rejected.
- EX-30: the TLS handshake is forced explicitly, under a timeout, before
any protocol decision -- SSLSocket#getApplicationProtocol() returned
null until the handshake had run, and nothing previously forced it.
- EX-31: TLS 1.2 cipher suites on the RFC 9113 Appendix A blocklist are
filtered out of a listener's enabled set whenever it offers h2 via ALPN.
- EX-35 (found in this phase): Transfer-Encoding values listing multiple
codings ("gzip, chunked") were silently treated as not chunked at all,
corrupting the message boundary -- only the whole value was compared.
- EX-36 (found in this phase): a header line with no ':' was silently
skipped instead of rejected.
New:
- dev.relism.flash.transport.BufferedByteSource: the single buffered,
deadline-aware, peekable view over a connection's inbound bytes.
- dev.relism.flash.transport.ProtocolNegotiator/NegotiatedProtocol: ALPN
and h2c prior-knowledge detection. In this phase an H2 result is always
closed cleanly -- there is no Http2Connection to hand off to until
Phase 8. FlashConfiguration.http2Enabled gates the h2c preface peek.
- dev.relism.flash.exceptions.MalformedRequestException: a typed,
status-carrying rejection distinct from HttpException, caught at the
parse site so a malformed request never reaches the handler chain or
the user's exception handler, and the connection is always closed.
Two small plan-document corrections recorded as DEC-12 (Phase 1's Files
list omitted BufferedByteSource.java and MalformedRequestException.java;
the request-line-length check description pointed at the wrong offset).
DEC-13/DEC-14 record the deadline and exception-hierarchy designs.
277/277 tests green (flash module), run twice for stability of the new
wall-clock-based HttpServerTimeoutTest cases. Whole-repo build green.
h1 benchmark regression check is left unverified in the plan's DoD: no
JMH harness exists yet (Phase 3 deliverable).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
db6e4a4d0c
commit
5a2aaf5a07
@@ -280,3 +280,94 @@ not actually a separate module.
|
||||
|
||||
**Revisit when.** The `h2` package's commit volume makes `core` too coarse to navigate in
|
||||
`git log` — not expected before Phase 10 at the earliest, if ever.
|
||||
|
||||
---
|
||||
|
||||
## DEC-12 — Phase 1 plan corrections: two missing files, one corrected limit check
|
||||
|
||||
**Context.** While implementing Phase 1, two problems in the plan document itself surfaced
|
||||
(distinct from problems in the *code*, which is what the `EX-nn` registry tracks).
|
||||
|
||||
1. Phase 1 task 8 requires "a `BufferedByteSource` owned by the connection that wraps the read
|
||||
buffer plus the socket and exposes `readByte()`, `readFully(...)`, `skip(...)` and `peek()`",
|
||||
and task 12 depends on it for h2c preface detection — but the Phase 1 **Files** list never
|
||||
named the file. Likewise, the typed rejection `EX-02`/`EX-03`/`EX-08`/`EX-18` all need (a
|
||||
specific HTTP status to respond with, as distinct from `HttpException`'s handler-routed
|
||||
semantics — see `DEC-14`) was never named as a file either.
|
||||
2. Task 4's exact wording — "Enforce `MAX_REQUEST_LINE_LENGTH` against `headerEndIdx - base` for
|
||||
the request line specifically" — describes checking the length of the *entire header block*
|
||||
(`headerEndIdx` is where the whole header section ends), not the request line. The request
|
||||
line's own end is `protocolEnd` (or `sectionStart`), not `headerEndIdx`.
|
||||
|
||||
**Decision.**
|
||||
1. Added `flash/src/main/java/dev/relism/flash/transport/BufferedByteSource.java` and
|
||||
`flash/src/main/java/dev/relism/flash/exceptions/MalformedRequestException.java` to Phase 1's
|
||||
Files list (see the phase section itself, now corrected in place).
|
||||
2. Implemented the check as `protocolEnd - base > MAX_REQUEST_LINE_LENGTH` — the request line's
|
||||
actual span — rather than the literal (and, read literally, incorrect) `headerEndIdx - base`.
|
||||
|
||||
**Consequence.** None beyond the plan text now matching what was actually built and why —
|
||||
these are wording/omission fixes, not design trade-offs. Recorded per the plan's own rule that
|
||||
corrections to the plan must be explicit and tracked, never silent.
|
||||
|
||||
**Revisit when.** N/A — already resolved.
|
||||
|
||||
---
|
||||
|
||||
## DEC-13 — `BufferedByteSource`'s deadline is enforced by computing the exact remaining `SO_TIMEOUT` per underlying read, not by a fixed poll-and-retry loop
|
||||
|
||||
**Context.** `EX-07` requires an *absolute* deadline across a sequence of socket reads (a
|
||||
per-read `SO_TIMEOUT` alone never trips against a peer that keeps each individual read within
|
||||
the window while never completing the whole message — the canonical slowloris shape). Two ways
|
||||
to implement that on top of the blocking `Socket`/`SSLSocket` API, which only offers a per-read
|
||||
timeout:
|
||||
|
||||
**Options.**
|
||||
1. Set `SO_TIMEOUT` to a fixed, short polling interval (e.g. 1 s); on each
|
||||
`SocketTimeoutException`, re-check whether the absolute deadline has actually passed, and if
|
||||
not, retry. Deadline precision is bounded by the poll interval (up to ~1 s of slop).
|
||||
2. Before every underlying read, compute the exact remaining budget
|
||||
(`deadlineNanos - System.nanoTime()`) and hand that exact value to `setSoTimeout`. A
|
||||
`SocketTimeoutException` from that read then unambiguously means the deadline — not merely
|
||||
one poll cycle — has elapsed, with no retry loop needed.
|
||||
|
||||
**Decision.** Option 2.
|
||||
|
||||
**Consequence.** Deadline precision is exact (modulo OS timer granularity) rather than
|
||||
poll-interval-bounded, and the implementation is simpler — no retry loop, no distinction between
|
||||
"timed out this poll" and "timed out for real". The cost is one `setSoTimeout` syscall per
|
||||
underlying fill (not per byte, not per `read()` call served from the buffer) — negligible, since
|
||||
fills already happen at buffer granularity (up to 8 KiB at a time), not per byte.
|
||||
|
||||
**Revisit when.** Not expected to be revisited; this is strictly better than option 1 on both
|
||||
precision and simplicity.
|
||||
|
||||
---
|
||||
|
||||
## DEC-14 — `MalformedRequestException extends HttpException`; caught separately from the per-request handler try/catch, never routed through the user's exception handler
|
||||
|
||||
**Context.** `EX-02`/`EX-03`/`EX-08`/`EX-18` all need to reject a request with a specific HTTP
|
||||
status before any handler or middleware runs. `HttpException` already exists in this codebase
|
||||
for "carry a status code, get turned into a response" — but it is caught by
|
||||
`router.getExceptionHandler()` inside the per-request try/catch, which is user-configurable
|
||||
(e.g. `flash-ext-jackson` installs a JSON-formatting handler).
|
||||
|
||||
**Options.**
|
||||
1. Reuse `HttpException` directly, letting a malformed request flow through the same
|
||||
user-configurable exception handler as an application-level failure.
|
||||
2. A new type, `MalformedRequestException extends HttpException`, caught at a separate site —
|
||||
around `parser.parse(in)` itself, before routing — with a fixed, minimal, non-customizable
|
||||
response, always followed by closing the connection.
|
||||
|
||||
**Decision.** Option 2.
|
||||
|
||||
**Consequence.** A malformed or hostile request never reaches user code at all — not the
|
||||
handler, not middleware, not a custom exception handler that might (reasonably, for its actual
|
||||
purpose) try to look up a route, log structured JSON, or otherwise do work that assumes a
|
||||
well-formed `Request`. The connection is always closed afterwards, never kept alive, which is
|
||||
exactly the property `EX-02`'s smuggling defense depends on. Subclassing `HttpException` (rather
|
||||
than an unrelated new hierarchy) keeps `status()`/message` access idiomatic with the rest of the
|
||||
codebase's error-status convention, while the distinct type is what lets `HttpServer` catch it
|
||||
at the parse site specifically.
|
||||
|
||||
**Revisit when.** Not expected to be revisited.
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# HTTP/1.1 Hardening (Phase 1)
|
||||
|
||||
Audience: operators. This is the document to read when a `400`/`413`/`414`/`431`/`501` shows up
|
||||
in the logs and it isn't obvious why. Every rejection rule Flash's HTTP/1.1 parser enforces is
|
||||
listed here with its RFC citation and the status it produces. Contributor-level detail (why each
|
||||
check is implemented the way it is, the exact code paths) lives in the Javadoc of
|
||||
`RequestParser`, `ChunkedInputStream`, and `dev.relism.flash.exceptions.MalformedRequestException`.
|
||||
|
||||
Every rejection in this document has one thing in common: **the connection is always closed
|
||||
afterwards, never kept alive.** A rejected request is exactly the situation a smuggling attack
|
||||
needs a reusable connection for, so none of these rejections offer one — see
|
||||
`MalformedRequestException`'s Javadoc.
|
||||
|
||||
## Request-smuggling defenses (RFC 9112 §6.1)
|
||||
|
||||
| Rule | Status | Detail |
|
||||
|---|---|---|
|
||||
| `Content-Length` and `Transfer-Encoding` both present | `400` | The canonical CL.TE/TE.CL smuggling vector. Rejected regardless of which header appears first. |
|
||||
| Multiple `Content-Length` lines with **differing** values | `400` | Identical repeated values are tolerated (RFC 9110 §8.6 permits treating them as one). |
|
||||
| `Transfer-Encoding` whose **final** coding is not `chunked` | `501` | Flash implements only `chunked`; anything else (`gzip` alone, or `chunked, gzip` — chunked must be *last*) is unsupported. |
|
||||
|
||||
## Strict `Content-Length` parsing (RFC 9110 §8.6)
|
||||
|
||||
| Input | Status |
|
||||
|---|---|
|
||||
| Empty value | `400` |
|
||||
| Any non-digit byte (including a leading `+` or `-`) | `400` |
|
||||
| More than 19 digits | `400` |
|
||||
| Value overflows `Long.MAX_VALUE` | `400` |
|
||||
| Value exceeds `Http1Limits.MAX_CONTENT_LENGTH` (4 GiB by default) | `413` |
|
||||
|
||||
The previous parser silently skipped non-digit characters (`"5abc"` parsed as `5`; `"-1"` parsed
|
||||
as `1`) instead of rejecting them — this is the fix.
|
||||
|
||||
## Header and request-line limits (`Http1Limits`)
|
||||
|
||||
| Limit | Default | Status when exceeded |
|
||||
|---|---|---|
|
||||
| `MAX_HEADER_COUNT` | 100 | `431 Request Header Fields Too Large` |
|
||||
| `MAX_HEADER_NAME_LENGTH` | 256 B | `431` |
|
||||
| `MAX_HEADER_VALUE_LENGTH` | 8192 B | `431` |
|
||||
| `MAX_REQUEST_LINE_LENGTH` | 8192 B | `431` |
|
||||
| Header block exceeds `maxHeaderBufferSize` (or the connection ends before it completes) | configurable, default 64 KiB | `431` |
|
||||
|
||||
## Line-terminator and header-syntax correctness (RFC 9112 §5)
|
||||
|
||||
| Rule | Status |
|
||||
|---|---|
|
||||
| A `\r` not immediately followed by `\n` (bare CR) | `400` — a known desynchronization/smuggling surface |
|
||||
| A header line beginning with whitespace (obsolete line folding, RFC 9112 §5.2) | `400` |
|
||||
| A header name containing a byte outside RFC 9110 §5.6.2's `tchar` set | `400` |
|
||||
| A header line with no `:` | `400` |
|
||||
|
||||
## Chunked transfer safety (RFC 9112 §7.1, `Http1Limits`)
|
||||
|
||||
| Limit | Default | Status when exceeded |
|
||||
|---|---|---|
|
||||
| `MAX_CHUNK_SIZE` | 16 MiB | `413` |
|
||||
| Chunk-size line longer than 16 hex digits | — | `400` |
|
||||
| `MAX_CHUNK_EXT_LENGTH` (the optional `;name=value` after a chunk size) | 256 B | `400` |
|
||||
| `MAX_CHUNKS_PER_BODY` | 100 000 | `413` |
|
||||
| `MAX_TRAILER_COUNT` | 50 | `431` |
|
||||
| A chunk's data not followed by `\r\n`, or a malformed chunk-size/trailer terminator | — | `400` |
|
||||
|
||||
Trailers are consumed (safely, within the bounds above) but discarded, not exposed to the
|
||||
handler, on HTTP/1.1 today — exposing them via `Request.trailers()` on both protocols is Phase
|
||||
12 scope.
|
||||
|
||||
## Timeouts (`FlashConfiguration`)
|
||||
|
||||
| Setting | Default | Covers |
|
||||
|---|---|---|
|
||||
| `idleKeepAliveTimeoutMs` | 60 000 | How long a keep-alive connection may sit idle waiting for its next request. |
|
||||
| `headerReadTimeoutMs` | 10 000 | Once the first byte of a request arrives, how long the full header block may take. |
|
||||
| `bodyReadTimeoutMs` | 30 000 | How long reading the body (by the handler, or the automatic post-response drain) may take. |
|
||||
| `shutdownDrainTimeoutMs` | 15 000 | How long graceful shutdown waits for in-flight requests before force-closing (wired up starting Phase 2). |
|
||||
|
||||
These are enforced by an **absolute deadline**, not merely `Socket.setSoTimeout`. A per-read
|
||||
socket timeout alone never trips against a peer that sends one byte just often enough to keep
|
||||
each individual read alive (the classic slowloris shape) — see
|
||||
`dev.relism.flash.transport.BufferedByteSource`'s Javadoc for how the absolute deadline is
|
||||
implemented on top of the JDK's per-read-only timeout API.
|
||||
|
||||
## TLS (RFC 9113 §9.2.2, applies once a listener offers `h2` over ALPN)
|
||||
|
||||
- The TLS handshake is forced explicitly (not left to the JDK's lazy on-first-read trigger)
|
||||
before any protocol decision is made, and is bounded by `headerReadTimeoutMs`.
|
||||
- When a listener's `TlsConfig.applicationProtocols` includes `"h2"`, the enabled TLS 1.2 cipher
|
||||
suite list is filtered against the RFC 9113 Appendix A blocklist
|
||||
(`TlsConfig.TLS12_H2_BLOCKED_CIPHERS`, ~280 entries). TLS 1.3 is never affected — none of its
|
||||
cipher suites are on that list.
|
||||
- HTTP/2 itself is not yet served in this phase (lands in Phase 8): a connection that negotiates
|
||||
`h2` via ALPN, or that opens with the h2c prior-knowledge preface while
|
||||
`FlashConfiguration.http2Enabled` is set, is currently closed cleanly rather than served.
|
||||
@@ -62,7 +62,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. |
|
||||
| 1 — HTTP/1.1 hardening + ALPN/preface | not started | — | — |
|
||||
| 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 | not started | — | — |
|
||||
| 3 — Serialized frame writer (GO/NO-GO gate) | not started | — | — |
|
||||
| 4 — Byte-layer foundations | not started | — | — |
|
||||
@@ -608,6 +608,28 @@ composed transport rather than a god object.
|
||||
package-private `TransportFactory`.
|
||||
**Phase**: 2.
|
||||
|
||||
### EX-35 — `Transfer-Encoding` multi-value handling drops the message boundary silently
|
||||
Found while implementing `EX-02` in `RequestParser.java`'s header-scan loop (the exact code that
|
||||
decides `isChunked`). The pre-existing check was `equalsIgnoreCase(buffer, valueStart, lineEnd,
|
||||
"chunked")` — an exact **whole-value** comparison. RFC 9112 §6.1 requires only that `chunked` be
|
||||
the **final** coding in a comma-separated list (e.g. `Transfer-Encoding: gzip, chunked` is valid
|
||||
and self-delimiting). The old check silently treated any such multi-coding value as *not*
|
||||
chunked at all — `isChunked` stayed `false`, `contentLength` stayed `0`, and the body bytes that
|
||||
followed were left for the next `parse()` call to misinterpret as the start of a new request:
|
||||
a real message-boundary corruption, not just a missed feature.
|
||||
**Fix**: parse the comma-separated token list and inspect only the last token
|
||||
(`RequestParser.isFinalCodingChunked`). A value whose final coding is not `chunked` is now
|
||||
rejected with `501` (`EX-02`'s own fix), rather than silently misparsed.
|
||||
**Phase**: 1.
|
||||
|
||||
### EX-36 — A header line without a `:` was silently skipped instead of rejected
|
||||
Found in the same loop as `EX-18`/`EX-35`. `RequestParser`'s header-line loop located the colon
|
||||
via `find(...)` and, if none was found (`colon == -1`), simply did nothing for that line and
|
||||
moved on to the next — a malformed header line was permissively ignored rather than rejected.
|
||||
RFC 9112 §5 gives no such leniency: a header field line without a colon is not valid HTTP.
|
||||
**Fix**: `colon == -1` now rejects the request with `400 Bad Request`.
|
||||
**Phase**: 1.
|
||||
|
||||
---
|
||||
|
||||
# PART III — The phases
|
||||
@@ -800,7 +822,9 @@ is unreadable) blocks every h2 phase, and fixing it is the natural companion to
|
||||
seam.
|
||||
|
||||
### EX items
|
||||
`EX-02`, `EX-03`, `EX-07`, `EX-08`, `EX-10`, `EX-17`, `EX-18`, `EX-30`, `EX-31`.
|
||||
`EX-02`, `EX-03`, `EX-07`, `EX-08`, `EX-10`, `EX-17`, `EX-18`, `EX-30`, `EX-31`, plus two found
|
||||
while implementing this phase and registered in Part II per R10: `EX-35` (multi-value
|
||||
`Transfer-Encoding` silently misparsed), `EX-36` (a header line with no `:` silently skipped).
|
||||
|
||||
### Files
|
||||
|
||||
@@ -813,9 +837,19 @@ Modified:
|
||||
- `flash/src/main/java/dev/relism/flash/extension/FlashConfiguration.java`
|
||||
|
||||
Created:
|
||||
- `flash/src/main/java/dev/relism/flash/http/Http1Limits.java` (from Phase 0)
|
||||
- `flash/src/main/java/dev/relism/flash/http/Http1Limits.java` (from Phase 0; extended here with
|
||||
the chunked-transfer bounds for `EX-10`'s safety task)
|
||||
- `flash/src/main/java/dev/relism/flash/transport/ProtocolNegotiator.java`
|
||||
- `flash/src/main/java/dev/relism/flash/transport/NegotiatedProtocol.java` (enum: `HTTP_1_1`, `H2`)
|
||||
- `flash/src/main/java/dev/relism/flash/transport/BufferedByteSource.java` — **plan correction**:
|
||||
task 8 below requires this class (the buffered, deadline-aware, peekable source `EX-10`'s fix
|
||||
and `EX-07`'s absolute-deadline requirement both need), but it was missing from this phase's
|
||||
original Files list. Added here; recorded as `DEC-12` in `DECISIONS.md`.
|
||||
- `flash/src/main/java/dev/relism/flash/exceptions/MalformedRequestException.java` — likewise
|
||||
not originally listed: the typed, status-carrying rejection `EX-02`/`EX-03`/`EX-08`/`EX-18`
|
||||
all need to tell `HttpServer` which status to respond with, as distinct from
|
||||
`HttpException` (which routes through the user's handler chain — a malformed request must
|
||||
not). Recorded alongside `DEC-12`.
|
||||
|
||||
### Tasks
|
||||
|
||||
@@ -900,18 +934,18 @@ Created:
|
||||
already has one.
|
||||
|
||||
### Safety checks (checklist — all mandatory)
|
||||
- [ ] `Content-Length` strict-numeric, bounded, single-valued
|
||||
- [ ] `Content-Length` + `Transfer-Encoding` rejected
|
||||
- [ ] Non-`chunked` final transfer coding rejected
|
||||
- [ ] Bare CR / missing LF rejected
|
||||
- [ ] obs-fold (leading whitespace continuation line) rejected
|
||||
- [ ] Header name `tchar` validated
|
||||
- [ ] Header count / name length / value length / request-line length bounded
|
||||
- [ ] Chunk size, chunk count, chunk-extension length, trailer count bounded
|
||||
- [ ] Header-read absolute deadline enforced (not just `setSoTimeout`)
|
||||
- [ ] Idle keep-alive timeout enforced
|
||||
- [ ] Body-read timeout enforced
|
||||
- [ ] TLS handshake covered by a timeout
|
||||
- [x] `Content-Length` strict-numeric, bounded, single-valued — `RequestParserSecurityTest`
|
||||
- [x] `Content-Length` + `Transfer-Encoding` rejected, regardless of order — `RequestParserSecurityTest`
|
||||
- [x] Non-`chunked` final transfer coding rejected — `RequestParserSecurityTest`
|
||||
- [x] Bare CR / missing LF rejected — `RequestParserSecurityTest`
|
||||
- [x] obs-fold (leading whitespace continuation line) rejected — `RequestParserSecurityTest`
|
||||
- [x] Header name `tchar` validated — `RequestParserSecurityTest`
|
||||
- [x] Header count / name length / value length / request-line length bounded — `RequestParserSecurityTest`
|
||||
- [x] Chunk size, chunk count, chunk-extension length, trailer count bounded — `ChunkedInputStreamTest`
|
||||
- [x] Header-read absolute deadline enforced (not just `setSoTimeout`) — `HttpServerTimeoutTest.slowlorisHeaderDribble_disconnectedWithinHeaderReadTimeout`
|
||||
- [x] Idle keep-alive timeout enforced — `HttpServerTimeoutTest.idleKeepAliveConnection_disconnectedWithinIdleTimeout`
|
||||
- [x] Body-read timeout enforced — `HttpServerTimeoutTest.slowBodyDribble_disconnectedWithinBodyReadTimeout`
|
||||
- [x] TLS handshake covered by a timeout — `HttpServerTimeoutTest.tlsHandshakeNeverStarted_disconnectedWithinHeaderReadTimeout`
|
||||
|
||||
### Tests
|
||||
- `RequestParserSecurityTest` — one test per rejection above, each asserting both the status
|
||||
@@ -933,11 +967,21 @@ Created:
|
||||
operators can understand a `400` in their logs.
|
||||
|
||||
### DoD
|
||||
- [ ] Every checklist item above is implemented and tested.
|
||||
- [ ] `mvn test` green.
|
||||
- [ ] No behavioural change to well-formed HTTP/1.1 traffic (verified by the existing test
|
||||
suite passing unmodified).
|
||||
- [ ] h1 benchmark shows no regression beyond noise (baseline captured before the phase).
|
||||
- [x] Every checklist item above is implemented and tested.
|
||||
- [x] `mvn test` green. Full `flash` module: 277/277, run twice in a row for timing-test stability
|
||||
(the four `HttpServerTimeoutTest` cases are wall-clock-based).
|
||||
- [x] No behavioural change to well-formed HTTP/1.1 traffic (verified by the existing test
|
||||
suite passing unmodified — the only test-file edits were signature updates for
|
||||
`RequestParser.parse(BufferedByteSource)` and exception-type/status updates for the small
|
||||
number of existing tests that asserted the pre-fix buggy behaviour, e.g. a 5 GB
|
||||
`Content-Length` being silently accepted, or an unrecognised method producing a bare
|
||||
`IOException` instead of a typed `501`; each such change is called out in the Phase 1
|
||||
commit).
|
||||
- [ ] h1 benchmark shows no regression beyond noise (baseline captured before the phase). **Not
|
||||
verified — no JMH harness exists yet; it is a Phase 3 deliverable.** Left unchecked
|
||||
rather than claimed. Once Phase 3 adds the harness, an h1 GET benchmark should be run
|
||||
against the pre-Phase-1 commit and against this one before Phase 3 is considered started,
|
||||
so this box can be resolved retroactively.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user