Files
Flash5/README.md
T
Zakaria El OrcheandClaude Sonnet 5 a315e1df8b feat(core): HTTP/2 Phase 2 — transport decomposition
Breaks HttpServer (563 lines, eleven responsibilities) into named,
single-purpose components and introduces the ConnectionProtocol seam
HTTP/2 plugs into starting Phase 8, per
flash/docs/http2/IMPLEMENTATION-PLAN.md Phase 2.

New packages:
- dev.relism.flash.transport: TransportFactory (composition root, EX-34),
  ListenerBinder, BoundListener, TransportTuning, AcceptLoop,
  ConnectionRunner (per-connection setup/teardown), ConnectionProtocol
  (the h1/h2 seam), ConnectionContext, ConnectionScratch + ScratchPool
  (EX-06), ServerLifecycle (implements ServerHandle; start/stop/graceful
  shutdown, EX-32).
- dev.relism.flash.http1: Http1Connection (the keep-alive request loop,
  implements ConnectionProtocol), Http1ResponseWriter, Http1KeepAlive
  (the shared Connection-header token-list scanner, EX-13).
- dev.relism.flash.websocket additions: WebSocketUpgrade (detection +
  handshake), WebSocketLoop (session loop), WebSocketProtocolException.

Existing-code defects fixed (EX-nn):
- EX-01: WebSocketSession's two blocking-write sites use ReentrantLock
  instead of synchronized (out) -- a virtual thread blocking inside
  synchronized pins its carrier platform thread on Java 21.
- EX-06: HttpServer's three ThreadLocals (SHA1, LONG_BUF,
  STREAM_RELAY_BUFFER) replaced by ConnectionScratch, pooled via
  ScratchPool instead of one-per-virtual-thread (i.e. one-per-connection)
  growth. The router's ThreadLocals are deliberately deferred to Phase 4
  per this EX item's own phasing -- see DEC-15 for the plan-wording fix.
- EX-11: WebSocketSession.readFrame's extended-length and mask-key bytes
  are now read in a single bounded readFully instead of one at a time.
- EX-12: full RFC 6455 frame validation -- continuation-frame
  reassembly, mandatory masking-direction enforcement, opcode
  validation, control-frame constraints (not fragmented, <=125 bytes),
  and WebSocketProtocolException carrying the correct close code (1002
  protocol error, 1009 message too big).
- EX-13: Connection header token-list scanning shared between the
  keep-alive decision and the WebSocket upgrade check.
- EX-14: HEAD responses report Content-Length but write no body.
- EX-15: Content-Type omitted when empty; Content-Length and the body
  omitted entirely for 204/304/1xx responses.
- EX-16: Date header (dev.relism.flash.http.DateHeader), refreshed once
  per second by a shared daemon thread; FlashConfiguration.sendDate.
- EX-32: two-stage graceful shutdown -- stop accepting, force
  Connection: close on the response an in-flight handler is still
  producing (re-checked after the handler runs, not just before
  dispatch, so a shutdown beginning mid-handler is still honoured),
  drain up to shutdownDrainTimeoutMs, then force-close.
- EX-34: ServerHandle.create delegates to TransportFactory instead of
  constructing HttpServer directly.

Two plan corrections recorded: DEC-15 (Phase 2's "no ThreadLocal
anywhere" DoD line contradicted EX-06's own multi-phase assignment --
corrected to match the registry) and DEC-16 (no separate
WebSocketFrameCodec class this phase; the EX-11/EX-12 fixes stay inside
WebSocketSession, which is one cohesive state machine under R6's own
carve-out -- revisit at Phase 15 if RFC 8441 needs the decoupling for
real).

HttpServer.java deleted.

311/311 tests green (flash module), run three times for stability of
the wall-clock-based timeout/shutdown tests. Whole-repo build green.
h1 benchmark regression check remains unverified in the plan's DoD (no
JMH harness until Phase 3, same caveat as Phase 1).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 12:03:44 +00:00

12 KiB
Raw Blame History

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

TransportFactory.create()           # binds every listener, wires the connection runner
  → AcceptLoop                      # one per listener × accept thread; hands sockets off
  → ConnectionRunner.accept()       # per-connection setup: TLS handshake, protocol negotiation
      → ProtocolNegotiator          # ALPN / h2c-preface — decides the protocol once
      → Http1Connection.run()       # the ConnectionProtocol seam; HTTP/2 plugs in here later
          → 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
          → Http1ResponseWriter.write() # status line, headers, then fixed or chunked body
          → loop or close socket        # based on Connection header, or ServerLifecycle draining
  • Virtual threads — each accepted socket runs on a virtual thread (Executors.newVirtualThreadPerTaskExecutor(), owned by TransportFactory). 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.
  • ConnectionProtocol seam — h1 and h2 (in progress, see flash/docs/http2/) are peers behind this interface, decided once per connection by ProtocolNegotiator, never by an if inside shared code. See flash/docs/http2/TRANSPORT.md for the full component breakdown.

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