fix(proxy): add is_database_transport_error for reconnect logic, restore broad is_database_connection_error

#21706 narrowed is_database_connection_error to only match PrismaErrors
with connectivity keywords. That was correct for the reconnect use case
but broke the allow_requests_on_db_unavailable behavior as a side effect.

Fix: add is_database_transport_error with the narrow keyword-gated check
and use it in the two reconnect call sites (auth_checks key lookup retry,
utils.py watchdog). Restore is_database_connection_error to its original
broad behavior (any PrismaError) so allow_requests_on_db_unavailable works
correctly.

Update the tests added in #21706 to assert against is_database_transport_error
instead of is_database_connection_error.
This commit is contained in:
Ishaan Jaffer 2026-02-21 10:51:18 -08:00
parent a5e886de79
commit 6b818ab3a1
5 changed files with 30 additions and 10 deletions

View file

@ -2000,7 +2000,7 @@ async def _fetch_key_object_from_db_with_reconnect(
proxy_logging_obj=proxy_logging_obj,
)
except Exception as e:
if PrismaDBExceptionHandler.is_database_connection_error(e):
if PrismaDBExceptionHandler.is_database_transport_error(e):
did_reconnect = False
if hasattr(prisma_client, "attempt_db_reconnect"):
auth_reconnect_timeout = getattr(

View file

@ -32,7 +32,28 @@ class PrismaDBExceptionHandler:
@staticmethod
def is_database_connection_error(e: Exception) -> bool:
"""
Returns True if the exception is from a database outage / connection error
Returns True if the exception is from a database outage / connection error.
Any PrismaError qualifies the DB failed to serve the request.
Used by allow_requests_on_db_unavailable logic.
"""
import prisma
if isinstance(e, DB_CONNECTION_ERROR_TYPES):
return True
if isinstance(e, prisma.errors.PrismaError):
return True
if isinstance(e, ProxyException) and e.type == ProxyErrorTypes.no_db_connection:
return True
return False
@staticmethod
def is_database_transport_error(e: Exception) -> bool:
"""
Returns True only for transport/connectivity failures where a reconnect
attempt makes sense (e.g. DB is unreachable, connection dropped).
Use this for reconnect logic data-layer errors like UniqueViolationError
mean the DB IS reachable, so reconnecting would be pointless.
"""
import prisma
@ -44,8 +65,6 @@ class PrismaDBExceptionHandler:
return True
if isinstance(e, prisma.errors.PrismaError):
error_message = str(e).lower()
# Treat generic PrismaError as connection error only when its text
# clearly indicates transport/connectivity failure.
connection_keywords = (
"can't reach database server",
"cannot reach database server",

View file

@ -3578,8 +3578,8 @@ class PrismaClient:
"""
try:
engine = self.db._original_prisma._engine # type: ignore[attr-defined]
if engine is not None and engine.process is not None:
return engine.process.pid
if engine is not None and engine.process is not None: # type: ignore[union-attr]
return engine.process.pid # type: ignore[union-attr]
except (AttributeError, TypeError):
pass
@ -4125,7 +4125,7 @@ class PrismaClient:
except Exception as e:
if isinstance(
e, asyncio.TimeoutError
) or PrismaDBExceptionHandler.is_database_connection_error(e):
) or PrismaDBExceptionHandler.is_database_transport_error(e):
await self.attempt_db_reconnect(
reason="db_health_watchdog_connection_error",
timeout_seconds=self._db_watchdog_reconnect_timeout_seconds,

View file

@ -72,8 +72,9 @@ def test_is_database_connection_error_prisma_connection_errors(prisma_error):
),
],
)
def test_is_database_connection_error_non_connection_prisma_errors(prisma_error):
assert PrismaDBExceptionHandler.is_database_connection_error(prisma_error) == False
def test_is_database_transport_error_non_connection_prisma_errors(prisma_error):
"""Data-layer errors should not trigger reconnect — DB is reachable when these occur."""
assert PrismaDBExceptionHandler.is_database_transport_error(prisma_error) == False
def test_is_database_connection_generic_errors():

View file

@ -215,7 +215,7 @@ async def test_db_health_watchdog_should_trigger_reconnect_on_db_error(mock_prox
"litellm.proxy.utils.asyncio.sleep",
AsyncMock(side_effect=[None, asyncio.CancelledError()]),
), patch(
"litellm.proxy.db.exception_handler.PrismaDBExceptionHandler.is_database_connection_error",
"litellm.proxy.db.exception_handler.PrismaDBExceptionHandler.is_database_transport_error",
return_value=True,
):
await client._db_health_watchdog_loop()