This page describes what Chappe does when a request arrives: how the connection is accepted, how the TCP/TLS preamble is inspected, how HTTP/1.1 and HTTP/2 are parsed, how the Router is invoked, how the response is written. No performance numbers here — see BENCHMARKS.md.

Layered architecture

Diagram
  • chappe-api has no internal dependency — it exposes the interfaces.

  • chappe-http only depends on chappe-api.

  • chappe-core orchestrates everything and provides the ServerProvider implementation through ServiceLoader.

Accept loop

  1. ChappeServer#start() opens a ServerSocketChannel (blocking mode).

  2. A virtual thread (the "acceptor") loops on accept().

  3. For every socket, a fresh virtual thread is spawned through Executors.newVirtualThreadPerTaskExecutor().

  4. The thread inspects the preamble (TLS ClientHello or cleartext HTTP/1.1) then dispatches to HttpConnection or Http2Connection.

No application-side Selector. The VM handles multiplexing virtual threads on platform carrier threads.

Protocol detection

Diagram

HTTP/1.1 pipeline — request sequence

Diagram

Components involved (io.vidocq.chappe.http):

  • HttpRequestParser — parses the request line + headers with zero allocation on the hot path (reuses ByteBufferPool buffers).

  • ChunkedInputStream / FixedLengthInputStream — lazy body reads.

  • HttpResponseWriter — coalesced write (status line + headers + first chunk in one write()).

  • ArrayHeaders — compact storage, case-insensitive lookup with no allocation.

HTTP/2 pipeline — request sequence

Diagram

Implementation (io.vidocq.chappe.http.h2):

  • Http2FrameReader / Http2FrameWriter — RFC 9113 framing.

  • HpackEncoder / HpackDecoder — RFC 7541 (61-entry static table, dynamic table sized by SETTINGS, Huffman for short values).

  • HpackHuffman — statically generated encode/decode tables.

  • Http2Settings — HEADER_TABLE_SIZE, MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, MAX_FRAME_SIZE, MAX_HEADER_LIST_SIZE.

  • Http2Stream — FSM state (idle → open → half-closed local/remote → closed).

  • Http2ConnectionException / Http2ErrorCode — GOAWAY / RST_STREAM handling.

HTTP/3 pipeline — sketch

HTTP/3 is not active yet. The target pipeline:

Diagram

The QUIC transport is planned to come from the JDK (targeting JDK 26+ — see ROADMAP.md); no in-house pure-Java QUIC engine is planned, and Chappe ships no QUIC code today.

Threading model

Diagram

Design points:

  • No platform thread is dedicated to Chappe — only virtual threads.

  • No pinning — no synchronized on the hot path, no blocking JNI. RequestContext uses ScopedValue, not ThreadLocal.

  • Lazy body — the body is only read when req.body().asInputStream() is called. If a handler returns without consuming, the body is drained cleanly before the next request (HTTP/1.1 keep-alive) or the stream is reset (HTTP/2).

  • Lingering close NEW — when an HTTP/1.1 connection ends after a response (an error such as 431 or 400, or Connection: close), Chappe closes it in stages, as RFC 9112 §9.6 describes. First it closes its write side, so the response is followed by a FIN. Then it reads and discards what the client still sends, for at most 2 s or 1 MiB. Then it closes. A plain close with request bytes still unread makes the TCP stack send a reset, and the reset destroys the response before the client reads it. Over TLS, Chappe sends its close_notify first, then half-closes and drains the ciphertext without decrypting it. A request body whose read failed (bad chunked framing, a body cut short) closes the connection the same way, without ever parsing another request from it; only a channel already closed, for instance by an interrupted read, closes at once.

Zero-allocation tricks on the hot path

Documented in chappe/OPTIMS.md:

  • Write coalescing — status line + headers + first chunk emitted in a single write().

  • Zero-alloc headers — ArrayHeaders reuses a scratch buffer, no HashMap on the hot path.

  • Thread-local buffer pool — ByteBufferPool shares direct ByteBuffer instances across virtual threads via the carriers.

  • Fast path 200 OK — pre-encoded HTTP/1.1 200 OK status line, direct copy.

  • StatusCodeCache — StatusCode enum cached in an indexed String[].

For measured numbers, see BENCHMARKS.md.

O(1) static index

StaticFileHandler loads a META-INF/chappe-static-index.properties index (produced by chappe-static-index-maven-plugin at build time). O(1) path lookup, no filesystem scan at runtime. .gz sidecars (and .br if present) are flagged in the index — preferPrecompressed(true) serves them zero-copy when Accept-Encoding allows.