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
-
chappe-apihas no internal dependency — it exposes the interfaces. -
chappe-httponly depends onchappe-api. -
chappe-coreorchestrates everything and provides theServerProviderimplementation throughServiceLoader.
Accept loop
-
ChappeServer#start()opens aServerSocketChannel(blocking mode). -
A virtual thread (the "acceptor") loops on
accept(). -
For every socket, a fresh virtual thread is spawned through
Executors.newVirtualThreadPerTaskExecutor(). -
The thread inspects the preamble (
TLS ClientHelloor cleartext HTTP/1.1) then dispatches toHttpConnectionorHttp2Connection.
No application-side Selector. The VM handles multiplexing virtual threads on platform carrier threads.
HTTP/1.1 pipeline — request sequence
Components involved (io.vidocq.chappe.http):
-
HttpRequestParser— parses the request line + headers with zero allocation on the hot path (reusesByteBufferPoolbuffers). -
ChunkedInputStream/FixedLengthInputStream— lazy body reads. -
HttpResponseWriter— coalesced write (status line + headers + first chunk in onewrite()). -
ArrayHeaders— compact storage, case-insensitive lookup with no allocation.
HTTP/2 pipeline — request sequence
Implementation (io.vidocq.chappe.http.h2):
-
Http2FrameReader/Http2FrameWriter— RFC 9113 framing. -
HpackEncoder/HpackDecoder— RFC 7541 (61-entry static table, dynamic table sized bySETTINGS, 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_STREAMhandling.
HTTP/3 pipeline — sketch
HTTP/3 is not active yet. The target pipeline:
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
Design points:
-
No platform thread is dedicated to Chappe — only virtual threads.
-
No pinning — no
synchronizedon the hot path, no blocking JNI.RequestContextusesScopedValue, notThreadLocal. -
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
431or400, orConnection: 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 itsclose_notifyfirst, 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 —
ArrayHeadersreuses a scratch buffer, noHashMapon the hot path. -
Thread-local buffer pool —
ByteBufferPoolshares directByteBufferinstances across virtual threads via the carriers. -
Fast path 200 OK — pre-encoded
HTTP/1.1 200 OKstatus line, direct copy. -
StatusCodeCache—StatusCodeenum cached in an indexedString[].
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.