realm-server: opt-in HTTP/2 for faster local indexing#4787
Conversation
Heavy aggregator-card renders (cohort, dashboards) fan out 80+ federated- search requests inside one Chromium tab during prerender. Chrome's HTTP/1.1 6-per-origin connection ceiling serializes them and turns a single render into minutes. HTTP/2 multiplexes the same requests over one connection. The realm-server now optionally serves HTTPS+HTTP/2 on a sibling port (4203 alongside 4201; 4204 alongside 4202 for test-realms) when a dev cert is provisioned. The h2 origin is an alias — incoming Host headers get rewritten to the canonical serverURL so URL-keyed realm lookup matches identically on both ports. Opt-in via `mise run infra:ensure-dev-cert`, which uses mkcert to generate a leaf cert in ~/.local/share/boxel/dev-certs/. Absent the cert, env-vars.sh leaves the TLS env vars unset and realm-server stays HTTP-only — CI and the software-factory hermetic harness are unchanged. When the cert is present, the prerender Chromium and test-realms indexer automatically use HTTP/2: page-pool injects window.__realmH2OriginMappings__ before every navigation, the host's VirtualNetwork rewrites outgoing realm fetches to the h2 origin, and puppeteer launches with --ignore-certificate-errors so the self-signed cert is accepted. This better mirrors hosted environments, which already speak HTTP/2 in front of the realm-server. Devs can additionally point their own browser at the h2 alias for the same speed-up; that requires a one-time `mkcert -install` for system trust. README's new "HTTP/2 dev access" section documents the opt-in flow, what auto-uses h2, what stays on http, and how to verify. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 67aea3096e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The task is wired as a hard dependency of services/realm-server, so exiting 1 on missing mkcert blocked the documented HTTP-only fallback path. env-vars.sh already leaves the TLS env vars unset when no cert files exist, so a missing mkcert should be a soft warning that lets the realm-server keep booting on HTTP/1.1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Preview deploymentsHost Test Results 1 files 1 suites 2h 3m 16s ⏱️ Results for commit e8411db. Realm Server Test Results 1 files ±0 1 suites ±0 11m 21s ⏱️ -13s Results for commit e8411db. ± Comparison against earlier commit e013c1b. |
There was a problem hiding this comment.
Pull request overview
Adds an opt-in HTTPS/HTTP/2 “alias” listener for the realm-server in local dev to speed up heavy prerender/indexing workloads by avoiding Chrome/Chromium’s HTTP/1.1 per-origin connection limits, and wires prerender/browser fetches through the alias via VirtualNetwork URL mappings.
Changes:
- Add an optional
http2.createSecureServer({ allowHTTP1: true })listener and Host-header canonicalization middleware in@cardstack/realm-server. - Inject
window.__realmH2OriginMappings__into prerender Chromium pages and apply mappings in the host app’sNetworkServiceVirtualNetwork. - Add a
misetask to provision a local dev cert viamkcert, and document the workflow in the root README.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents new HTTP/2 dev access (ports, setup, verification). |
| packages/realm-server/server.ts | Adds HTTP/2 secure listener support and Host rewrite middleware. |
| packages/realm-server/main.ts | Starts/stops the optional TLS/H2 listener alongside the HTTP listener. |
| packages/realm-server/prerender/page-pool.ts | Injects prerender globals, including optional H2 origin mappings, into new pages. |
| packages/realm-server/prerender/browser-manager.ts | Adds --ignore-certificate-errors when H2 rerouting is enabled. |
| packages/realm-server/lib/dev-service-registry.ts | Adjusts server typing imports (net.Server/AddressInfo). |
| packages/host/app/services/network.ts | Installs H2 origin mappings into VirtualNetwork when injected by prerender. |
| mise-tasks/lib/env-vars.sh | Exposes TLS cert/key paths + mapping env vars when dev cert files exist. |
| mise-tasks/infra/ensure-dev-cert | New task to provision a local cert/key pair using mkcert. |
| mise-tasks/services/realm-server | Adds dev-cert provisioning task as a dependency. |
| mise-tasks/services/realm-server-base | Adds dev-cert provisioning task as a dependency. |
| mise-tasks/services/test-realms | Adds dev-cert provisioning dependency and passes TLS port for test realms. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Parse Host via URL so bracketed IPv6 authorities (`[::1]:4203`) are handled correctly. String-splitting on ':' was breaking those. - When the canonical serverURL has no explicit port (env-mode under Traefik routing), don't append a default :80/:443 to subdomain rewrites. The realm-server's wildcard subdomain mounts expect the same port-less form Traefik forwards. - Drop the stale `packages/realm-server/README.md` references in code comments — the HTTP/2 dev access docs live in the repo-root README. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lukemelia
left a comment
There was a problem hiding this comment.
Maybe we should discuss this tradeoff? It seems simpler to me to move everything to HTTP/2 and solve the local cert issue rather than maintain both.
I was originally going to go down that route but it was a munch bigger change as it means we use https for everything local--so there is a migration, and i didn't want to force everyone to use http/2. but i'm totally fine with that path if others are ok with it. |
| let tlsPort = process.env.REALM_SERVER_TLS_PORT | ||
| ? Number(process.env.REALM_SERVER_TLS_PORT) | ||
| : undefined; |
There was a problem hiding this comment.
[Claude Code] Fixed in e8411db — main.ts now validates that REALM_SERVER_TLS_PORT is a positive integer in TCP-port range; a non-numeric or out-of-range value logs a warning and skips the alias listener instead of crashing the server.
| let certFile = process.env[TLS_CERT_FILE_ENV]; | ||
| let keyFile = process.env[TLS_KEY_FILE_ENV]; | ||
| if (!certFile || !keyFile) { | ||
| return undefined; | ||
| } | ||
| let instance = http2.createSecureServer( | ||
| { | ||
| cert: readFileSync(certFile), | ||
| key: readFileSync(keyFile), | ||
| allowHTTP1: true, | ||
| }, |
There was a problem hiding this comment.
[Claude Code] Fixed in e8411db — wrapped both the readFileSync calls and http2.createSecureServer in listenSecure with try/catch. A missing/unreadable/malformed cert now logs a warning and returns undefined, so realm-server keeps booting on HTTP/1.1 only. Failure is observable in the log but no longer load-bearing.
| // When the realm-server is exposing an HTTP/2 alias for indexing | ||
| // fan-outs, the host's VirtualNetwork re-routes per-page fetches | ||
| // inside Chromium to the h2 listener. The cert is a local mkcert | ||
| // leaf, which puppeteer's bundled Chromium doesn't trust by | ||
| // default, so we skip the check here. Safe for prerender: the | ||
| // origins are fixed by REALM_H2_ORIGIN_MAPPINGS and the connection | ||
| // is loopback-only. | ||
| if (process.env.REALM_H2_ORIGIN_MAPPINGS) { | ||
| launchArgs.push('--ignore-certificate-errors'); | ||
| } |
There was a problem hiding this comment.
[Claude Code] Fixed in e8411db — added allH2DestinationsAreLoopback() that parses the env JSON and verifies every mapping destination is https: and a loopback hostname (localhost, *.localhost, 127.0.0.1, ::1). --ignore-certificate-errors is now only appended when that check passes; an env var with an off-loopback destination leaves Chromium's default trust policy in effect.
| private installH2OriginMappings(virtualNetwork: VirtualNetwork) { | ||
| let h2MappingsRaw = ( | ||
| globalThis as unknown as { __realmH2OriginMappings__?: string } | ||
| ).__realmH2OriginMappings__; | ||
| if (!h2MappingsRaw) { | ||
| return; | ||
| } | ||
| let pairs: Array<{ from?: string; to?: string }>; | ||
| try { | ||
| let parsed = JSON.parse(h2MappingsRaw); | ||
| pairs = Array.isArray(parsed) ? parsed : []; | ||
| } catch { | ||
| return; | ||
| } | ||
| for (let { from, to } of pairs) { | ||
| if (!from || !to) continue; | ||
| let fromUrl: URL; | ||
| let toUrl: URL; | ||
| try { | ||
| fromUrl = new URL(from); | ||
| toUrl = new URL(to); | ||
| } catch { | ||
| continue; | ||
| } | ||
| if (fromUrl.origin === toUrl.origin) continue; | ||
| virtualNetwork.addURLMapping(fromUrl, toUrl); | ||
| } |
There was a problem hiding this comment.
[Claude Code] Fixed in e8411db — extracted parseH2OriginMappings and require each mapping's destination to be https: and a loopback hostname (localhost, *.localhost, 127.0.0.1, ::1). A polluted global can no longer redirect realm fetches off-loopback; off-loopback / non-https entries are silently dropped.
| // HTTP/2 alias re-route: when the prerender harness injects a JSON | ||
| // list of {from, to} origin pairs as window.__realmH2OriginMappings__, | ||
| // route realm-server fetches through the HTTPS/h2 listener(s) so | ||
| // they multiplex over one connection per origin instead of | ||
| // serializing through Chrome's HTTP/1.1 6-per-origin ceiling. | ||
| // Canonical realm URLs (in card data) stay on the http origin — only | ||
| // the wire fetch is rewritten. See the repo-root README's "HTTP/2 dev | ||
| // access" section. | ||
| private installH2OriginMappings(virtualNetwork: VirtualNetwork) { | ||
| let h2MappingsRaw = ( | ||
| globalThis as unknown as { __realmH2OriginMappings__?: string } | ||
| ).__realmH2OriginMappings__; | ||
| if (!h2MappingsRaw) { | ||
| return; | ||
| } | ||
| let pairs: Array<{ from?: string; to?: string }>; | ||
| try { | ||
| let parsed = JSON.parse(h2MappingsRaw); | ||
| pairs = Array.isArray(parsed) ? parsed : []; | ||
| } catch { | ||
| return; | ||
| } |
There was a problem hiding this comment.
[Claude Code] Added in e8411db — packages/host/tests/unit/services/network-h2-mappings-test.ts covers the parser's happy and rejection paths: undefined/empty input, malformed JSON, non-array, valid single + multi mappings (incl. 127.0.0.1 and [::1]), off-loopback to, http to, mixed valid/invalid arrays, and same-origin from/to. Smoke-ran the equivalent logic in isolation — 11/11 pass.
Tightening up the new HTTPS/h2 alias path so unexpected input fails safely instead of corrupting boot or relaxing cert trust toward an arbitrary origin. - main.ts: validate `REALM_SERVER_TLS_PORT` is a positive integer in range. A non-numeric value now logs a warning and skips the alias instead of calling `listenSecure(NaN)`. - server.ts/`listenSecure`: wrap the cert/key read and the http2.createSecureServer call in try/catch. A missing, unreadable, or malformed cert now logs a warning and skips the alias instead of preventing the realm-server from booting at all. - host/network.ts: extract `parseH2OriginMappings` as a public helper and require each mapping's destination to be `https:` and a loopback hostname (`localhost`, `*.localhost`, `127.0.0.1`, `::1`). A polluted global can no longer redirect realm fetches off-loopback. - prerender/browser-manager.ts: only pass `--ignore-certificate-errors` when every mapping destination passes the same loopback check. If the env var is unset, malformed, or points anywhere off-loopback, Chromium's default trust policy stays in effect. - network-h2-mappings-test.ts: new qunit unit covering the parser's happy and rejection paths (loopback-only, https-only, JSON-fault tolerance, mixed valid/invalid arrays). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Superseded by #4797 — single-origin HTTPS+HTTP/2 design per @lukemelia's suggestion, with same-port HTTP→HTTPS dispatcher and an auto-migration that rewrites canonical URLs across every public table. |
Summary
DX improvement for local indexing. Heavy aggregator cards (cohort, dashboards) fan out 80+ federated-search requests during a single prerender, and Chrome's HTTP/1.1 6-per-origin connection ceiling serializes them — turning a cohort render into multiple minutes. HTTP/2 multiplexes the same fetches over one connection.
This PR adds an opt-in HTTPS/HTTP-2 alias listener that sits next to the existing HTTP/1.1 server. Indexing automatically uses the h2 alias when a dev cert is provisioned; devs can optionally point their own browser at the same alias for the same speed-up, which better mirrors hosted environments (they already speak HTTP/2 in front of the realm-server).
https://localhost:4203(alias for:4201) andhttps://localhost:4204(alias for:4202) whenREALM_SERVER_TLS_CERT_FILE/KEY_FILEare set. The h2 origin is an alias — an inboundHostheader rewrite middleware ensures URL-keyed realm lookup matches identically on both ports.window.__realmH2OriginMappings__override injected into every page, consumed by the host'sVirtualNetwork. Canonical card URLs are unchanged in card data; only the wire fetch is rewritten.--ignore-certificate-errorsso the self-signed dev cert is accepted withoutmkcert -install.Opting in
That generates a leaf cert via
mkcertinto~/.local/share/boxel/dev-certs/. The script prints clear install hints ifmkcertis missing.If you also want your manual browser to trust the cert without warnings when visiting
https://localhost:4203/…directly, runmkcert -installonce (requires sudo). The prerender / test harness don't need this — they use--ignore-certificate-errors.See the new "HTTP/2 dev access" section in the README for the full guide (port chart, what auto-uses h2, verify command, opt-out).
Test plan
mise run infra:ensure-dev-certis idempotent and exits 0 on subsequent runs.mise run devboots realm-server HTTP-only, no behavior change.curl -kI --http2 https://localhost:4203/_alivereturnsHTTP/2 200.curl -kI --http1.1 https://localhost:4203/_alivereturnsHTTP/1.1 200(ALPN fallback works)./_grafana-reindex?authHeader=…&realm=base/— completes without errors.https://localhost:4203in a browser (aftermkcert -install), log in, load a card — all realm fetches showh2protocol in DevTools network panel.mise run devshutdown closes both listeners cleanly.pnpm lintpasses onpackages/realm-serverandpackages/host(lint:js + prettier — pre-existing lint:types errors in../base/*.gtsare unrelated to this PR).🤖 Generated with Claude Code