Skip to content

fix(cli): make /healthz pure liveness and add /readyz readiness probe - #694

Merged
blove merged 3 commits into
mainfrom
fix/healthz-liveness-readyz
Sep 17, 2026
Merged

blove merged 3 commits into
mainfrom
fix/healthz-liveness-readyz

Conversation

@blove

@blove blove commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Fixes #688.

Problem

On the vercel target /healthz initialised the per-request stores, so with DATABASE_URL unset or the database unreachable it returned 500 {"error":{"kind":"execution_error","message":"Unexpected runtime server failure"}}. It behaved as a readiness probe, so a deployment missing one env var looked completely down.

Change

The runtime shares one route table across the Node, Hono and Vercel targets, so this applies to all of them.

  • GET /healthz is liveness. It never calls requestStores and answers 200 {"status":"ready"} whenever the process serves HTTP. The body is unchanged so every harness and the Dockerfile HEALTHCHECK keep working. The deliberate capability-gap 500 (B4_E1005) still applies to it, as before.
  • GET /readyz is readiness. It builds the request's stores and probes the threads, checkpointer and permissions stores with a real query against a thread id that never exists, running lazy migrations on the way. It answers 200 {"status":"ready","checks":{...}} or 503 {"status":"not_ready","checks":{...}}. A throwing store factory is reported as requestStores (on Vercel that message names DATABASE_URL), an omitted store carries B4_E5301, connection-string userinfo is redacted (postgres://***@host/db), messages are capped, and each distinct cause is logged to stderr once. Stores built for the probe are disposed through the normal request lifetime. The memory store is deliberately not probed (lazy and optional).
  • Route matching now runs before store construction so the two probes can decide what to do with the factory. Every other route, including 404s, builds stores exactly as before.

Tests

  • New packages/cli/test/health-probes.test.ts: liveness never calls the factory; readiness 200 + disposal + ready() honoured; factory throw reported by name with credentials redacted from body and log; one failing store named while the others report ok; omitted store reports B4_E5301; Node runtime /readyz 200 against boot-resolved SQLite stores.
  • request-stores and hono-target tests that relied on /healthz building stores now drive /readyz; the API-reference behaviour-contract fingerprints were updated to match. thread-access-coverage classifies /readyz as exempt (route count 15 → 16). The gated workerd lane waits on /readyz so it still proves the store factory reached Postgres.

Docs

Production topology, middleware, embedding, Kubernetes deployment, Docker blueprint, chart README/values comments and llms.txt now describe both probes. The Helm chart template is unchanged: healthPath still applies to all three probes, and the README says what pointing it at /readyz implies.

Verification

pnpm lint, cli tsc --noEmit, node scripts/check-docs.mjs and the API-reference analysis pass. The full @b4run/cli vitest suite passes (1821 tests); two timing-sensitive files that failed only under concurrent load (hono-node-roundtrip, vercel-native-lane) pass when rerun alone.

Not released; changeset added as a patch.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
b4-run Ready Ready Preview Sep 17, 2026 4:49pm UTC

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

blove and others added 3 commits September 17, 2026 09:46
…iness

On the vercel (and hono) target the per-request store factory ran for every
request, health checks included, so a deployment with DATABASE_URL unset or
its database unreachable answered /healthz with a 500 and looked completely
down instead of "up, not ready".

- GET /healthz no longer calls requestStores; it answers 200 {status:"ready"}
  whenever the process serves HTTP (body kept for harness compatibility).
- New GET /readyz builds the request's stores and probes threads, checkpointer
  and permissions with a real query (running lazy migrations on the way).
  200 {status:"ready",checks} or 503 {status:"not_ready",checks}; a throwing
  factory is reported as `requestStores`, an omitted store carries B4_E5301,
  connection-string credentials are redacted, and each cause is logged once.
- Route matching now happens before store construction so the two probes can
  decide what to do with the factory; every other route is unchanged.
- Tests that relied on /healthz building stores now use /readyz; the
  thread-access coverage exempt list and route count include /readyz.
- Docs: production topology, middleware, embedding, kubernetes, docker
  blueprint, chart README/values and llms.txt describe both probes.

Fixes #688

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…(error)

Rebased onto #692, which landed the edge-safe error-chain serializer. The
readiness probe's own reporting had the exact bug that fixed: the Postgres
drivers reject a failed connect with a DOM ErrorEvent, so `String(error)`
rendered "[object ErrorEvent]" — the one failure /readyz exists to name would
have arrived unnamed.

- describeReadinessFailure renders through formatErrorChain, so the root cause
  and its driver code reach the check body
- redaction now runs over the whole rendered chain, which matters because the
  chain is built from driver messages that quote the connection string, and
  unlike the catch-all's stderr log this one is served in a response body
- docs say plainly that /readyz bodies carry real causes (hosts, ports,
  database names, driver codes) and so must sit behind the outer boundary

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@blove
blove force-pushed the fix/healthz-liveness-readyz branch from f9d5d04 to c39b7a9 Compare September 17, 2026 16:47
@blove
blove merged commit f20ba3b into main Sep 17, 2026
30 of 31 checks passed
@blove
blove deleted the fix/healthz-liveness-readyz branch September 17, 2026 17:42
blove added a commit that referenced this pull request Sep 17, 2026
#694 added /readyz beside /healthz. The scoped runtime route a SPA fallback
installs did not list it, so on a composed Vercel deployment /readyz matched
the fallback and served the SPA document with a 200 rather than reaching the
runtime — a readiness probe that looks healthy while reporting nothing.

Add readyz, and cover the composed route with one case per rooted runtime
surface so a future surface cannot be added without updating it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
blove added a commit that referenced this pull request Sep 17, 2026
#694 landed and inverted what this guide said about /healthz. Route
matching now happens before store construction, so:

- /healthz is pure liveness: no stores, 200 whenever the function serves
  HTTP, including with DATABASE_URL unset. The guide had said the
  opposite, that /healthz opens a pool and a bad connection string
  surfaces there as a 500;
- /readyz is the new dependency probe, and is what the guide now tells
  readers to hit after a deploy — with the two caveats that it reports
  real causes (hostnames, ports, driver codes) and that neither probe is
  gated by middleware or thread access.

/readyz is also added to the frontend routing lists and the deploy
checklist, and the local-bundle snippet now probes it, since /healthz no
longer proves anything about the database.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
blove added a commit that referenced this pull request Sep 17, 2026
Only the generated SEO lastmod manifest conflicted; regenerated rather
than hand-merged. No code overlap with #694, #701, or the
software-factory work order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
blove added a commit that referenced this pull request Sep 17, 2026
…708)

The "Run the Vercel bundle locally" recipe added in #698 verified nothing, because
#694 landed after it. #694 made GET /healthz pure process liveness and added
GET /readyz, and the recipe still called /healthz, which never builds the
request's stores — so it answered 200 whether or not DATABASE_URL resolved. That
is the opposite of the section's purpose: proving the pg fallback reached a plain
local Postgres with no WebSocket proxy.

Measured on a Vercel fixture built from main, against a real Postgres container:

  /healthz, reachable database:   200 {"status":"ready"}
  /healthz, unreachable database: 200 {"status":"ready"}
  /readyz,  reachable database:   200, checkpointer/permissionsStore/threadsStore ok
  /readyz,  unreachable database: 503 not_ready, naming connect ECONNREFUSED

The two sample bodies in the page are that run's output. The password in the
failing connection string does not appear in the response, which is what the
redaction note rests on.

Also adds /readyz to the rooted surfaces the runtime owns, a list written before
#694 that omitted it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
blove added a commit that referenced this pull request Sep 17, 2026
…el build output (#696)

* docs: design for composed Vercel build output (#679)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(cli): resolve build.vercel config and relax the Vercel output validator

Pure resolver for build.vercel (functionName, static, functions, routes),
route composition in Build Output order, and a validator that accepts a
composed tree while keeping the runtime function config exact.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(cli): compose static assets, extra functions, and routes into the Vercel build output

build.vercel describes the Build Output tree beyond the runtime function.
The target checks every configured path before touching .vercel/, copies
the static dir, bundles each extra function, orders routes as user routes
-> filesystem -> runtime -> SPA fallback, and validates the composed tree
before the existing atomic publish. The runtime function is b4.func when
static assets are configured so it does not shadow static/index.html.

Refs #679, #687

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* style: format Vercel target sources with the repo config and refresh SEO lastmod

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(web): refresh SEO lastmod after merging main

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(web): refresh SEO lastmod after merging main

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(cli): route /readyz to the runtime under a Vercel SPA fallback

#694 added /readyz beside /healthz. The scoped runtime route a SPA fallback
installs did not list it, so on a composed Vercel deployment /readyz matched
the fallback and served the SPA document with a 200 rather than reaching the
runtime — a readiness probe that looks healthy while reporting nothing.

Add readyz, and cover the composed route with one case per rooted runtime
surface so a future surface cannot be added without updating it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(cli): assert the composed runtime route from its exported constant

The integration test duplicated the route string, so adding /readyz to the
implementation left a stale literal asserting the old value. Reference the
constant instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(web): refresh SEO lastmod after merging main

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(cli): declare response streaming on the Vercel runtime function

The runtime function's .vc-config.json carried only handler, launcherType
and runtime, so Vercel's Node launcher buffered its responses. The runtime
answers /agui/:routeId and /threads/:id/runs/stream with text/event-stream,
which meant a deployed frontend received nothing until a run finished
instead of tokens as they were produced. Extra functions configured through
build.vercel.functions could already opt into streaming; the one function
that always streams could not.

supportsResponseStreaming is fixed rather than configurable: it follows from
what the runtime serves, not from how the app is deployed. validateVercelOutput
keeps asserting the runtime config exactly, deriving both the written bytes and
the expectation from the same constant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(cli): give every build.vercel rejection the B4_E1003 code and docs link

resolveVercelComposition threw bare CliErrors, so a malformed
build.vercel.static.dir printed an uncoded line while an unknown
build.vercel key printed the coded, linked form check-error-codes.test.ts
asserts. Move the shared invalidBuildConfig helper into vercel-compose.ts,
which vercel-config.ts already depends on, and route every throw through it.

Also qualify the local-run snippet in the edge docs: the runtime function is
index.func only until build.vercel.static renames it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(web): refresh SEO lastmod after merging main

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(vercel): describe the composed tree in the Vercel guide

#700 added the guide against the pre-change behaviour: its compose section
said the target emits no static dir, no extra functions and no routes, and
cited #679/#687 as open proposals. Rewrite it around build.vercel, keep the
assemble-it-yourself path as a scoped subsection since outDir still serves
that case, and qualify the artifacts table as the bare-build shape.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

This branch was successfully deployed

1 active and 1 inactive deployments
vercel-preview — c39b7a9b Deployed Sep 17, 2026 by blove via vercel-native #1968
Preview — c39b7a9b Deployed Sep 17, 2026 by vercel[bot]
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.

vercel target: /healthz initialises stores and fails when the database is unavailable

1 participant