Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Xdynix opened this issue Aug 3, 2024 · 1 comment
Open

Comments

@Xdynix
Copy link

Xdynix commented Aug 3, 2024

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):

  • Python version: 3.12
  • Django version: 5.0.7
  • Django-Ninja version: 1.2.2
  • Pydantic version: 2.8.2
@Xdynix
Copy link
Author

Xdynix commented Aug 4, 2024

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()).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant