Repository navigation
fix(cli): make /healthz pure liveness and add /readyz readiness probe - #694
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
blove
temporarily deployed
to
vercel-preview
September 17, 2026 05:31 — with
GitHub Actions
Inactive
blove
enabled auto-merge (squash)
September 17, 2026 15:08
blove
temporarily deployed
to
vercel-preview
September 17, 2026 15:10 — with
GitHub Actions
Inactive
blove
force-pushed
the
fix/healthz-liveness-readyz
branch
from
September 17, 2026 16:45
9afa143 to
f9d5d04
Compare
…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
force-pushed
the
fix/healthz-liveness-readyz
branch
from
September 17, 2026 16:47
f9d5d04 to
c39b7a9
Compare
blove
temporarily deployed
to
vercel-preview
September 17, 2026 16:54 — with
GitHub Actions
Inactive
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>
14 of 16 tasks
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>
This was referenced Sep 17, 2026
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #688.
Problem
On the
verceltarget/healthzinitialised the per-request stores, so withDATABASE_URLunset or the database unreachable it returned500 {"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 /healthzis liveness. It never callsrequestStoresand answers200 {"status":"ready"}whenever the process serves HTTP. The body is unchanged so every harness and the DockerfileHEALTHCHECKkeep working. The deliberate capability-gap 500 (B4_E1005) still applies to it, as before.GET /readyzis 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 answers200 {"status":"ready","checks":{...}}or503 {"status":"not_ready","checks":{...}}. A throwing store factory is reported asrequestStores(on Vercel that message namesDATABASE_URL), an omitted store carriesB4_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).Tests
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 reportsB4_E5301; Node runtime/readyz200 against boot-resolved SQLite stores.request-storesandhono-targettests that relied on/healthzbuilding stores now drive/readyz; the API-reference behaviour-contract fingerprints were updated to match.thread-access-coverageclassifies/readyzas exempt (route count 15 → 16). The gated workerd lane waits on/readyzso it still proves the store factory reached Postgres.Docs
Production topology, middleware, embedding, Kubernetes deployment, Docker blueprint, chart README/values comments and
llms.txtnow describe both probes. The Helm chart template is unchanged:healthPathstill applies to all three probes, and the README says what pointing it at/readyzimplies.Verification
pnpm lint, clitsc --noEmit,node scripts/check-docs.mjsand the API-reference analysis pass. The full@b4run/clivitest 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