feat(web): add an unauthenticated GET /healthz probe to the backend - #2585
Conversation
Orchestrators (Docker, Kubernetes, process managers) had no cheap way to
check the web backend is up without exercising a real proxy/connect flow.
Both the prod Hono server and the dev Vite backend now answer GET/HEAD
/healthz with 200, a fixed {"status":"ok"} body and Cache-Control:
no-store.
The route sits outside /api/*, so the bearer-token and origin checks do
not apply (a probe cannot learn a per-start token), and it discloses
nothing beyond "up" -- no version, uptime, servers or config.
Closes #2438
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The new integration test file lacks the repository-required purpose header.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Adds a consistent unauthenticated /healthz endpoint to the web development and production backends.
Changes:
- Adds shared health-check matching and response helpers.
- Integrates the endpoint into both backends with integration coverage.
- Documents orchestrator usage and security behavior.
| File | Description |
|---|---|
clients/web/server/health.ts |
Defines the health-check contract. |
clients/web/server/server.ts |
Registers the production route. |
clients/web/server/vite-hono-plugin.ts |
Handles health probes during development. |
clients/web/src/test/integration/server/health.test.ts |
Tests helpers and production behavior. |
clients/web/README.md |
Documents endpoint semantics and usage. |
docs/docker.md |
Documents external orchestrator probes. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Every file in the repo carries a header explaining its purpose (AGENTS.md, Project Structure); the new health.test.ts was missing one. Copilot review on #2585. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
The dev Vite middleware wrote the health response inline, in a file that is excluded from coverage, so its headers and HEAD handling had no test. Move it into handleNodeHealthRequest() in health.ts and drive that from a real node:http server in health.test.ts. health.ts stays at 100% on all four dimensions. Copilot review on #2585. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
Copilot review round 2, summary:
Verification for c22de80: I queued the fixed-port smokes and Storybook under the gate lease again, and the lease gave up after 66 minutes without starting, the third time on this machine. Those stages passed under the lease on the first commit, and since then nothing changed except this adapter. Requesting round 3. |
|
Copilot review loop closed. Round 3 (on c22de80) was clean: "Approval recommended", no findings, no inline comments, no suppressed comments. Per the review loop's stop rule, one clean round ends it. Rounds 1 and 2 each had one in-scope finding, both fixed and answered in their threads. |


Closes #2438
What
The web backend now answers
GET /healthz(andHEAD) with200, a fixed{"status":"ok"}body andCache-Control: no-store. Orchestrators (Docker, Kubernetes, process managers) can use it to check the backend is up without going through a real proxy or connect flow.clients/web/server/health.ts(new) holds the path, the body, the headers, a request matcher and the response builder. It is unit tested and gated at 100% on all four dimensions.clients/web/server/server.ts(prod):app.get(HEALTH_PATH, …)is registered before the static and SPA fallbacks, so the probe is never answered withindex.html.clients/web/server/vite-hono-plugin.ts(dev): the existing middleware answers the same route, sonpm run devand prod agree. I checked this by hand against a livevitedev server: GET returns 200 with{"status":"ok"}, HEAD returns 200 with no body, and/api/configstill returns 401.health.tsentry inclients/web/README.md, plus one sentence indocs/docker.mdpointing external orchestrators at the route.Decisions (made deliberately, per the issue)
/healthzrather than/api/health. Every/api/*route sits behind thex-mcp-remote-authbearer check and the origin allow-list (AGENTS.md, "Web backend auth token"). A probe has no way to learn a token that is generated fresh on each start. A top-level path needs no exception in the auth middleware./api/healthwould have needed one carved in./api. A version string helps fingerprinting. "Is it running" is already answerable fromGET /, so this adds no new information. The tests also check that the response never carries the API token, whichGET /embeds.HEALTHCHECKstill probes/. Switchingscripts/docker-healthcheck.mjsto/healthzwould be a natural follow-up. I left it out to keep this diff to the endpoint the issue asks for.Tests
clients/web/src/test/integration/server/health.test.tscovers:startHonoServer: 200 with only the fixed body and no token leak, HEAD with no body, a disallowedOriginis still answered,/api/configstill returns 401Gate
npm run formatwas clean.npm run local:gatedid not finish as a single run. The machine-wide gate lease was ~18–20 gates deep from parallel agent sessions, and two queued runs reached the lease's 45-minute wait cap without starting. So I ran every gate stage on its own:smoke,smoke:web:firefox,local:storybook) ran under the lease. All green.local:validate,verify:skills:cli,coverage:{web,cli,tui,launcher},verify:build-gate,verify:bundle-externals) ran without it. All green except one environmental failure, below.Two kinds of failure came from the sandbox, not this diff:
transport.test.ts,server-extra-coverage.test.tsand the CLI revocation tests. With the proxy env unset, all of them pass, and so doescoverage:cli.secret-store-selection.test.ts > isOnMountPoint > is false when the path itself can't be resolvedfails because the worktree is a bind mount in this container, so.really is on a mount point. That is core code this PR does not touch, and CI's runner is not a container.🤖 Generated with Claude Code