diff --git a/litellm/proxy/guardrails/guardrail_hooks/presidio.py b/litellm/proxy/guardrails/guardrail_hooks/presidio.py index 7e2f6bfec33..a4a72cd7941 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/proxy/guardrails/guardrail_hooks/presidio.py @@ -389,7 +389,8 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): async with self._session_lock: if self._http_session is None or self._http_session.closed: self._http_session = aiohttp.ClientSession() - yield self._http_session + session = self._http_session + yield session else: # Background thread/loop -> use loop-bound session cache # This avoids "attached to a different loop" or "no running event loop" errors diff --git a/tests/unit/proxy/guardrails/guardrail_hooks/test_presidio.py b/tests/unit/proxy/guardrails/guardrail_hooks/test_presidio.py index e312378969a..2c664940c4e 100644 --- a/tests/unit/proxy/guardrails/guardrail_hooks/test_presidio.py +++ b/tests/unit/proxy/guardrails/guardrail_hooks/test_presidio.py @@ -5068,3 +5068,24 @@ def test_get_session_iterator_reuses_one_session_per_background_loop( assert len(sessions) == 10 assert all(session is sessions[0] for session in sessions) assert presidio_guardrail._http_session is None + + +@pytest.mark.asyncio +async def test_get_session_iterator_does_not_hold_lock_while_session_is_in_use( + presidio_guardrail: OPTIONAL_PresidioPIIMasking, +) -> None: + both_entered = asyncio.Event() + active_users = 0 + + async def use_session() -> None: + nonlocal active_users + async with presidio_guardrail._get_session_iterator(): + active_users += 1 + if active_users == 2: + both_entered.set() + await asyncio.wait_for(both_entered.wait(), timeout=1) + + try: + await asyncio.gather(use_session(), use_session()) + finally: + await presidio_guardrail._close_http_session()