This page is the canonical Chappe reference: Maven artifacts, exported Java modules, the major public API, the CLI YAML schema, CLI flags, and the pre-compression Maven plugin. For RFC conformance see Concepts and chappe-conformance in the repository.
Maven artifacts
| Artifact | Role |
|---|---|
|
Public API: |
|
HTTP/1.1 + HTTP/2 layer, TLS, |
|
Server engine (virtual threads, protocol detection, lifecycle, |
|
Standalone |
|
Integration tests (183 tests). |
|
HTTP conformance (53 tests against RFC 9110/9112/9113/6455). |
|
Comparative benchmarks (Jetty, Helidon, JDK HttpServer). |
|
Maven plugin: O(1) index + |
|
Usage samples. |
Java modules
| Module | Exports / declarations |
|---|---|
|
|
|
|
|
|
|
|
chappe-api does not opens anything. No module exposes its internals through reflection: every dependency goes through the exports listed above.
Major public API (io.vidocq.chappe.api)
Server
public interface Server extends AutoCloseable {
void start();
void stop();
int port();
static Builder builder();
interface Builder { ... }
}
The builder is the nested Server.Builder interface:
-
port(int) -
host(String) -
handler(Handler) -
backlog(int) -
readTimeout(Duration)/writeTimeout(Duration)/idleTimeout(Duration) -
maxRequestSize(long)/maxHeaderSize(int) -
tls(SSLContext) -
alpnProtocols(String…) -
shutdownGracePeriod(Duration) -
build()returns aServer
The executor is not configurable: every connection is handled on its own virtual thread.
Timeouts
-
readTimeout(default 30 s) bounds a blocking read of the request. -
idleTimeout(default 60 s) bounds inactivity on the read side: a connection that sends nothing for that long is closed. -
writeTimeout(default 30 s) NEW bounds one blocking write of an HTTP/1.1 response: how long the socket may refuse the bytes Chappe is sending, because the client does not read. When it expires, Chappe closes the connection, and the client gets a truncated body. It is not an inactivity bound on the response: nothing is timed while there is nothing to send, so a Server-Sent Events stream with long gaps between events is never cut by it. Until 0.3.0 the value was accepted by the builder and never applied.
Router
Router.builder()
.get(path, handler)
.post(path, handler)
.put(path, handler)
.delete(path, handler)
.patch(path, handler)
.head(path, handler) // also auto for GET
.options(path, handler)
.route(method, path, handler) // explicit HttpMethod
.group(prefix, sub -> sub.filter(...).get(...))
.mount(prefix, handler)
.mount(prefix, handler, stripPrefix) // control prefix stripping
.webSocket(path, webSocketHandler) // RFC 6455 endpoint
.grpc(path, grpcHandler) // gRPC over HTTP/2
.grpcWeb(path, grpcHandler) // gRPC-Web (browser clients)
.filter(filter)
.notFound(handler)
.build();
Behavior:
-
automatic
405 Method Not Allowedwith theAllowheader when the path matches but not the method. -
trailing slash transparent:
/usersand/users/map to the same route. -
percent-decoding on path params:
/users/John%20Doe→pathParam("id") = "John Doe".
Filter
public interface Filter {
Handler apply(Handler next);
static Filter addHeader(String name, String value);
static Filter addHeaderIf(BooleanSupplier when, String name, String value);
static Filter addHeaderIfEnv(String envVar, String expected, String name, String value);
static Filter gzip();
static Filter gzip(int threshold);
static Filter accessLog(); (1)
static Filter accessLog(Consumer<String> sink); (2)
default Filter andThen(Filter next);
}
| 1 | Logs every request to System.out in extended Apache Combined Log Format (see Observability). |
| 2 | Variant with a custom sink (useful to redirect into an application logger). |
BuildInfo
public final class BuildInfo {
public static String version(); // e.g. "0.2.0"
public static String gitCommit(); // e.g. "8d670fb0"
public static String buildTimestamp(); // e.g. "2026-05-09T19:43:04Z"
public static String javaVersion(); // e.g. "25.0.3"
public static String serverHeader(); // e.g. "Chappe/0.2.0+8d670fb0 (2026-05-09T19:43:04Z)"
}
Loaded once at startup from chappe-build.properties at the jar root (generated by Maven resource filtering plus git-commit-id-maven-plugin when building chappe-api). Used automatically by the HTTP layer to inject the Server and X-Chappe-Build headers.
StaticFileHandler
StaticFileHandler.builder()
.addPath(Path) // filesystem (zero-copy sendfile)
.addClasspath(String resourceRoot) // classpath (jar, module)
.cacheInMemory(boolean) // ETag cache for classpath resources
.cacheControl(String)
.preferPrecompressed(boolean) // .br / .gz sidecars when Accept-Encoding allows
.spaFallback(String) // 200 on 404 (SPA mode)
.notFoundFile(String) // 404 on 404 (mutually exclusive)
.indexFile(String) // directory index (default index.html)
.cleanUrls(boolean) // extensionless /admin retried as admin.html
.build();
AcceptEncoding
record Entry(String name, double qvalue) {}
List<Entry> AcceptEncoding.parse(String header);
boolean AcceptEncoding.accepts(String header, String coding);
RFC 9110 §12.5.3 algorithm: q-values, wildcard, implicit identity.
chappe serve CLI configuration
Flags
| Flag | Effect |
|---|---|
|
load a YAML file |
|
override |
|
override |
|
override |
|
override |
|
enable SPA mode (status 200 on 404) |
|
override |
|
enable/disable compression |
|
enable/disable Apache CLF access log on stdout |
|
add a header (repeatable, merged into |
|
help |
Flags override YAML values.
Environment variables
| Variable | Effect |
|---|---|
|
enables the |
|
enables the access log (equivalent to |
YAML schema
server:
port: 8080
bind: 0.0.0.0
static:
root: /var/www/site
fallback: /404.html # status 404
spa-fallback: /index.html # status 200 (mutually exclusive with fallback)
index-files: [index.html]
cache-control: "max-age=3600, public"
gzip: true # negotiated through Accept-Encoding
headers:
always: # injected on every response
X-Content-Type-Options: "nosniff"
Referrer-Policy: "strict-origin-when-cross-origin"
staging: # active when env var STAGING=true
X-Robots-Tag: "noindex, nofollow"
logging:
level: INFO
access-log: false
Documented subset: nested maps, inline lists [a, b] and block - item, string/int/bool scalars, # comments. No anchors, no !! tags, no multilines.
Observability
Build identification headers
Every HTTP response from Chappe carries two headers identifying the exact binary that produced it:
Server: Chappe/0.2.0+8d670fb0 (2026-05-09T19:43:04Z)
X-Chappe-Build: Chappe/0.2.0+8d670fb0 (2026-05-09T19:43:04Z)
Format: Chappe/<Maven version>+<short Git hash>(<build timestamp ISO-8601 UTC>). Read once at startup via BuildInfo (see public API).
-
Serveris the standard header. It can be rewritten by an upstream reverse proxy (NPM, openresty, Cloudflare). -
X-Chappe-Buildmirrors the same value in a non-standard header that proxies usually forward untouched — useful to identify the binary behind a proxy that sets its ownServer.
Idempotency: if the application has already set either header on the Response, Chappe will not overwrite it.
Access log
Enabled via --access-log (CLI), CHAPPE_ACCESS_LOG=true (env), or logging.access-log: true (YAML). CLI overrides env, env overrides YAML.
Format: extended Apache Combined Log Format on System.out (captured by Docker/Portainer):
1.2.3.4 - yann.blazart@gmail.com [09/May/2026:18:50:54 +0000] "GET /a.png HTTP/1.1" 200 877719 12ms
Field resolution:
-
Client IP:
X-Forwarded-For(first hop) →X-Real-IP→Request.remoteAddress()→-. -
User:
X-Forwarded-User→Gap-Auth(oauth2-proxy) →-. -
Size:
Response.body().contentLength()(or-for streamed/chunked bodies). -
Duration: nanoseconds measured by the outermost filter wrapper, captures the final status even if the handler throws.
Available as a public API filter via Filter.accessLog() / Filter.accessLog(Consumer<String>); the CLI launcher applies it as the outermost wrapper.
Maven plugin: pre-compression
<plugin>
<groupId>io.vidocq.chappe</groupId>
<artifactId>chappe-static-index-maven-plugin</artifactId>
<executions>
<execution>
<goals><goal>index</goal></goals>
<configuration>
<compress>gzip</compress> <!-- default: none -->
<compressThreshold>1024</compressThreshold>
</configuration>
</execution>
</executions>
</plugin>
Generates:
-
META-INF/chappe-static-index.properties— O(1) index (path → size + ETag). -
.gzsidecars for files above the threshold.
Served zero-copy by StaticFileHandler.preferPrecompressed(true).
HTTP capabilities
| Spec | Status | Notes |
|---|---|---|
RFC 9110 (HTTP Semantics) |
OK |
Date, HEAD/204/304, 405+Allow, Host, 100-continue |
RFC 9112 (HTTP/1.1) |
OK |
request parsing, chunked (request and response trailers, §7.1.2), keep-alive, pipelining, lingering close (§9.6, plaintext and TLS) NEW |
RFC 9113 (HTTP/2) |
OK |
framing, flow control, lifecycle, GOAWAY, SETTINGS, CONTINUATION |
RFC 7541 (HPACK) |
OK |
static table, dynamic table, Huffman encode/decode |
RFC 6455 (WebSocket) |
OK |
handshake, TEXT/BINARY framing, fragmentation, UTF-8 validation, client masking, close handshake, auto-PONG — over HTTP/1.1 |
gRPC over HTTP/2 |
OK |
transport framing (5-byte prefix), trailers, unary / server-stream / client-stream / bidi, gRPC-Web via |
RFC 9114 (HTTP/3) |
planned |
QUIC via the JDK, targeting JDK 26+ — see ROADMAP.md |
RFC 8441 (WebSocket H2) |
planned |
WebSocket runs over HTTP/1.1 today |
Compatibility
-
Java: 25 (LTS), no preview flag required (
ScopedValueis final since JEP 506). -
Maven: 3.9.16 (POM
modelVersion 4.0.0). -
Java Modules strict: no classpath, no
--add-opens.
Bugs and benchmarks
-
BUG.md — reproducible bugs, status, minimal repro.
-
BENCHMARKS.md — performance numbers, hardware, JVM, command.
Source
All code and roadmap live at https://codefloe.com/Vidocq/chappe.