Cette page rassemble les recettes opérationnelles : terminer du TLS, négocier HTTP/2 via ALPN, monter plusieurs sous-applications, servir des fichiers statiques en zero-copy, négocier la compression, et utiliser la CLI chappe serve pour livrer un site sans launcher Java.
TLS / HTTPS
Chappe utilise SSLContext/SSLEngine du JDK. Aucun wrapper, aucune réimplémentation.
import javax.net.ssl.SSLContext;
import io.vidocq.chappe.api.*;
SSLContext sslContext = ...; // chargé depuis un keystore JKS/PKCS12
try (var server = Server.builder()
.port(8443)
.tls(sslContext)
.alpnProtocols("h2", "http/1.1")
.handler(router)
.build()) {
server.start();
Thread.currentThread().join();
}
Points clés :
-
alpnProtocols("h2", "http/1.1")annonce les deux protocoles via ALPN ; HTTP/2 est sélectionné si le client le propose. -
TLS 1.3 et TLS 1.2 sont supportés (politique
SSLContextstandard). -
Le certificat et la clé sont chargés depuis un keystore JDK (
KeyStore.getInstance("PKCS12")). Pas de PEM natif côté Chappe.
HTTP/2
HTTP/2 (RFC 9113) est porté par chappe-http : framing, HPACK (RFC 7541) avec encodage Huffman, flow control, gestion CONTINUATION, lifecycle GOAWAY/SETTINGS. Le module est activé automatiquement quand TLS + ALPN h2 sont configurés.
Sans TLS (h2c, HTTP/2 cleartext), HTTP/1.1 reste actif par défaut. L'`Upgrade: h2c` est négocié via la couche chappe-http.
HTTP/3 / QUIC
HTTP/3 (RFC 9114) est planifié. Le moteur QUIC sous-jacent dépend du JDK (JDK 26+ ou implémentation pure ; voir lien:https://codeberg.org/Vidocq/chappe/blob/main/BUG.md[BUG.md] pour le statut courant).
Virtual hosts
Chappe ne fournit pas d’abstraction « VirtualHost » nominative — préférer un Router dispatcher sur 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();
Pour TLS multi-domaines, fournir un SSLContext configuré avec SNI (le JDK gère la sélection de certificat via X509ExtendedKeyManager#chooseEngineServerAlias).
Fichiers statiques avancés
Fallback chain filesystem → classpath, cache mémoire, compression sidecars, mode SPA :
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) // cache ETag pour ressources classpath
.cacheControl("max-age=3600, public")
.preferPrecompressed(true) // sidecars .br / .gz si Accept-Encoding
.spaFallback("/index.html") // route inconnue → index (SPA)
.build();
Mutuellement exclusifs : spaFallback(path) (200 sur 404) et notFoundFile(path) (404 sur 404). Un seul des deux par builder.
Compression négociée
Le helper Filter.gzip() négocie Accept-Encoding et compresse à la volée :
Router.builder()
.filter(Filter.addHeader("X-Content-Type-Options", "nosniff"))
.filter(Filter.addHeaderIfEnv("STAGING", "true",
"X-Robots-Tag", "noindex, nofollow"))
.filter(Filter.gzip()) // négocie Accept-Encoding
.get("/", handler)
.build();
Skip automatique :
-
le body fait moins de 1 Ko (seuil par défaut) ;
-
la réponse a déjà un
Content-Encoding; -
le
Cache-Control: no-transformest présent ; -
le
Content-Typen’est pas text-like (text/* + application/json + application/xml + application/javascript + +xml + +json).
AcceptEncoding.parse(header) et AcceptEncoding.accepts(header, coding) exposent l’algorithme RFC 9110 §12.5.3 (q-values, wildcard, identity implicite) — utile pour les handlers qui veulent négocier eux-mêmes.
Mount d’une sous-application
Router#mount(prefix, handler) délègue tous les verbes à un sous-handler ; le préfixe est strippé automatiquement :
Router.builder()
.get("/health", _ -> Response.ok("UP")) // route exacte (priorité)
.mount("/app", servletHandler) // ex. Foy
.mount("/api", restHandler) // ex. Cassini
.mount("/static", StaticFileHandler.of(webRoot))
.notFound(_ -> Response.of(StatusCode.NOT_FOUND))
.build();
Côté handler monté :
-
request.path()est relatif au mount (/app/index.jsparrive comme/index.jsp), -
request.contextPath()retourne le préfixe ("/app"), -
request.attribute(key, value)permet de partager des attributs avec l’extension (compat Servlet).
CLI standalone
Pour servir un site statique sans launcher applicatif (cas Docker typique) :
./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 via fichier YAML (subset documenté : maps imbriquées, listes, scalaires, commentaires # ; pas d’anchors ni multilignes) :
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: # actif si l'env var STAGING=true
X-Robots-Tag: "noindex, nofollow"
Les flags CLI ont priorité sur le YAML. Voir Référence — CLI chappe serve pour la liste complète des flags.