This page defines the Chappe vocabulary. The names line up with the HTTP RFCs (9110/9112/9113/9114) and modern Java 25 APIs (ScopedValue, virtual threads). No in-house jargon — every term should ring a bell to a regular HTTP developer.

Server, Router, Handler

Three interfaces hold the entire public API.

Type Role

Server

Lifecycle: start() opens the listener, close() shuts it down. Built via Server.builder().

Router

Request dispatcher by method + path. Fluent builder. Supports exact routes, path params ({id}), wildcards (*), mount(prefix, handler), group(prefix, …​), filters.

Handler

Functional interface: Response handle(Request req). Everything is a handler — a router, a static file handler, a composed filter, a lambda.

Request and Response

Both types are immutable at the API surface. Response is built through a builder; Request is provided by the engine.

// Request — read-only access
req.method()           // HttpMethod: GET, POST, ...
req.path()             // String relative to the mount, no query string
req.queryParams()      // Map<String, List<String>>
req.pathParams()       // Map<String, String>
req.headers()          // Headers (case-insensitive)
req.body()             // Body (asInputStream(), asBytes(), ...)
req.contextPath()      // mount prefix, "" otherwise
req.pathInfo()         // alias of path() for mounts
req.remoteAddress()    // client SocketAddress
req.isSecure()         // true if TLS
req.scheme()           // "http" or "https"
req.attribute(k, v)    // mutable per-request attributes (Servlet-compat)

Body

Body abstracts the request or response body. Several implementations exist, all sealed inside chappe-api:

Type Use case

EmptyBody

204, 304, body-less requests.

ByteArrayBody

Short responses (text, inline JSON). Single allocation.

InputStreamBody

Wraps an InputStream (proxying, transformations).

OutputStreamBody

Push streaming: Body.ofOutputStream(out → out.write(…​)). Compatible with ServletOutputStream.

FileBody

Zero-copy via FileChannel.transferTo()Body.ofFile(path).

Filters

A Filter is a declarative middleware. It turns a Handler into another Handler:

Filter logging = next -> req -> {
    System.out.println(req.method() + " " + req.path());
    return next.handle(req);
};

Helpers shipped on Filter:

  • Filter.addHeader(name, value) — add a header to every response.

  • Filter.addHeaderIf(predicate, name, value) — conditional header (predicate evaluated per request).

  • Filter.addHeaderIfEnv(envVar, expected, name, value) — gate on an environment variable (typical: staging).

  • Filter.gzip() / Filter.gzip(threshold) — on-the-fly compression negotiated through Accept-Encoding.

Connection, frame, multiplexing

HTTP/2 vocabulary (RFC 9113), reused by HTTP/3 (RFC 9114) over a different transport.

Term Meaning in Chappe

Connection

A TCP socket (HTTP/1.1, HTTP/2) or a QUIC stream-set (HTTP/3). One virtual thread per connection.

Frame

HTTP/2 transport unit: HEADERS, DATA, SETTINGS, WINDOW_UPDATE, GOAWAY, CONTINUATION, PING, RST_STREAM, PRIORITY. Implemented in io.vidocq.chappe.http.h2.Http2Frame.

Stream

Logical sub-flow multiplexed over an HTTP/2 connection — one request + one response. Http2Stream carries the FSM (idle, open, half-closed, closed).

Multiplexing

Several streams in flight on one connection. HTTP/1.1 cannot do it (pipelining ≠ multiplexing); HTTP/2 and HTTP/3 can.

HPACK

HTTP/2 header compression (RFC 7541) — static table + dynamic table + Huffman. Implemented in HpackEncoder / HpackDecoder / HpackHuffman / HpackDynamicTable / HpackStaticTable.

Flow control

Per-stream and per-connection byte windows (HTTP/2); WINDOW_UPDATE frames as the receiver consumes.

Virtual thread per request

Chappe enforces a strict rule: one virtual thread per connection. No platform pool, no EventLoopGroup, no application-side Selector.

  • Executors.newVirtualThreadPerTaskExecutor() is the only executor used.

  • The Java 25 VM multiplexes virtual threads onto a handful of carrier threads.

  • Application code writes synchronous blocking style: body.asInputStream().readAllBytes() is fine, never a scalability regression.

  • No ThreadLocal — see RequestContext below.

RequestContext and Scoped Values

RequestContext.CURRENT is a ScopedValue<RequestContext> (JEP 506, finalized in Java 25). It carries request attributes, tenant, authenticated user — everything ThreadLocal carried in classic servers, only cleaner:

  • explicit propagation via ScopedValue.where(…​).run(…​);

  • automatic cleanup when the scope ends;

  • virtual-thread compatible without pinning risk.

// Read it from anywhere in the call chain
Request req = RequestContext.currentRequest();

Mount, contextPath, pathInfo

Router#mount(prefix, handler) delegates a whole path prefix, all verbs included. The prefix is stripped before the call:

  • request.path() is relative to the mount (/dashboard),

  • request.contextPath() is the prefix (/admin),

  • request.pathInfo() is an alias of path() for mounts.

Resolution order inside a Router:

  1. exact routes (first match by path + method),

  2. 405 Method Not Allowed if the path matches but the method does not (sets the Allow header),

  3. mounts (first matching prefix, registration order),

  4. notFound (404 fallback).

ServerProvider SPI

ServerProvider is a ServiceLoader service. chappe-core registers ChappeServerProvider via provides/with in its module-info. An alternate implementation (e.g. cassini-jdk-http, which falls back on the JDK HttpServer for tests) can register alongside.

Diagram: concept graph

Diagram