From be37cf782c2c78a672acbdb5565c656b602983b2 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 13 May 2026 13:18:54 -0700 Subject: [PATCH] fix(proxy): expose db status on public /health/readiness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backport of #27866 onto litellm_1.84.0rc2. External readiness probes consumed the legacy detailed payload's `db` field to drive alerting and pod-rotation decisions. Stripping the body to {"status": "healthy"} broke those probes silently — the HTTP code still flipped to 503, but probes checking body.db == "connected" treated the response as healthy. Add `db` back to the unauthenticated payload. The rest of the diagnostic fields (litellm_version, callbacks, cache, log_level) stay behind /health/readiness/details so the recon-leak gate from #26912 holds. Values match the legacy contract: "connected", "disconnected", "Not connected". The 503-on-DB-disconnect behavior from LIT-2607 is preserved. --- .../health_endpoints/_health_endpoints.py | 22 +++++++++++++------ .../health_endpoints/test_health_endpoints.py | 15 ++++++++----- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/litellm/proxy/health_endpoints/_health_endpoints.py b/litellm/proxy/health_endpoints/_health_endpoints.py index 096e23e673d..4394b916643 100644 --- a/litellm/proxy/health_endpoints/_health_endpoints.py +++ b/litellm/proxy/health_endpoints/_health_endpoints.py @@ -1530,15 +1530,21 @@ def _allow_public_health_readiness_details() -> bool: return general_settings.get("allow_public_health_readiness_details") is True -async def _set_public_readiness_status(response: Response) -> None: +async def _resolve_public_readiness_db(response: Response) -> str: + """ + Return the db status string for the public probe and flip the response to + 503 when a configured DB is unreachable. Mirrors the legacy values: + "Not connected" (no DB configured), "connected", "disconnected". + """ from litellm.proxy.proxy_server import prisma_client if prisma_client is None: - return + return "Not connected" db_health_status = await _db_health_readiness_check() if db_health_status["status"] != "connected": response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE + return db_health_status["status"] @router.get( @@ -1547,15 +1553,17 @@ async def _set_public_readiness_status(response: Response) -> None: ) async def health_readiness(response: Response): """ - Public readiness probe. Keep this low-detail for unauthenticated load - balancers by default. Admins can opt into the legacy detailed public - payload with general_settings.allow_public_health_readiness_details. + Public readiness probe. Returns a low-detail payload safe to expose to + unauthenticated load balancers — `status` plus `db` so orchestrators and + external probes can distinguish "healthy" from "DB unreachable" without a + credential. Admins can opt into the legacy detailed payload with + general_settings.allow_public_health_readiness_details. """ if _allow_public_health_readiness_details(): return await _get_health_readiness_details(response=response) - await _set_public_readiness_status(response=response) - return {"status": "healthy"} + db_status = await _resolve_public_readiness_db(response=response) + return {"status": "healthy", "db": db_status} @router.get( diff --git a/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py b/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py index 2edcb00c967..df066e35a06 100644 --- a/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py +++ b/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py @@ -614,9 +614,14 @@ def test_health_readiness(proxy_client): duration_ms < 500 ), f"Health check took {duration_ms:.2f}ms, expected < 500ms for readiness endpoint" - # Assert response contains only low-detail public probe fields + # Assert response contains only low-detail public probe fields. `db` is + # included so unauthenticated probes can distinguish "DB unreachable" + # from a fully-healthy worker; its value depends on whether the test env + # exposes DATABASE_URL. response_data = response.json() - assert response_data == {"status": "healthy"} + assert set(response_data.keys()) == {"status", "db"} + assert response_data["status"] == "healthy" + assert response_data["db"] in {"connected", "disconnected", "Not connected"} print(f"Response time: {duration_ms:.2f}ms") @@ -1520,7 +1525,7 @@ async def test_health_readiness_returns_503_when_db_disconnected(): result = await health_readiness(response=response) assert response.status_code == 503 - assert result == {"status": "healthy"} + assert result == {"status": "healthy", "db": "disconnected"} @pytest.mark.asyncio @@ -1543,7 +1548,7 @@ async def test_health_readiness_returns_200_when_db_connected(): result = await health_readiness(response=response) assert response.status_code == 200 - assert result == {"status": "healthy"} + assert result == {"status": "healthy", "db": "connected"} @pytest.mark.asyncio @@ -1562,7 +1567,7 @@ async def test_health_readiness_returns_200_when_no_db_configured(): result = await health_readiness(response=response) assert response.status_code == 200 - assert result == {"status": "healthy"} + assert result == {"status": "healthy", "db": "Not connected"} def test_clean_endpoint_data_strips_credentials_keeps_routing_fields():