From d8f6981be1f05772895593cdd436e6c0d4cdfd9a Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Sun, 9 Aug 2026 09:48:10 +0000 Subject: [PATCH] =?UTF-8?q?revert(api):=20remove=20/health/ready=20?= =?UTF-8?q?=E2=80=94=20superseded=20by=20/api/readyz=20(GRO-2689)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CTO architectural ruling (PR #235 review): K8s readiness/liveness probes must remain DB-less to prevent transient DB blips from cycling pods. The /api/readyz endpoint (GRO-2687) is the canonical DB-health signal for the monitoring layer and satisfies the GRO-2678 detection-gap requirement. Removes: - GET /health/ready route from src/index.ts - src/__tests__/health-ready.test.ts Refs GRO-2689, GRO-2687, GRO-2678 --- src/__tests__/health-ready.test.ts | 102 ----------------------------- src/index.ts | 11 ---- 2 files changed, 113 deletions(-) delete mode 100644 src/__tests__/health-ready.test.ts diff --git a/src/__tests__/health-ready.test.ts b/src/__tests__/health-ready.test.ts deleted file mode 100644 index 6a8ea88..0000000 --- a/src/__tests__/health-ready.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { Hono } from "hono"; - -// ─── Mock db module ─────────────────────────────────────────────────────────── - -let selectImpl: () => Promise; - -vi.mock("@groombook/db", () => { - const staff = new Proxy( - { _name: "staff" }, - { - get(_target, prop) { - if (prop === "_name") return "staff"; - return { table: "staff", column: prop }; - }, - } - ); - - return { - getDb: () => ({ - select: (_fields: unknown) => ({ - from: (_table: unknown) => ({ - limit: (_n: number) => selectImpl(), - }), - }), - }), - staff, - }; -}); - -// ─── Build test app ─────────────────────────────────────────────────────────── - -async function makeApp() { - const { getDb, staff } = await import("@groombook/db"); - - const app = new Hono(); - app.get("/health/ready", async (c) => { - try { - await getDb().select({ id: staff.id }).from(staff).limit(1); - return c.json({ status: "ready" }, 200); - } catch (err) { - const pgCode = (err as Record).code ?? "unknown"; - console.error("[health/ready] DB check failed:", pgCode); - return c.json({ status: "degraded" }, 503); - } - }); - return app; -} - -// ─── Tests ──────────────────────────────────────────────────────────────────── - -describe("GET /health/ready", () => { - beforeEach(() => { - vi.restoreAllMocks(); - }); - - it("returns 200 {status:'ready'} when DB query succeeds", async () => { - selectImpl = () => Promise.resolve([{ id: "staff-1" }]); - - const app = await makeApp(); - const res = await app.request("/health/ready", { method: "GET" }); - const body = (await res.json()) as Record; - - expect(res.status).toBe(200); - expect(body.status).toBe("ready"); - }); - - it("returns 503 {status:'degraded'} when DB query throws (schema dropped)", async () => { - const schemaErr = Object.assign(new Error("relation \"staff\" does not exist"), { code: "42P01" }); - selectImpl = () => Promise.reject(schemaErr); - - const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - - const app = await makeApp(); - const res = await app.request("/health/ready", { method: "GET" }); - const body = (await res.json()) as Record; - - expect(res.status).toBe(503); - expect(body.status).toBe("degraded"); - - // Must not leak SQL error details in the response body - expect(JSON.stringify(body)).not.toContain("42P01"); - expect(JSON.stringify(body)).not.toContain("relation"); - - consoleSpy.mockRestore(); - }); - - it("returns 503 {status:'degraded'} on any DB connection error", async () => { - selectImpl = () => Promise.reject(new Error("ECONNREFUSED")); - - const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - - const app = await makeApp(); - const res = await app.request("/health/ready", { method: "GET" }); - const body = (await res.json()) as Record; - - expect(res.status).toBe(503); - expect(body.status).toBe("degraded"); - - consoleSpy.mockRestore(); - }); -}); diff --git a/src/index.ts b/src/index.ts index 99a66fa..6da003b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,17 +65,6 @@ app.use( app.get("/health", (c) => c.json({ status: "ok" })); // /api/health: used by Gateway HTTPRoute (/api/* → API pod) app.get("/api/health", (c) => c.json({ status: "ok" })); -// /health/ready: DB-touching readiness probe — K8s removes pod from endpoints when schema is dropped (GRO-2689) -app.get("/health/ready", async (c) => { - try { - await getDb().select({ id: staff.id }).from(staff).limit(1); - return c.json({ status: "ready" }, 200); - } catch (err) { - const pgCode = (err as Record).code ?? "unknown"; - console.error("[health/ready] DB check failed:", pgCode); - return c.json({ status: "degraded" }, 503); - } -}); // /api/readyz: DB-touching deep health check consumed by monitoring (not K8s probes) // Distinct from /health so a dropped schema triggers an alert without cycling pods (GRO-2678) app.get("/api/readyz", async (c) => {