This page collects the operational recipes: terminating TLS, negotiating HTTP/2 via ALPN, mounting several sub-applications, serving static files with zero-copy, negotiating compression, and running the chappe serve CLI to ship a site without a Java launcher.
TLS / HTTPS
Chappe uses the JDK’s SSLContext/SSLEngine directly. No wrapper, no reimplementation.
import javax.net.ssl.SSLContext;
import io.vidocq.chappe.api.*;
SSLContext sslContext = ...; // loaded from a JKS / PKCS12 keystore
try (var server = Server.builder()
.port(8443)
.tls(sslContext)
.alpnProtocols("h2", "http/1.1")
.handler(router)
.build()) {
server.start();
Thread.currentThread().join();
}
Key points:
-
alpnProtocols("h2", "http/1.1")advertises both protocols via ALPN; HTTP/2 is selected when the client offers it. -
TLS 1.3 and TLS 1.2 are supported (standard
SSLContextpolicy). -
Certificate and key are loaded from a JDK keystore (
KeyStore.getInstance("PKCS12")). No native PEM loader on the Chappe side.
HTTP/2
HTTP/2 (RFC 9113) is carried by chappe-http: framing, HPACK (RFC 7541) with Huffman coding, flow control, CONTINUATION handling, GOAWAY/SETTINGS lifecycle. The module turns on automatically when TLS + ALPN h2 are configured.
Without TLS (h2c, HTTP/2 cleartext), HTTP/1.1 stays the default. The Upgrade: h2c handshake is negotiated by the chappe-http layer.
HTTP/3 / QUIC
HTTP/3 (RFC 9114) is planned. The underlying QUIC engine depends on the JDK (JDK 26+ or a pure implementation; see lien:https://codeberg.org/Vidocq/chappe/blob/main/BUG.md[BUG.md] for the current status).
Virtual hosts
Chappe does not ship a named "VirtualHost" abstraction — prefer a Router that dispatches on Host/:authority:
Handler hostA = Router.builder().get("/", _ -> Response.ok("site A")).build();
Handler hostB = Router.builder().get("/", _ -> Response.ok("site B")).build();
Handler dispatcher = req -> switch (req.header("Host").orElse("")) {
case "a.example.com" -> hostA.handle(req);
case "b.example.com" -> hostB.handle(req);
default -> Response.of(StatusCode.NOT_FOUND);
};
Server.builder().port(8080).handler(dispatcher).build().start();
For multi-domain TLS, hand in an SSLContext configured with SNI (the JDK picks the certificate via X509ExtendedKeyManager#chooseEngineServerAlias).
Advanced static file serving
Filesystem → classpath fallback chain, in-memory cache, sidecar compression, SPA mode:
StaticFileHandler.builder()
.addPath(Path.of("./public")) // 1. filesystem (zero-copy sendfile)
.addClasspath("static") // 2. classpath (jar, module)
.addClasspath("META-INF/resources") // 3. webjars / Servlet web fragments
.cacheInMemory(true) // ETag cache for classpath
.cacheControl("max-age=3600, public")
.preferPrecompressed(true) // .br / .gz sidecars when Accept-Encoding allows
.spaFallback("/index.html") // unknown route → index (SPA)
.build();
Mutually exclusive: spaFallback(path) (200 on 404) and notFoundFile(path) (404 on 404). Pick one per builder.
Negotiated compression
The Filter.gzip() helper negotiates Accept-Encoding and compresses on the fly:
Router.builder()
.filter(Filter.addHeader("X-Content-Type-Options", "nosniff"))
.filter(Filter.addHeaderIfEnv("STAGING", "true",
"X-Robots-Tag", "noindex, nofollow"))
.filter(Filter.gzip()) // negotiates Accept-Encoding
.get("/", handler)
.build();
Auto-skip rules:
-
the body is below 1 KiB (default threshold);
-
the response already carries a
Content-Encoding; -
Cache-Control: no-transformis present; -
the
Content-Typeis not text-like (text/* + application/json + application/xml + application/javascript + +xml + +json).
AcceptEncoding.parse(header) and AcceptEncoding.accepts(header, coding) expose the RFC 9110 §12.5.3 algorithm (q-values, wildcard, implicit identity) — handy for handlers that want to negotiate themselves.
Mounting a sub-application
Router#mount(prefix, handler) delegates all verbs to a sub-handler; the prefix is stripped automatically:
Router.builder()
.get("/health", _ -> Response.ok("UP")) // exact route (priority)
.mount("/app", servletHandler) // e.g. Foy
.mount("/api", restHandler) // e.g. Cassini
.mount("/static", StaticFileHandler.of(webRoot))
.notFound(_ -> Response.of(StatusCode.NOT_FOUND))
.build();
Inside the mounted handler:
-
request.path()is relative to the mount (/app/index.jsparrives as/index.jsp), -
request.contextPath()returns the prefix ("/app"), -
request.attribute(key, value)shares attributes with the extension (Servlet-compat).
Standalone CLI
To serve a static site without an application launcher (typical Docker case):
./mvnw -ntp -pl chappe-cli -am package
java \
-jar chappe-cli/target/chappe-cli-*-shaded.jar \
serve --root /var/www/site --port 8080 --gzip
Configuration through a YAML file (documented subset: nested maps, lists, scalars, # comments; no anchors, no multilines):
server:
port: 8080
bind: 0.0.0.0
static:
root: /var/www/site
spa-fallback: /index.html
cache-control: "max-age=3600, public"
gzip: true
headers:
always:
X-Content-Type-Options: "nosniff"
Referrer-Policy: "strict-origin-when-cross-origin"
staging: # active when env var STAGING=true
X-Robots-Tag: "noindex, nofollow"
CLI flags override YAML values. See Reference — chappe serve CLI for the full flag list.