diff --git a/api/__tests__/handlers.test.ts b/api/__tests__/handlers.test.ts index bedf17ee..47b52ef5 100644 --- a/api/__tests__/handlers.test.ts +++ b/api/__tests__/handlers.test.ts @@ -1318,6 +1318,27 @@ describe("GET /api/v1/keepers/health", () => { lastSuccessMs: null, }); }); + + it("returns 503 when the upstream rate limiter fails", async () => { + vi.mocked(checkRateLimit).mockRejectedValueOnce( + new Error("Upstash timeout") + ); + + const res = makeRes(); + await keepersHandler( + fakeReq({ + query: { action: "health" }, + method: "GET", + headers: {}, + }), + res + ); + + expect(res.statusCode).toBe(503); + expect(res.body).toEqual({ + error: "Rate limiter unavailable; refusing to run", + }); + }); }); describe("GET /api/v1/admin/vault-state", () => { diff --git a/api/v1/keepers/[action].ts b/api/v1/keepers/[action].ts index d68cde34..cbfd7889 100644 --- a/api/v1/keepers/[action].ts +++ b/api/v1/keepers/[action].ts @@ -43,7 +43,14 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { if (action === "health") { if (applyCors(req, res)) return; - if (!(await checkRateLimit(req, res))) return; + try { + if (!(await checkRateLimit(req, res))) return; + } catch (err) { + console.error("[keepers/health] rate limit check failed:", err); + return res + .status(503) + .json({ error: "Rate limiter unavailable; refusing to run" }); + } const result = await handleGetKeeperHealth(); if (result.error) console.error("[keepers/health] error:", result.error); res.setHeader("Cache-Control", "no-store");