diff --git a/litellm/proxy/db/exception_handler.py b/litellm/proxy/db/exception_handler.py index 5502543b926..bc905d3c958 100644 --- a/litellm/proxy/db/exception_handler.py +++ b/litellm/proxy/db/exception_handler.py @@ -9,6 +9,21 @@ from litellm.proxy._types import ( ) from litellm.secret_managers.main import str_to_bool +try: + import prisma # optional dependency, only installed when the proxy is generated against a DATABASE_URL + + _prisma_available = True +except ImportError: + # Master-key-only deployments run without a DATABASE_URL and never install + # prisma. The classifiers below must not crash the auth-failure path in that + # mode, so they short-circuit to False when prisma is unavailable. + _prisma_available = False + +# Assigned once so the ALL_CAPS name is not redefined across the try/except +# branches; the module-level ``import prisma`` above is what the classifiers +# below dereference (``prisma.errors.*``) when this flag is True. +PRISMA_AVAILABLE = _prisma_available + # Bounds the __cause__/__context__ walk in is_database_service_unavailable_error_in_chain. # Real exception chains are a few links deep; the cap also makes the walk cycle-safe. _MAX_EXCEPTION_CHAIN_DEPTH: Final = 20 @@ -55,6 +70,11 @@ class PrismaDBExceptionHandler: Reporting decisions want the opposite breadth; use ``is_database_infrastructure_error`` for those. """ + if not PRISMA_AVAILABLE: + # No DATABASE_URL / prisma not installed: there is no DB layer to + # be unavailable, so this cannot be a DB-connectivity failure. + return False + import prisma.engine.errors if isinstance(e, DB_CONNECTION_ERROR_TYPES): @@ -79,7 +99,10 @@ class PrismaDBExceptionHandler: ``RecordNotFoundError``, etc.) are excluded — the DB IS reachable and the request itself is what failed. """ - import prisma + if not PRISMA_AVAILABLE: + # No DATABASE_URL / prisma not installed: there is no DB layer to + # be unavailable, so this cannot be a DB-connectivity failure. + return False data_layer_errors: Final = ( prisma.errors.DataError, @@ -119,7 +142,8 @@ class PrismaDBExceptionHandler: per-row data rejection has to additionally consult ``is_database_service_unavailable_error`` before acting on a True here. """ - import prisma + if not PRISMA_AVAILABLE: + return False return type(e) is prisma.errors.DataError @@ -132,7 +156,8 @@ class PrismaDBExceptionHandler: Use this for reconnect logic — data-layer errors like UniqueViolationError mean the DB IS reachable, so reconnecting would be pointless. """ - import prisma + if not PRISMA_AVAILABLE: + return False if isinstance(e, DB_CONNECTION_ERROR_TYPES): return True @@ -200,7 +225,10 @@ class PrismaDBExceptionHandler: are already classified by type/keyword above, and data-layer ones (the DB IS reachable) must stay 401. """ - import prisma + if not PRISMA_AVAILABLE: + # No prisma engine in play, so no prisma-engine-internal error is + # possible. + return False if isinstance(e, prisma.errors.PrismaError): return False diff --git a/tests/test_litellm/proxy/db/test_exception_handler.py b/tests/test_litellm/proxy/db/test_exception_handler.py index d80e3acb4b8..1ddc1552c03 100644 --- a/tests/test_litellm/proxy/db/test_exception_handler.py +++ b/tests/test_litellm/proxy/db/test_exception_handler.py @@ -428,6 +428,38 @@ def test_handle_db_exception_with_non_db_error(): PrismaDBExceptionHandler.handle_db_exception(regular_error) +# Regression test for https://github.com/BerriAI/litellm/issues/35457 +# +# On a master-key-only proxy (no DATABASE_URL) the optional `prisma` dependency +# is never installed. These classifiers are reached on EVERY auth failure via +# `_user_api_key_auth_builder`, so an unconditional `import prisma` inside them +# raised ModuleNotFoundError and surfaced a 500 where a 401 was expected. With +# prisma unavailable they must instead return False (not a DB error) and not +# raise, so auth failures stay 401. +@pytest.mark.parametrize( + "classifier", + [ + PrismaDBExceptionHandler.is_database_connection_error, + PrismaDBExceptionHandler.is_prisma_data_error, + PrismaDBExceptionHandler.is_database_transport_error, + PrismaDBExceptionHandler.is_prisma_engine_internal_error, + ], +) +def test_classifiers_return_false_when_prisma_unavailable(monkeypatch, classifier): + """ + When prisma is not installed (master-key-only deployment), the classifiers + must return False without raising ModuleNotFoundError. + """ + monkeypatch.setattr( + "litellm.proxy.db.exception_handler.PRISMA_AVAILABLE", False + ) + + # A plain auth failure carries no prisma types; the classifier must handle + # it without touching the (absent) prisma module. + auth_error = Exception("Authentication Error, invalid api key") + assert classifier(auth_error) is False + + def _permanent_prisma_faults(): """Every prisma error class that is not a transient outage and not a data-layer error, built by enumeration so the list cannot drift out of sync