vitalik / django-ninja

💨 Fast, Async-ready, Openapi, type hints based framework for building APIs
https://django-ninja.dev
MIT License
6.94k stars 420 forks source link

[BUG] Sync-only Authentication Callbacks not Working on Async Operations #1251

Open Xdynix opened 1 month ago

Xdynix commented 1 month ago

Describe the bug

If an authentication callback can only works in sync context, then it will not work on async views.

Example Code:

from ninja.security import django_auth

api = NinjaAPI()

@api.get("/foobar", auth=django_auth)
async def foobar(request) -> str:
    return "foobar"

Accessing the view will raise: django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.

This is because accessing request.user involves DB query (when using Django's default session engine) and can only be run in sync context. But in AsyncOperation._run_authentication() it didn't switch the context for the authentication callback.

Versions (please complete the following information):

Xdynix commented 1 month ago

This issue is probably beyond the scope of Django Ninja, but I think it would be helpful if the documentation mentioned it.

My current workaround is to create a decorator like this:

from asyncio import get_running_loop
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor
from functools import wraps
from typing import Any, TypeVar, cast

F = TypeVar("F", bound=Callable[..., Any])

def ensure_sync_context(func: F) -> F:
    """Run function with its own thread when in async context."""

    @wraps(func)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        try:
            get_running_loop()
        except RuntimeError:
            return func(*args, **kwargs)
        else:
            with ThreadPoolExecutor(max_workers=1) as executor:
                return executor.submit(func, *args, **kwargs).result()

    return cast(F, wrapper)

Then for callbacks that I am sure will perform async-unsafe operations, I will add this decorator (such as SessionAuth.authenticate()).