diff --git a/src/fastapi_multiauth/utils.py b/src/fastapi_multiauth/utils.py index 25e58d4..adcc34f 100644 --- a/src/fastapi_multiauth/utils.py +++ b/src/fastapi_multiauth/utils.py @@ -6,6 +6,7 @@ import inspect from typing import Any, Callable +import anyio.to_thread from fastapi import HTTPException, Request @@ -43,7 +44,7 @@ def ensure_async(fn: Callable[..., Any]) -> Callable[..., Any]: @functools.wraps(fn) async def wrapper(*args: Any, **kwargs: Any) -> Any: - result = fn(*args, **kwargs) + result = await anyio.to_thread.run_sync(functools.partial(fn, *args, **kwargs)) if inspect.isawaitable(result): return await result return result diff --git a/tests/test_multiauth.py b/tests/test_multiauth.py index 1ffaa3e..e97442c 100644 --- a/tests/test_multiauth.py +++ b/tests/test_multiauth.py @@ -3443,6 +3443,19 @@ def factory_validator(token: str): response = client.get("/me", headers={"Authorization": f"Bearer {VALID_TOKEN}"}) assert response.status_code == 200 + async def test_sync_validator_runs_in_worker_thread(self): + """A blocking sync validator must not run on the event loop.""" + import threading + + from fastapi_multiauth.utils import ensure_async + + loop_thread = threading.get_ident() + + def validator(token: str) -> int: + return threading.get_ident() + + assert await ensure_async(validator)("x") != loop_thread + class TestJWTValidatorJWKSRobustness: JWKS_URL = "https://idp.example.com/jwks"