Skip to content

realm-server: opt-in HTTP/2 for faster local indexing#4787

Closed
habdelra wants to merge 4 commits into
mainfrom
worktree-cs-11114-http2
Closed

realm-server: opt-in HTTP/2 for faster local indexing#4787
habdelra wants to merge 4 commits into
mainfrom
worktree-cs-11114-http2

Conversation

@habdelra
Copy link
Copy Markdown
Contributor

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).

  • Realm-server listens on https://localhost:4203 (alias for :4201) and https://localhost:4204 (alias for :4202) when REALM_SERVER_TLS_CERT_FILE/KEY_FILE are set. The h2 origin is an alias — an inbound Host header rewrite middleware ensures URL-keyed realm lookup matches identically on both ports.
  • Prerender Chromium and the test-realms indexer transparently re-route fetches to the h2 alias via a window.__realmH2OriginMappings__ override injected into every page, consumed by the host's VirtualNetwork. Canonical card URLs are unchanged in card data; only the wire fetch is rewritten.
  • Puppeteer launches with --ignore-certificate-errors so the self-signed dev cert is accepted without mkcert -install.
  • Absent the cert, env-vars.sh leaves the TLS env vars unset → realm-server stays HTTP-only. CI and the software-factory hermetic harness are untouched.

Opting in

mise run infra:ensure-dev-cert     # one-time, idempotent, no sudo
mise run dev-all                   # picks up the cert automatically

That generates a leaf cert via mkcert into ~/.local/share/boxel/dev-certs/. The script prints clear install hints if mkcert is missing.

If you also want your manual browser to trust the cert without warnings when visiting https://localhost:4203/… directly, run mkcert -install once (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-cert is idempotent and exits 0 on subsequent runs.
  • Without the cert (CI path): mise run dev boots realm-server HTTP-only, no behavior change.
  • With the cert: curl -kI --http2 https://localhost:4203/_alive returns HTTP/2 200.
  • curl -kI --http1.1 https://localhost:4203/_alive returns HTTP/1.1 200 (ALPN fallback works).
  • Trigger a base-realm reindex via /_grafana-reindex?authHeader=…&realm=base/ — completes without errors.
  • Open https://localhost:4203 in a browser (after mkcert -install), log in, load a card — all realm fetches show h2 protocol in DevTools network panel.
  • mise run dev shutdown closes both listeners cleanly.
  • pnpm lint passes on packages/realm-server and packages/host (lint:js + prettier — pre-existing lint:types errors in ../base/*.gts are unrelated to this PR).

🤖 Generated with Claude Code

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>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread mise-tasks/services/realm-server
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>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 12, 2026

Preview deployments

Host Test Results

    1 files      1 suites   2h 3m 16s ⏱️
2 669 tests 2 654 ✅ 15 💤 0 ❌
2 688 runs  2 673 ✅ 15 💤 0 ❌

Results for commit e8411db.

Realm Server Test Results

    1 files  ±0      1 suites  ±0   11m 21s ⏱️ -13s
1 334 tests +6  1 334 ✅ +6  0 💤 ±0  0 ❌ ±0 
1 413 runs  +6  1 413 ✅ +6  0 💤 ±0  0 ❌ ±0 

Results for commit e8411db. ± Comparison against earlier commit e013c1b.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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’s NetworkService VirtualNetwork.
  • Add a mise task to provision a local dev cert via mkcert, 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.

Comment thread packages/realm-server/server.ts Outdated
Comment thread packages/realm-server/server.ts Outdated
Comment thread packages/host/app/services/network.ts Outdated
Comment thread mise-tasks/services/realm-server
Comment thread mise-tasks/infra/ensure-dev-cert Outdated
Comment thread mise-tasks/lib/env-vars.sh Outdated
Comment thread README.md
- 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>
@habdelra habdelra requested a review from a team May 12, 2026 16:54
Copy link
Copy Markdown
Contributor

@lukemelia lukemelia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@habdelra
Copy link
Copy Markdown
Contributor Author

habdelra commented May 12, 2026

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.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.

Comment thread packages/realm-server/main.ts Outdated
Comment on lines +505 to +507
let tlsPort = process.env.REALM_SERVER_TLS_PORT
? Number(process.env.REALM_SERVER_TLS_PORT)
: undefined;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code] Fixed in e8411dbmain.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.

Comment thread packages/realm-server/server.ts Outdated
Comment on lines +356 to +366
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,
},
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Comment on lines +32 to +41
// 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');
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Comment on lines +97 to +123
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);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Comment thread packages/host/app/services/network.ts Outdated
Comment on lines +89 to +110
// 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;
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code] Added in e8411dbpackages/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>
@habdelra
Copy link
Copy Markdown
Contributor Author

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.

@habdelra habdelra closed this May 12, 2026
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.

3 participants