From 4a2b0c0696bd276fc8c822734dd018e41637ae81 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 26 Jun 2026 00:42:32 +0000 Subject: [PATCH] fix(jwt): surface DB-fallback membership lookup failures at warning level A transient get_team_membership failure on the DB team fallback path is recoverable: the team is still resolved and the request proceeds, just without per-team membership budget enforcement for that request. Logging that at debug hid a silent budget-enforcement gap from operators, so it now logs at warning and states that enforcement was skipped. Behavior is otherwise unchanged: the resolved team is returned with a None membership rather than failing the request, covered by test_resolve_db_team_fallback_survives_membership_lookup_error. --- litellm/proxy/auth/handle_jwt.py | 5 +- .../proxy/auth/test_handle_jwt.py | 50 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/auth/handle_jwt.py b/litellm/proxy/auth/handle_jwt.py index a39854fa309..df3463b9b3e 100644 --- a/litellm/proxy/auth/handle_jwt.py +++ b/litellm/proxy/auth/handle_jwt.py @@ -2038,8 +2038,9 @@ class JWTAuthManager: ), ) except Exception: - verbose_proxy_logger.debug( - "JWT DB team fallback: membership lookup failed for team_id=%s", + verbose_proxy_logger.warning( + "JWT DB team fallback: membership lookup failed for team_id=%s; " + "proceeding without per-team membership budget enforcement", candidate_team_id, exc_info=True, ) diff --git a/tests/test_litellm/proxy/auth/test_handle_jwt.py b/tests/test_litellm/proxy/auth/test_handle_jwt.py index d9e02329fd4..79ce29e534d 100644 --- a/tests/test_litellm/proxy/auth/test_handle_jwt.py +++ b/tests/test_litellm/proxy/auth/test_handle_jwt.py @@ -4827,6 +4827,56 @@ async def test_resolve_db_team_fallback_loads_team_membership(): assert team_membership.budget_id == "budget_xyz" +@pytest.mark.asyncio +async def test_resolve_db_team_fallback_survives_membership_lookup_error(): + """A transient membership-lookup failure must not deny an otherwise-authorized + request: the resolved team is still returned with a None membership (budget + enforcement degrades gracefully) instead of the error propagating as a 403.""" + user_object = LiteLLM_UserTable( + user_id="u_flaky", + user_role=LitellmUserRoles.INTERNAL_USER, + teams=["team_flaky"], + ) + + async def fake_get_team(team_id, **kwargs): + return LiteLLM_TeamTable(team_id=team_id) + + async def boom_get_membership(user_id, team_id, **kwargs): + raise Exception("transient DB error") + + with ( + patch( + "litellm.proxy.auth.handle_jwt.get_team_object", + new_callable=AsyncMock, + side_effect=fake_get_team, + ), + patch( + "litellm.proxy.auth.handle_jwt.get_team_membership", + new_callable=AsyncMock, + side_effect=boom_get_membership, + ), + ): + ( + team_id, + team_object, + team_membership, + ) = await JWTAuthManager._resolve_db_team_fallback( + user_object=user_object, + user_id="u_flaky", + requested_model=None, + enforce_team_based_model_access=True, + team_id_upsert=False, + prisma_client=None, + user_api_key_cache=MagicMock(), + parent_otel_span=None, + proxy_logging_obj=MagicMock(), + ) + + assert team_id == "team_flaky" + assert team_object is not None + assert team_membership is None + + @pytest.mark.asyncio async def test_auth_builder_db_fallback_does_not_validate_rbac_team_against_db_membership(): """When fallback_to_db_teams is on and the JWT carries an RBAC team role but no