Files
Flash5/README.md
T
Zakaria El OrcheandClaude Sonnet 5 5a2aaf5a07 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>
2026-08-13 11:40:03 +00:00

11 KiB

Flash

A high-performance HTTP/1.1 server library for Java 21, built around virtual threads and a zero-allocation FSM router.

Modules

Module Description
flash Core server library — router, request parser, HTTP I/O transport
flash-extensions/flash-ext-jackson Jackson JSON integration
flash-extensions/flash-ext-openapi OpenAPI 3.0 spec + Swagger UI
flash-extensions/flash-ext-oidc OIDC Authorization Code + PKCE flow
flash-extensions/flash-ext-mcp MCP (Model Context Protocol) server — Streamable HTTP, optional OAuth2 via flash-ext-oidc
flash-extensions/flash-ext-view-core Minimal shared SSR runtime primitives
flash-extensions/flash-ext-view-jte Opinionated jte SSR extension
flash-extensions/flash-ext-view-thymeleaf Opinionated Thymeleaf SSR extension
flash-bench Demo harness (OIDC + OpenAPI + Jackson)

Requirements

  • Java 21+
  • Maven 3.8+

Quick start

FlashApp.create(8080)
    .get("/ping", (req, res) -> "pong")
    .start();

With full configuration:

FlashApp.create(
    FlashConfiguration.builder()
        .port(8080)
        .host("0.0.0.0")
        .maxHeaderBufferSize(65536)
        .build()
)
.get("/ping", (req, res) -> "pong")
.start();

Route registration

Lambda routes

FlashApp app = FlashApp.create(8080);

app.get("/hello", (req, res) -> "world");

app.post("/echo", (req, res) -> {
    byte[] body = req.body().bytes();
    return res.status(200).body(body);
});

app.get("/users/{id}", (req, res) -> {
    String id = req.pathParam("id");
    return "user:" + id;
});

Class-based handlers

Extend RequestHandler, annotate it, then scan its package. Dependencies are cached in onInit() after Flash has resolved its complete boot-time service graph:

@GET("/api/users")
public class ListUsers extends RequestHandler {
    private UserService users;

    @Override protected void onInit() { users = require(UserService.class); }
    @Override public Object handle(Request req, Response res) { return users.list(); }
}

app.scan("dev.example.api");

Middleware

Apply middleware at registration. Flash composes the final chain at boot:

Middleware authCheck = next -> (req, res) -> {
    if (req.header("Authorization") == null)
        return res.status(401).body("Unauthorized");
    return next.handle(req, res);
};

app.get("/secure", (req, res) -> "secret data", authCheck);

Multiple middlewares are composed outermost-first (left-to-right in the call):

app.get("/admin", handler, logging, auth, rateLimit);
// execution order: logging → auth → rateLimit → handler

Classpath scan

Scans a package for classes that extend RequestHandler and carry @Route. Each is instantiated via its public no-arg constructor:

app.scan("dev.example.handlers");

Namespace mounting

Mount a scoped sub-router under a prefix. All routes registered inside the scope get the prefix prepended automatically. The scope inherits the parent's extension context (annotation processors, services):

app.mount("/api", scope -> {
    scope.get("/health", (req, res) -> "ok");          // → GET /api/health
    scope.scan("dev.example.api");
});

Extensions

Extensions have one declarative configure method. They declare services, processors and route callbacks; Flash resolves the complete graph, materialises routes, compiles both routers, then opens listeners. Extension install order never makes a service “not ready”.

FlashApp.create(8080)
    .install(new JacksonExtension())
    .install(new OpenApiExtension("/openapi", "My API", "1.0.0"))
    .install(new OidcExtension(oidcConfig))
    .scan("dev.example.handlers")
    .start();

See extension-specific READMEs for full details:

Error handlers

app.onNotFound((req, res) -> res.status(404).body("Not found: " + req.path()));

app.onException((ex, req, res) -> {
    if (ex instanceof IllegalArgumentException)
        return res.status(400).body(ex.getMessage());
    return res.status(500).body("Internal error");
});

FlashConfiguration

Field Default Description
port TCP port to bind
host "0.0.0.0" Bind address
tls null TLS for the default listener — see TLS
listeners [] Multiple bind targets (port + host + optional TLS) on one app — see TLS
maxHeaderBufferSize 65536 Max size of the header buffer (bytes)
wsFrameBufferSize 65536 Per-connection WebSocket read buffer (bytes)
headerReadTimeoutMs 10000 Once a request's first byte arrives, how long the full header block may take. Bounds slowloris-style attacks — see HTTP1-HARDENING.md.
idleKeepAliveTimeoutMs 60000 How long a keep-alive connection may sit idle waiting for its next request.
bodyReadTimeoutMs 30000 How long reading a request body (handler or automatic drain) may take.
shutdownDrainTimeoutMs 15000 How long graceful shutdown waits for in-flight requests before force-closing.
http2Enabled false Whether this server will ever negotiate HTTP/2. Off by default until the HTTP/2 connection state machine lands (see flash/docs/http2/IMPLEMENTATION-PLAN.md).

TLS

HTTPS and WSS are a transport-layer concern only: once a listener is bound, the accepted Socket is either plain or an SSLSocket indistinguishably from HttpServer's point of view onward — the request parser, router, and WebSocket upgrade never branch on it. WSS is therefore not a separate feature; it's a WebSocket upgrade running over whatever transport it was handed.

Quick start

FlashApp.create(FlashConfiguration.builder()
        .port(443)
        .tls(TlsConfig.keystore(Path.of("cert.p12"), "changeit"))
        .build())
    .get("/ping", (req, res) -> "pong")   // HTTPS
    .ws("/live", handler)                 // WSS, same route API
    .start();

Multiple listeners

One app can bind any number of ports, each independently plain or TLS:

FlashApp.create(FlashConfiguration.builder()
        .listener(new FlashConfiguration.Listener(80))                                   // plain
        .listener(new FlashConfiguration.Listener(443, TlsConfig.keystore(cert, pass)))  // TLS
        .build());

A non-empty listeners list takes precedence over the top-level port/host/tls fields. Each listener gets its own accept threads; the router, WS router, and virtual-thread executor are shared by all of them — one app, N ports.

TlsConfig

Factory Use
TlsConfig.keystore(Path, String) Builds the SSLContext from a PKCS12/JKS keystore (type guessed from the extension). Pins TLSv1.2/TLSv1.3 as enabled protocols; cipher suites are left at the JDK's own curated default.
TlsConfig.ofContext(SSLContext) Escape hatch — the given SSLContext is used exactly as built. Flash never calls setSSLParameters on this path beyond what you explicitly request via clientAuth/applicationProtocols, so anything else you configured (custom KeyManager, ALPN, cipher suites) is authoritative.

Chainable on either factory:

TlsConfig.keystore(cert, pass)
    .clientAuth(ClientAuth.REQUIRE)                    // mTLS: NONE (default) | OPTIONAL | REQUIRE
    .applicationProtocols("acme-tls/1", "http/1.1")    // ALPN, in preference order

SNI falls out of keystore() for free: a keystore holding more than one certificate entry is matched against the requested hostname by each certificate's SAN (falling back to CN) — no per-hostname config. The first entry in the keystore is the default when SNI is absent or matches nothing (same convention as nginx/HAProxy's default_server).

ALPN and custom certificate selection (e.g. TLS-ALPN-01 / RFC 8737 for on-demand ACME issuance): ALPN is resolved while consuming ClientHello/producing ServerHello, which always precedes Certificate production. A custom X509ExtendedKeyManager passed via ofContext can therefore read engine.getHandshakeApplicationProtocol() (or ((SSLSocket) socket).getHandshakeApplicationProtocol()) inside chooseEngineServerAlias/chooseServerAlias — the negotiated protocol is already resolved by then, so the certificate decision can key off it.

mTLS with a private CA: clientAuth(...) only requests/requires a client certificate; keystore() deliberately doesn't expose a way to configure which CAs are trusted for that certificate (it uses the JDK default trust store). For a private CA, build the SSLContext yourself with a TrustManagerFactory and use ofContext(...).

Reading TLS info from a request

app.get("/whoami", (req, res) -> {
    if (!req.isSecure()) return "plain";
    SSLSession session = req.sslSession();          // null iff !isSecure()
    X509Certificate peer = (X509Certificate) session.getPeerCertificates()[0]; // mTLS only
    return session.getCipherSuite() + " / " + session.getProtocol();
});

Request.isSecure() / Request.sslSession() cost nothing extra per request: the SSLSocket reference is threaded through once per connection (same mechanism as remoteAddress()), and sslSession() only calls SSLSocket#getSession() — a cached-field read once the handshake that got the request this far has already completed, never a forced handshake.

WebSocketSession mirrors this exactly (isSecure(), sslSession()) by delegating to the upgrading Request — no separate TLS state is tracked for WS.

Architecture

ServerSocket.accept()
  → RequestParser.parse()       # zero-alloc header parsing, buffer reuse across keep-alive
  → GlobalRouter.route()        # two-tier: mounted sub-routers (longest prefix) then FastPathRouterImpl
  → RequestHandler.handle()     # user handler; return value sets body
  → Request.drain()             # consume unread body for keep-alive
  → HttpServer writes response  # status line, headers, then fixed or chunked body
  → loop or close socket        # based on Connection header
  • Virtual threads — each accepted socket runs on a virtual thread (Executors.newVirtualThreadPerTaskExecutor()). Java 21 required.
  • Zero-allocation routerFastPathRouterImpl uses fpr-core, a byte-level FSM that matches on METHOD + path bytes with no per-request allocation.
  • Keep-aliveRequestParser reuses its header buffer across requests on the same connection.
  • Chunked transfer — both chunked request bodies (decoded via ChunkedInputStream) and chunked response bodies are supported.
  • TLS is transport-only — see TLS. Listeners bind either a plain ServerSocket or an SSLServerSocket; nothing downstream of accept() branches on which.

Build & test

# Build all modules (skip tests)
mvn clean package -DskipTests

# Run all tests
mvn test

# Run a single test class
mvn test -pl flash -Dtest=RequestParserTest

# Run the benchmark demo server
java -jar flash-bench/target/flash-bench-1.0-SNAPSHOT.jar