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