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 lien:https://codeberg.org/Vidocq/chappe/blob/main/BENCH.md[BENCH.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.

  • Http2SettingsHEADER_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 / Http2ErrorCodeGOAWAY / RST_STREAM handling.

HTTP/3 pipeline — sketch

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

Diagram

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).

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 headersArrayHeaders reuses a scratch buffer, no HashMap on the hot path.

  • Thread-local buffer poolByteBufferPool 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.

  • StatusCodeCacheStatusCode enum cached in an indexed String[].

For measured numbers, see lien:https://codeberg.org/Vidocq/chappe/blob/main/BENCH.md[BENCH.md] and chappe/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.