From b0248590a8b84959b79af3bb0fddc21ea5ee88ed Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 19:22:18 +0000 Subject: [PATCH] Fix /_/health reporting stale degraded status after schema recovers handleHealth() seeded its error from the isolate's remembered lastSchemaFailure() but never cleared it when the live schemaStatus() read succeeded. An isolate that once failed to migrate, then saw the schema actually get applied by another path (a different isolate racing the same cold start, or its own later attempt outside the health/setup exemption), kept answering 503 "degraded" with the old error indefinitely, contradicting its own schema.ready: true in the same response body. health.ts and setup.ts are exempt from the normal per-request ensureSchema() call, so nothing else clears the memo. A live read that reports every migration applied is direct proof the schema is fine right now, so clear the stale error in that case. --- src/__tests__/handler/schema-guard.test.ts | 16 ++++++++++++++++ src/api/health.ts | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/src/__tests__/handler/schema-guard.test.ts b/src/__tests__/handler/schema-guard.test.ts index 4487794..53a1d28 100644 --- a/src/__tests__/handler/schema-guard.test.ts +++ b/src/__tests__/handler/schema-guard.test.ts @@ -308,6 +308,22 @@ describe("GET /_/health", () => { expect(body.status).toBe("degraded"); expect(body.schema.error).toContain("D1 is unavailable right now"); }); + + it("recovers once a fresh live read reports the schema ready, even with a failure remembered from before", async () => { + // This isolate's first attempt hits a transient error and remembers it. + await worker.fetch(req("/x"), { ...env, DB: brokenDb("D1 is unavailable right now") }, createExecutionContext()); + // The schema is then genuinely applied for real, the way another isolate + // racing the same cold start would: through migrate() directly, without + // this isolate's ensureSchema() ever running again to clear its memo. + await migrate(env); + + const res = await SELF.fetch(req("/_/health")); + expect(res.status).toBe(200); + const body = (await res.json()) as { status: string; schema: { ready: boolean; error?: string } }; + expect(body.status).toBe("ok"); + expect(body.schema.ready).toBe(true); + expect(body.schema.error).toBeUndefined(); + }); }); describe("/_/setup", () => { diff --git a/src/api/health.ts b/src/api/health.ts index d128ee0..175be50 100644 --- a/src/api/health.ts +++ b/src/api/health.ts @@ -25,6 +25,11 @@ export async function handleHealth(env: Env): Promise { const status = await schemaStatus(env); applied = status.applied.length > 0 ? status.applied[status.applied.length - 1].name : null; ready = status.ready; + // A live read that finds every migration applied is direct proof the + // schema is fine right now, even on an isolate that still remembers an + // earlier attempt failing (its own, or one it never repeated because + // health is exempt from ensureSchema()). + if (ready) error = null; } catch (err) { error = err instanceof Error ? err.message : String(err); }