Skip to content

json-tls coverage for flagship/emerging entries on the H1 table - #1273

Open
MDA2AV wants to merge 15 commits into
mainfrom
feat/json-tls-coverage
Open

json-tls coverage for flagship/emerging entries on the H1 table#1273
MDA2AV wants to merge 15 commits into
mainfrom
feat/json-tls-coverage

Conversation

@MDA2AV

@MDA2AV MDA2AV commented Aug 22, 2026

Copy link
Copy Markdown
Owner

32 flagship/emerging entries on the H1 leaderboard have no json-tls number. This subscribes 27 of them; the other five cannot, each for a reason established by running it.

Every entry serves the same app on 8081 that it serves on 8080 — no second copy of the routes — and every listener is guarded on the PEMs where the harness mounts them conditionally.

Implemented

entry how
koa app.callback() behind node:https
hono-node the same app.fetch, via the adapter's createServer
node-h3 srvx takes the PEM paths and builds the https server itself
fastify binds one instance per server, so routes moved into build(); both ports register from it
nestjs the adapter's instance is the Express app Nest wired the controllers onto
rocket Rocket's own tls feature, second instance off Config::figment()
axum the same Router over axum-server's rustls acceptor; ring, so no C toolchain
warp the same handler behind warp-tls on a forked thread
drogon addListener on 8081 with TLS
plug-cowboy a second Plug.Cowboy listener on the same router
phoenix-bandit a second Bandit listener in front of the same endpoint plug
ring, ring-jetty9-adapter both Jetty adapters take a KeyStore instance, merged into the existing run-jetty opts
http4k an https listener on the same Undertow builder
http4s Ember binds one port per builder, so server() is the single definition and the two run parTupled
micronaut dual-protocol with ssl.port 8081
humming-bird a second Application over the same router, ALPN pinned to http/1.1
sisk Cadente applies TLS per host, not per port, so TLS gets its own server and BuildRouter() is the single definition
slim, laravel, frankenphp-trueasync FrankenPHP: a TLS site on 8081 with the same worker
hyperf a second Hyperf HTTP server behind Swoole's TLS
lapis an ssl server block running the same lapis app
mojolicious a second listen URL on the same Mojo::Server::Prefork
h2o-mruby an 8081 listener in the existing cert-conditional block
slimeweb a second serve() as its own process — pools are loop-affine
php already subscribed; needs only a run

Cannot have json-tls

  • http-kit — 2.9.0-beta4 has no TLS at all. No ssl/tls anywhere in server.clj, none in its documented options.
  • robyn — 0.83.0 likewise. start() takes host, port, _check_port, and two timeouts; no ssl/certificate/keyfile reference in the package.
  • sanic — 25.3.0 binds and accepts a TLS listener then never sends a ServerHello, at any worker count. A second plaintext listener answers fine, so it is TLS specifically; only single_process=True serves it, which would pin the entry to one core. A prebuilt SSLContext is no way round it either — the manager spawns and SSLContext cannot be pickled.
  • symfony-spawn-franken — does not build on main: composer exits 127 in its base image. Its json-tls could not be validated, so it is left out rather than shipped unverified.
  • rage — skipped at the maintainer's request.

Each reason is recorded in the entry itself, so nobody re-derives it.

Two runtime fixes picked up on the way

Both are pre-existing breakage that stopped the entry from starting at all, found while getting it to a validating state:

  • h2o-mruby — the build stage installs libbrotli-dev so h2o links brotli, but the runtime image carried only libssl3 and jq; it died on libbrotlidec.so.1.
  • frankenphp-trueasync — the frankenphp binary links libwebp, which its base image does not carry; it died on libwebpdemux.so.2. Found the full set with ldd rather than one failure at a time.

Both entries clone/pull :latest upstream, so this arrives with whatever was last pushed.

validate

All 27 pass, with two exceptions that are pre-existing and untouched here — both are entries that assemble a static cache in the entry and never revalidate against disk, which the staleness probe now catches:

  • humming-bird 46/1 — loads every static file into a [String: StaticFile] map at startup
  • frankenphp-trueasync 55/1 — worker.php scandirs /data/static and file_get_contents each file

Both fail the same way on main; each needs its own fix.

nestjs also carries 5 pre-existing POST fragmentation failures (main fails the identical five).

Benchmarking is a separate PR once this lands.

🤖 Generated with Claude Code

MDA2AV added 3 commits August 22, 2026 18:28
Each entry serves the JSON workload but had no TLS listener, so json-tls
scored 0. All four now bind 8081 with the same app that serves 8080:

  koa        app.callback() behind node:https
  hono-node  the same app.fetch, via the adapter's createServer option
  node-h3    srvx takes the PEM paths and builds the https server itself
  fastify    Fastify binds one instance per server, so routes moved into
             build(); both ports register from that one definition

The certs are only mounted for the TLS profiles, so every listener is
guarded on their presence and stays closed otherwise.

validate: koa 37/0, fastify 47/0, hono-node 37/0, node-h3 37/0
Both serve the JSON workload with no TLS listener, so json-tls scored 0.

rocket  enables its own tls feature and configures a second instance on
        8081. Routes moved into build() so the two ports mount the same
        set; both configs start from Config::figment(), which keeps the
        ROCKET_* env from the Dockerfile applying to each.
axum    serves the same Router over axum-server's rustls acceptor, the
        one axum's own tls-rustls example uses. ring rather than
        aws-lc-rs, so the build image needs no C toolchain.

Guarded on the PEMs, which the harness only mounts for the TLS profiles.

validate: rocket 34/0, axum 37/0
plug-cowboy     a second Plug.Cowboy listener in front of the same router.
                Plug.Cowboy derives its ref from plug + scheme, so the two
                children do not collide.
phoenix-bandit  the endpoint's https: config already holds 8443 for the h2
                profiles and Phoenix binds one https listener per endpoint,
                so json-tls gets a second Bandit listener in front of the
                same endpoint plug -- the identical pipeline, not a copy.

Both guarded on the PEMs, which the harness only mounts for TLS profiles.

validate: plug-cowboy 37/0, phoenix-bandit 70/0 (the latter needs #1272,
which fixes a probe bug that fails any container running as non-root)
@MDA2AV
MDA2AV force-pushed the feat/json-tls-coverage branch from aaad428 to 5fd521b Compare August 22, 2026 17:28
MDA2AV added 6 commits August 22, 2026 18:39
Both Jetty adapters take a KeyStore instance for :keystore, so the mounted
PEMs are converted in-process and merged into the existing run-jetty opts as
:ssl? / :ssl-port 8081 -- one server, same handler on both ports.

The conversion uses CertificateFactory + KeyFactory rather than
java.security.PEMDecoder: PEMDecoder is still a preview API on the JDK 25 in
the clojure base image, so it would need --enable-preview to load.

Verified against each adapter's source rather than assumed: ring sets
.setKeyStore for a non-string :keystore and builds its SslConnectionFactory
with "http/1.1", and sunng's does the same with h2? defaulting off.

http-kit is deliberately absent -- 2.9.0-beta4's run-server has no TLS
support at all (no ssl/tls anywhere in server.clj, none in its documented
options), so it cannot serve json-tls without a hand-rolled TLS layer.

validate: ring 52/0, ring-jetty9-adapter 46/0
http4k    an https listener on the same Undertow builder, so 8081 runs the
          identical handler chain as 8080
http4s    Ember binds one port per builder, so server() is the single
          definition and the two resources run parTupled
micronaut dual-protocol with ssl.port 8081; the listener stays Micronaut's,
          only the key material is prepared before it starts

All three want a KeyStore/SSLContext and the harness mounts PEMs, so each
converts the pair in-process with CertificateFactory + KeyFactory rather
than java.security.PEMDecoder, which is still a preview API on these JDKs.

validate: http4k 37/0, http4s 37/0, micronaut 37/0
tools-deps writes .cpcache into the framework directory on any classpath
resolution, including from inside a container where the files land root
owned. Two of them were picked up by a git add -A.
nestjs  the adapter's instance is the very Express app Nest wired the
        controllers and compression onto, so putting it behind node:https
        serves the same pipeline rather than a second copy.
sisk    Cadente applies the certificate to the whole listening host rather
        than per port -- setting SslOptions, or calling UseSsl, on the
        plaintext host turns 8080 into HTTPS and it starts 301-ing plaintext
        to https. Verified by running it: the log lists both ports as
        https:// and curl gets a redirect. Two ListeningHosts do not work
        either, since a Router binds to one host and sharing an instance
        leaves both serving nothing. So TLS runs as its own server with its
        own Router, and BuildRouter() is now the single definition both are
        built from.

validate: sisk 47/0, nestjs 32 passed / 5 failed.

The nestjs failures are the POST TCP-fragmentation checks and are
pre-existing: main without this change fails the same five (29/5, the
three extra passes here being the json-tls checks). Not addressed here.
slim runs on FrankenPHP, so json-tls is an 8081 site block with the same
php_server as 8080 -- TLS terminated by the server the entry already uses.

The site cannot be declared unconditionally: Caddy refuses to start when a
tls directive points at files that are not there, and the harness mounts
/certs only for the TLS profiles. Caught by running it -- the container
exited outright with no certs mounted, which would have taken out every
non-TLS profile. So the block ships as a separate file the entrypoint drops
into an imported directory only when the certificate exists. Verified both
ways: 8080 serves with no certs mounted, both ports serve with them.

sanic keeps no json-tls, but the comment now records what was actually
found: 25.3.0 binds and accepts on a TLS listener and then never sends a
ServerHello, at any worker count, whether it is the only listener or a
second prepare(). A second plaintext prepare() answers fine, so it is TLS
specifically, and single_process=True serves it -- which would pin the entry
to one core. A prebuilt SSLContext is not a way around it either, since the
worker manager spawns and an SSLContext cannot be pickled.

validate: slim 37/0
A second Hyperf HTTP server on 8081 behind Swoole's own TLS. Three things
the obvious version gets wrong, each found by running it:

- Swoole port options go under 'settings'; 'options' is Hyperf's own and the
  ssl_cert_file/ssl_key_file put there never reach the port, which aborts
  startup with "require ssl_cert_file and ssl_key_file options".
- Hyperf scopes routes to a server by name, so the TLS server needs the same
  set registered against it. routes.php now registers one closure twice.
- Hyperf keys the ON_REQUEST callback by class, so pointing both servers at
  Hyperf\HttpServer\Server makes the second replace the first -- it logs
  "http will be replaced by http-tls" and then neither port answers. The
  TLS server gets its own Server subclass so it has its own router.

The listener is only added when the certificate is mounted, which the
harness does for the TLS profiles only; Swoole aborts at startup if a
listener names certificate files that are not there.

validate: hyperf 53/0
@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

👋 Heads up! This PR modifies the following frameworks:

MDA2AV added 6 commits August 22, 2026 19:16
Octane's Caddyfile carries a {$CADDY_EXTRA_CONFIG} placeholder that Octane
itself never sets, so Caddy resolves it from the process environment. The
json-tls site goes there -- same worker, same app as 8080, only the listener
differs -- rather than forking Octane's Caddyfile through --caddyfile.

Two things worth recording, both found by running it:

- The site cannot be declared unconditionally. Caddy refuses to start when a
  tls directive points at files that are not there, and the harness mounts
  /certs only for the TLS profiles, so the entrypoint sets the variable only
  when the certificate exists. Verified 8080 still serves with none mounted.
- root * "{$APP_PUBLIC_PATH}" does not work inside the extra config. Octane
  substitutes that placeholder into its own template rather than exporting
  it, so in the extra config it resolves empty: TLS handshakes fine and then
  the request hangs with no response at all. The path is written out.

validate: laravel 37/0
drogon       addListener on 8081 with TLS, same handlers as 8080
mojolicious  a second listen URL on the same Mojo::Server::Prefork
lapis        an ssl server block running the same lapis app

All three abort at startup on a tls/ssl directive naming certificate files
that are not there, and the harness mounts /certs only for the TLS profiles,
so each is conditional -- drogon and mojolicious check in code, and nginx
cannot, so lapis's block ships as a file the entrypoint drops into an
included directory. Each verified running both ways: 8080 serves with no
certs mounted, both ports serve with them.

validate: drogon 37/0, lapis 37/0, mojolicious 37/0
An 8081 listener inside the existing cert-conditional block. 8443 advertises
h2 through ALPN, so it cannot double as the HTTP/1.1 listener.

Also adds libbrotli1 to the runtime image. The build stage installs
libbrotli-dev, so h2o links against brotli, but the runtime stage only
carried libssl3 and jq -- the container died on startup with "error while
loading shared libraries: libbrotlidec.so.1". Pre-existing and unrelated to
json-tls: the entry builds h2o from a --depth 1 clone of upstream master, so
it picks up whatever that links against today.

validate: h2o-mruby 53/0 (the clang build of mruby SIGSEGVs at random on this
machine and needed several attempts -- see the toolchain note; the tree
itself builds clean)
warp          the same handler behind warp-tls on a forked thread; Debian
              ships libghc-warp-tls-dev, so it is one more build dep
humming-bird  a second Application over the same router, using
              Hummingbird's own TLS server. One Application binds one
              address, so the TLS listener cannot share the first. ALPN is
              pinned to http/1.1 so json-tls does not fall into h2.

Both guarded on the PEMs, which the harness only mounts for TLS profiles.

validate: warp 37/0; humming-bird 46 passed / 1 failed.

The humming-bird failure is the static staleness probe and is pre-existing:
the entry loads every static file into an in-memory [String: StaticFile] map
at startup, .br and .gz buffers included, so it never revalidates against
disk. That is a cache assembled in the entry, which both the standard and
tuned rules exclude, and it fails the same way on main. Untouched here --
this change only adds a listener -- but it needs its own fix.
slimeweb's serve() makes its own listener TLS rather than adding one, so the
second port needs a second serve(). It cannot share the process: asyncpg
pools are bound to the loop that created them, and @app.start() runs per
serve, so the second pool lands on the other loop and /async-db dies with
"got Future attached to a different loop". Measured, not guessed -- the
threaded version validated 51 passed / 6 failed against a 51/0 baseline.

So the TLS listener runs as its own process, started without DATABASE_URL so
it creates no pool at all. 8081 only carries json-tls, which never touches
the database, and the connection budget is untouched -- the harness runs
Postgres with max_connections=256 and this pool already asks for that many.

robyn keeps no json-tls: 0.83.0 has no TLS support at all. start() takes
host, port, _check_port, client_timeout and keep_alive_timeout, and there is
no ssl/certificate/keyfile reference anywhere in the package.

validate: slimeweb 54/0 (51 baseline + the three json-tls checks)
An :8081 site alongside the existing :8443, same worker as :8080, with the
ALPN pinned to http/1.1 so json-tls does not fall into h2. /certs is mounted
for every profile (scripts/lib/framework.sh), which is why :8443 is already
unconditional and this can be too.

Also adds libwebpdemux2 and libwebpmux3. The frankenphp binary in
trueasync/php-true-async:latest-frankenphp links libwebp but the image does
not carry it, so the container exited at startup with "error while loading
shared libraries: libwebpdemux.so.2" -- the entry could not run at all.
Found the full set with ldd on the binary rather than one failure at a time.
The tag is :latest, so this arrives with whatever upstream last pushed.

validate: 55 passed / 1 failed.

The failure is the static staleness probe, and it is pre-existing: worker.php
scandirs /data/static at startup and file_get_contents each file, .br and .gz
included, into $staticFiles. That is a cache assembled in the entry, which
the rules exclude, and it never revalidates against disk. Untouched here.

symfony-spawn-franken is deliberately absent: it does not build on main
either -- composer exits 127 in the same base image -- so its json-tls could
not be validated.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant