feat: add a readiness probe at /healthz #105

Sammanfogat
supernaut sammanfogade 1 incheckning från feat/healthz in i main 2026-07-29 22:25:15 +00:00
Ägare

Adds GET /healthz, the prerequisite for gating deploys (#66). Nothing else in that issue — the
container healthcheck and auto-update --rollback — can be built without something meaningful to
probe.

Why it checks the database

A static 200 would defeat the entire purpose. The deploy failures worth catching are exactly the ones
where the process starts fine and cannot serve: a wrong DATABASE_URL, a failed migration, a
missing secret. Verified against a real built server with DATABASE_URL unset:

Request Result
GET /healthz 503 {"db":"unreachable","ok":false}
GET / 200

The homepage still serves, because it does not touch Postgres. So a liveness-only probe — or a static
200 — would have reported that container healthy while sign-up was completely broken. That gap is the
whole reason for this endpoint.

Deliberate trade-off, and where the tolerance belongs

Including the database makes this a readiness probe, not a liveness probe: a Postgres blip marks
the portal unhealthy even though Node is fine. That is the right answer for "should this image be
promoted?"
and the wrong answer for "should this container be killed?".

The consequence is that the container healthcheck needs enough retries that a transient blip cannot
trigger a pointless restart or a rollback of a perfectly good image. That tolerance belongs in the
Quadlet unit, not in this endpoint, and is called out in the code comment so it is not forgotten when
the infra side lands.

Shape

  • src/lib/health.ts — the logic, with the DB probe injected so it is testable without a live
    Postgres. Matches the repo's convention of logic in lib/ and thin pages.
  • src/pages/healthz.ts — a thin route mapping the report to 200/503.

Two details that matter:

  • A timeout (2s). A hung connection is the failure mode this guards: without it the request would
    hang until the healthcheck's own timeout killed it, which reports far less clearly than an explicit
    503. The timer is always cleared, so a slow-but-successful probe leaves no pending handle.
  • The error never reaches the client. Driver messages can carry host and credential detail, and
    this endpoint is public and unauthenticated. Only a coarse status is returned; the detail goes to the
    container log. There is a test asserting the report cannot contain such text.

SELECT 1 touches no application table, so the probe stays valid mid-migration.

Verified

6 new tests (ok / rejects / hangs / slow-but-inside-deadline / no error leakage / timer cleared);
82 total, up from 76. check, eslint, stylelint, format:check, mdlint, drizzle-kit check
and build all pass, plus the real-server check above.

Refs #66

Adds `GET /healthz`, the prerequisite for gating deploys (#66). Nothing else in that issue — the container healthcheck and `auto-update --rollback` — can be built without something meaningful to probe. ## Why it checks the database A static 200 would defeat the entire purpose. The deploy failures worth catching are exactly the ones where the process starts *fine* and cannot serve: a wrong `DATABASE_URL`, a failed migration, a missing secret. Verified against a real built server with `DATABASE_URL` unset: | Request | Result | | --- | --- | | `GET /healthz` | **503** `{"db":"unreachable","ok":false}` | | `GET /` | **200** | The homepage still serves, because it does not touch Postgres. So a liveness-only probe — or a static 200 — would have reported that container healthy while sign-up was completely broken. That gap is the whole reason for this endpoint. ## Deliberate trade-off, and where the tolerance belongs Including the database makes this a **readiness** probe, not a liveness probe: a Postgres blip marks the portal unhealthy even though Node is fine. That is the right answer for *"should this image be promoted?"* and the wrong answer for *"should this container be killed?"*. The consequence is that the container healthcheck needs enough retries that a transient blip cannot trigger a pointless restart or a rollback of a perfectly good image. That tolerance belongs in the Quadlet unit, not in this endpoint, and is called out in the code comment so it is not forgotten when the infra side lands. ## Shape - `src/lib/health.ts` — the logic, with the DB probe **injected** so it is testable without a live Postgres. Matches the repo's convention of logic in `lib/` and thin pages. - `src/pages/healthz.ts` — a thin route mapping the report to 200/503. Two details that matter: - **A timeout (2s).** A hung connection is the failure mode this guards: without it the request would hang until the healthcheck's own timeout killed it, which reports far less clearly than an explicit 503. The timer is always cleared, so a slow-but-successful probe leaves no pending handle. - **The error never reaches the client.** Driver messages can carry host and credential detail, and this endpoint is public and unauthenticated. Only a coarse status is returned; the detail goes to the container log. There is a test asserting the report cannot contain such text. `SELECT 1` touches no application table, so the probe stays valid mid-migration. ## Verified 6 new tests (ok / rejects / hangs / slow-but-inside-deadline / no error leakage / timer cleared); 82 total, up from 76. `check`, `eslint`, `stylelint`, `format:check`, `mdlint`, `drizzle-kit check` and `build` all pass, plus the real-server check above. Refs #66
supernaut lade till 1 incheckning 2026-07-29 22:14:43 +00:00
feat: add a readiness probe at /healthz
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m16s
0f55334847
supernaut sammanfogade incheckning 25f99441e4 till main 2026-07-29 22:25:15 +00:00
supernaut tog bort grenen feat/healthz 2026-07-29 22:25:15 +00:00
supernaut refererade denna ändringsförfrågan från en incheckning 2026-07-29 22:25:17 +00:00
supernaut refererade denna ändringsförfrågan från en incheckning 2026-08-03 09:41:50 +00:00
Logga in för att delta i denna konversation.
Inga granskare
Ingen milstolpe
Inget projekt
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Förfallodatumet är ogiltigt eller utanför gränserna. Använd formatet "åååå-mm-dd".

Inget förfallodatum satt.

Beroenden

Inga beroenden satta

Referens
bitborg/bitborg-web!105
Ingen beskrivning angiven.