feat(core): HTTP/2 Phase 0 — groundwork (limits, error model, decision log)

Establishes the package layout, limits/error model and decision-log
convention that every later HTTP/2 phase depends on, per
flash/docs/http2/IMPLEMENTATION-PLAN.md Phase 0.

- dev.relism.flash.h2: package-info (architecture overview), Http2ErrorCode
  (the 14 RFC 9113 §7 codes with precomputed 4-byte wire encodings),
  Http2Exception (connection error -> GOAWAY) and Http2StreamException
  (stream error -> RST_STREAM), neither extending IOException, both with
  stack-trace capture disabled on the hot rejection path.
- Http2Limits: every bound Phase 0 requires (concurrent streams, frame
  size, header list size, CONTINUATION/reset/settings/ping rate bounds,
  flow-control windows, HPACK table size/string length, assembly and idle
  timeouts), each documented with the attack or RFC clause it addresses.
- dev.relism.flash.http.Http1Limits: the h1 bounds needed by EX-03 (strict
  Content-Length) and EX-08 (header count/size limits).
- flash/docs/http2/DECISIONS.md seeded with DEC-01..DEC-11 (the ten
  decisions implied by the plan itself, plus DEC-11 recording that commits
  keep scope `core` rather than adding `h2` to AGENTS.md).
- flash/docs/http2/IMPLEMENTATION-PLAN.md: added the Progress Ledger
  (tracks phase status across sessions) and checked off Phase 0's DoD.

19 new tests, full flash module suite green (226/226).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Zakaria El Orche
2026-08-13 10:59:49 +00:00
co-authored by Claude Sonnet 5
parent 8f1f30b973
commit db6e4a4d0c
13 changed files with 3968 additions and 0 deletions
@@ -0,0 +1,95 @@
package dev.relism.flash.h2;
/**
* The 14 HTTP/2 error codes defined by RFC 9113 §7.
*
* <p>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).
*
* <p>{@code Http2ErrorCode} is used to reject a peer <em>and</em> to interpret what a peer
* sends us: {@link #fromCode(int)} decodes a received 32-bit value. RFC 9113 does not reserve
* unknown codes for future use in a way that requires us to accept them silently as one of the
* 14 — an endpoint that receives an error code it does not recognise treats it as
* {@code INTERNAL_ERROR}-equivalent for logging purposes; {@link #fromCode(int)} returns
* {@code null} for that case and callers log the raw integer rather than guessing a mapping.
*/
public enum Http2ErrorCode {
/** Graceful shutdown or successful completion; not an error. RFC 9113 §7. */
NO_ERROR(0x00),
/** The peer violated the protocol in a way not covered by a more specific code. */
PROTOCOL_ERROR(0x01),
/** Unexpected internal condition on our side (e.g. an uncaught exception on the demux loop). */
INTERNAL_ERROR(0x02),
/** A flow-control window was violated: overflow past 2^31-1, or a peer exceeded its window. */
FLOW_CONTROL_ERROR(0x03),
/** The peer did not acknowledge our SETTINGS within {@code SETTINGS_ACK_TIMEOUT_MS}. */
SETTINGS_TIMEOUT(0x04),
/** A frame was received for a stream that is already closed. */
STREAM_CLOSED(0x05),
/** A frame's length did not match what its type requires (RFC 9113 §4.2, per-type rules). */
FRAME_SIZE_ERROR(0x06),
/** The stream was refused before any processing; safe for the client to retry elsewhere. */
REFUSED_STREAM(0x07),
/** Used by clients to cancel a stream; Flash never sends it, only receives it. */
CANCEL(0x08),
/** An HPACK decoding failure. Terminates the connection because the dynamic table state is lost. */
COMPRESSION_ERROR(0x09),
/** A CONNECT-tunnelled stream failed. */
CONNECT_ERROR(0x0a),
/** The peer is generating excessive load (rate-limit rejection: Rapid Reset, PING/SETTINGS floods). */
ENHANCE_YOUR_CALM(0x0b),
/** The negotiated TLS parameters fall below RFC 9113 §9.2's minimum security requirements. */
INADEQUATE_SECURITY(0x0c),
/** Defined by RFC 9113 for HTTP/1.1-only resources; Flash serves everything over h2, so unused. */
HTTP_1_1_REQUIRED(0x0d);
private static final Http2ErrorCode[] BY_CODE = new Http2ErrorCode[values().length];
static {
for (Http2ErrorCode c : values()) {
BY_CODE[c.code] = c;
}
}
private final int code;
private final byte[] bytes;
Http2ErrorCode(int code) {
this.code = code;
this.bytes = new byte[]{
(byte) (code >>> 24),
(byte) (code >>> 16),
(byte) (code >>> 8),
(byte) code
};
}
/** The numeric error code as it appears on the wire. */
public int code() {
return code;
}
/**
* The pre-encoded 4-byte big-endian wire form. Safe to write directly into a
* {@code RST_STREAM} or {@code GOAWAY} payload with a single {@code System.arraycopy} —
* never allocated or formatted per use.
*/
public byte[] bytes() {
return bytes;
}
/**
* Decodes a 32-bit error code received from a peer. Returns {@code null} for a value
* outside the 14 defined codes; the caller should log the raw integer rather than assume
* a mapping, since RFC 9113 permits future extension codes we do not yet know about.
*/
public static Http2ErrorCode fromCode(int code) {
if (code >= 0 && code < BY_CODE.length) {
return BY_CODE[code];
}
return null;
}
}
@@ -0,0 +1,73 @@
package dev.relism.flash.h2;
/**
* A <b>connection-level</b> 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.
*
* <p>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.
*
* <p>Deliberately does <b>not</b> extend {@link java.io.IOException}: the connection loop
* distinguishes a protocol violation (a decision Flash made about the peer's bytes) from a
* socket failure (the peer went away) by catching these as unrelated types. Conflating them
* would make it possible to accidentally treat a hostile peer's malformed frame as a harmless
* disconnect, or vice versa.
*
* <h3>Why stack traces are disabled</h3>
* This exception is thrown on the connection's hot rejection path — a single malformed byte
* from a hostile or buggy peer can trigger it, and under a scripted attack that can happen many
* times per second across many connections. JVM stack trace capture ({@code fillInStackTrace})
* is by far the most expensive part of constructing a {@code Throwable}, and it buys nothing
* here: the call site is exactly where {@code errorCode()} says it is, and the debug message
* already names the specific violation. The 4-argument {@link RuntimeException} constructor
* disables both suppression and stack-trace writing.
*
* <h3>Preallocated singletons</h3>
* For the common, message-less rejections (frame validation failures, HPACK structural errors)
* this class exposes shared singleton instances. Reusing one instance across threads and across
* many throws is safe <em>only because</em> the instance carries no per-throw mutable state and
* writable-stack-trace is disabled — nothing about a throw mutates the exception object.
*/
public final class Http2Exception extends RuntimeException {
private final Http2ErrorCode errorCode;
private Http2Exception(Http2ErrorCode errorCode, String message) {
super(message, null, false, false);
this.errorCode = errorCode;
}
/** The RFC 9113 §7 error code to send in the {@code GOAWAY} frame. */
public Http2ErrorCode errorCode() {
return errorCode;
}
/**
* 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.
*/
public static Http2Exception of(Http2ErrorCode code, String message) {
return new Http2Exception(code, message);
}
// ── Preallocated, message-less singletons for the hot rejection paths ──────────────────
public static final Http2Exception PROTOCOL_ERROR =
new Http2Exception(Http2ErrorCode.PROTOCOL_ERROR, "protocol error");
public static final Http2Exception FRAME_SIZE_ERROR =
new Http2Exception(Http2ErrorCode.FRAME_SIZE_ERROR, "frame size error");
public static final Http2Exception FLOW_CONTROL_ERROR =
new Http2Exception(Http2ErrorCode.FLOW_CONTROL_ERROR, "flow control error");
public static final Http2Exception COMPRESSION_ERROR =
new Http2Exception(Http2ErrorCode.COMPRESSION_ERROR, "compression error");
public static final Http2Exception INTERNAL_ERROR =
new Http2Exception(Http2ErrorCode.INTERNAL_ERROR, "internal error");
public static final Http2Exception SETTINGS_TIMEOUT =
new Http2Exception(Http2ErrorCode.SETTINGS_TIMEOUT, "settings ack timeout");
}
@@ -0,0 +1,148 @@
package dev.relism.flash.h2;
/**
* Every bound the HTTP/2 implementation enforces against a peer's input, in one place.
*
* <p>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 array or buffer throw on overrun. Each field's Javadoc names the specific attack
* or resource it bounds and, where one exists, the CVE.
*
* <p>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.
*
* <p>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.
*/
public final class Http2Limits {
private Http2Limits() {
}
/**
* Maximum number of streams a single connection may have open concurrently. Advertised to
* the peer as {@code SETTINGS_MAX_CONCURRENT_STREAMS}. Bounds per-connection memory (each
* open stream owns a per-stream HPACK arena and request/response state) against a peer that
* simply opens streams and never closes them.
*/
public static final int MAX_CONCURRENT_STREAMS = 100;
/**
* The largest frame payload we accept without the peer first raising it via our own
* {@code SETTINGS_MAX_FRAME_SIZE}. RFC 9113 §4.2 fixes the protocol default at 16384 and
* requires any advertised value to stay within {@code 16384..16777215}. Bounds the memory a
* single frame read can force us to hold.
*/
public static final int MAX_FRAME_SIZE_LOCAL = 16_384;
/**
* Maximum total size (name + value + 32 per RFC 7541 §4.1's accounting, summed over every
* header) of a decoded header list. Advertised as {@code SETTINGS_MAX_HEADER_LIST_SIZE}
* (RFC 9113 §6.5.2). This is the primary defence against an HPACK bomb: a small compressed
* block that references dynamic-table entries to expand into an enormous header list.
*/
public static final int MAX_HEADER_LIST_SIZE = 32_768;
/**
* Maximum number of CONTINUATION frames accepted for a single header block before the
* connection is torn down. Defence against CVE-2024-27316 (the "HTTP/2 CONTINUATION
* Flood"): a peer that never sets {@code END_HEADERS} can otherwise force unbounded
* decode/reassembly work per header block.
*/
public static final int MAX_CONTINUATION_FRAMES_PER_BLOCK = 8;
/**
* Maximum number of {@code RST_STREAM} frames accepted from the peer within
* {@link #RESET_RATE_INTERVAL_MS}. Defence against CVE-2023-44487 ("HTTP/2 Rapid Reset"):
* opening a stream and immediately resetting it does not count against
* {@link #MAX_CONCURRENT_STREAMS}, so without a rate bound a peer can force unbounded
* per-stream setup/teardown work at effectively unlimited concurrency.
*/
public static final int MAX_RESET_STREAMS_PER_INTERVAL = 200;
/** The rolling window (milliseconds) over which {@link #MAX_RESET_STREAMS_PER_INTERVAL} is measured. */
public static final long RESET_RATE_INTERVAL_MS = 10_000;
/**
* Maximum number of new streams accepted from the peer within
* {@link #RESET_RATE_INTERVAL_MS}. A companion bound to
* {@link #MAX_RESET_STREAMS_PER_INTERVAL}: Rapid Reset defences that only count resets can
* still be bypassed by a peer that creates streams fast enough that the reset counter never
* saturates within any single window boundary.
*/
public static final int MAX_STREAMS_CREATED_PER_INTERVAL = 400;
/**
* Maximum number of SETTINGS parameter entries accepted in a single SETTINGS frame. A
* SETTINGS frame is already bounded in byte length by {@link #MAX_FRAME_SIZE_LOCAL} (each
* entry is 6 bytes), but an explicit entry-count bound keeps the per-entry validation loop
* itself cheap to reason about and gives a distinct, loud rejection reason.
*/
public static final int MAX_SETTINGS_ENTRIES_PER_FRAME = 64;
/**
* Maximum number of outstanding (unanswered) PING responses queued for the writer. A PING
* flood forces a PONG per PING; without a bound, a peer that reads its own responses slowly
* can make us buffer unbounded PONG frames.
*/
public static final int MAX_PING_QUEUE_DEPTH = 64;
/**
* Maximum number of zero-length DATA frames accepted per stream. Zero-length DATA consumes
* no flow-control window, so window accounting does not bound it — without this limit a
* peer can force unbounded per-frame dispatch/validation CPU work at zero cost to itself.
*/
public static final int MAX_EMPTY_DATA_FRAMES_PER_STREAM = 1_000;
/**
* 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;
/**
* The connection-level flow-control window Flash advertises. Sized above
* {@link #INITIAL_WINDOW_SIZE_LOCAL} so a single active stream is never bottlenecked by the
* connection window before its own stream window, but well below
* {@code MAX_CONCURRENT_STREAMS * INITIAL_WINDOW_SIZE_LOCAL} — real traffic is never all
* streams simultaneously saturating their windows, and sizing for that worst case would
* commit 100 MiB of receive window to every connection regardless of load.
*/
public static final int CONNECTION_WINDOW_SIZE_LOCAL = 16 * 1_048_576;
/**
* 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 <em>we receive</em>.
*/
public static final int HPACK_DYNAMIC_TABLE_SIZE_LOCAL = 4_096;
/**
* Maximum length, in decoded bytes, of a single HPACK string literal. Applied during
* Huffman decode as bytes are produced, not to the encoded length — a Huffman string can
* expand by roughly 8/5, so bounding only the encoded length would let a compact input
* still decode past this limit.
*/
public static final int MAX_HPACK_STRING_LENGTH = 8_192;
/**
* Maximum time, in milliseconds, allowed between a HEADERS frame's arrival and the header
* block's completion (its {@code END_HEADERS} flag, possibly after CONTINUATION frames). A
* peer that starts a header block and then stalls indefinitely would otherwise hold the
* per-stream arena and the connection's HPACK assembly buffer forever.
*/
public static final long HEADER_BLOCK_ASSEMBLY_TIMEOUT_MS = 10_000;
/**
* Maximum time, in milliseconds, a stream may remain open with no frame activity in either
* direction. Bounds resource pinning by a peer that opens a stream and then goes silent
* without closing it — the h2 equivalent of the h1 slowloris defence in
* {@code FlashConfiguration.idleKeepAliveTimeoutMs}.
*/
public static final long STREAM_IDLE_TIMEOUT_MS = 60_000;
}
@@ -0,0 +1,46 @@
package dev.relism.flash.h2;
/**
* A <b>stream-level</b> HTTP/2 error, scoped to one stream id. Results in an {@code RST_STREAM}
* frame for {@link #streamId()} with {@link #errorCode()}; the connection and every other
* stream on it are unaffected. Compare {@link Http2Exception}, whose scope is the whole
* connection.
*
* <p>Deliberately does <b>not</b> extend {@link java.io.IOException}, for the same reason as
* {@link Http2Exception}: the connection loop must be able to distinguish "we decided to reject
* this stream" from "the socket failed" by catching unrelated exception types.
*
* <h3>Why this allocates, unlike {@code Http2Exception}'s singletons</h3>
* 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.
*
* <p>Stack trace capture is disabled for the same cost reason as {@link Http2Exception}.
*/
public final class Http2StreamException extends RuntimeException {
private final Http2ErrorCode errorCode;
private final int streamId;
public Http2StreamException(int streamId, Http2ErrorCode errorCode, String message) {
super(message, null, false, false);
this.streamId = streamId;
this.errorCode = errorCode;
}
/** The id of the stream this error terminates. */
public int streamId() {
return streamId;
}
/** The RFC 9113 §7 error code to send in the {@code RST_STREAM} frame. */
public Http2ErrorCode errorCode() {
return errorCode;
}
}
@@ -0,0 +1,73 @@
/**
* 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}).
*
* <h2>Architecture in one page</h2>
*
* <h3>The demux loop</h3>
* 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 <b>never blocks
* on application work</b> — 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.
*
* <h3>Virtual-thread-per-stream dispatch</h3>
* 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).
*
* <h3>The writer discipline</h3>
* 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.
*
* <h3>The arena strategy</h3>
* 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 <b>per-stream arena</b>, 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.
*
* <h2>What this package deliberately does not implement</h2>
* <ul>
* <li><b>Server push ({@code PUSH_PROMISE}).</b> 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.</li>
* <li><b>Priority scheduling ({@code PRIORITY} frames, and the deprecated priority fields on
* {@code HEADERS}).</b> 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.</li>
* <li><b>{@code Upgrade: h2c}.</b> 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}.</li>
* </ul>
*
* <h2>Package layout</h2>
* 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, 79, and 1415 respectively.
*/
package dev.relism.flash.h2;
@@ -0,0 +1,58 @@
package dev.relism.flash.http;
/**
* Bounds the HTTP/1.1 parser ({@code RequestParser}, {@code ChunkedInputStream}) enforces
* against a peer's input, in one place.
*
* <p>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.
*
* <p>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.
*/
public final class Http1Limits {
private Http1Limits() {
}
/**
* The largest {@code Content-Length} value accepted, in bytes. RFC 9112 places no upper
* bound on the header's numeric value, but an unbounded value from a hostile peer is a
* resource-exhaustion vector for any code path that pre-sizes a buffer from it. Requests
* declaring a length above this are rejected with {@code 413 Payload Too Large} before any
* body byte is read.
*/
public static final long MAX_CONTENT_LENGTH = 100L * 1024 * 1024;
/**
* Maximum number of header lines accepted in a single request. Without this bound, a
* request with tens of thousands of one-byte headers passes the total header-block size
* check ({@code maxHeaderBufferSize}) while still forcing every subsequent
* {@code HeaderMap} 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;
/**
* Maximum length, in bytes, of a single header field name. RFC 9110 §5.1 places no formal
* limit; this bound exists purely to cap per-header memory and scan cost.
*/
public static final int MAX_HEADER_NAME_LENGTH = 256;
/**
* Maximum length, in bytes, of a single header field value. Bounds per-header memory and
* scan cost the same way {@link #MAX_HEADER_NAME_LENGTH} bounds the name.
*/
public static final int MAX_HEADER_VALUE_LENGTH = 8_192;
/**
* Maximum length, in bytes, of the request line ({@code METHOD SP target SP version}).
* Tracked separately from the overall header-buffer size so an oversized request line is
* rejected with a specific, correct status ({@code 414 URI Too Long}) rather than folded
* into the generic header-block-too-large case.
*/
public static final int MAX_REQUEST_LINE_LENGTH = 8_192;
}