9

I had a django 2 app and i used django channels for socket connection.

i just update django to version 3. and now daphne show this error when i try to make a socket connection. i had not any problem with django 2.

[Failure instance: Traceback: <class 'django.core.exceptions.SynchronousOnlyOperation'>: You cannot call this from an async context - use a thread or sync_to_async.
/home/ubuntu/pl_env/lib/python3.6/site-packages/autobahn/websocket/protocol.py:2844:processHandshake
/home/ubuntu/pl_env/lib/python3.6/site-packages/txaio/tx.py:429:as_future
/home/ubuntu/pl_env/lib/python3.6/site-packages/twisted/internet/defer.py:151:maybeDeferred
/home/ubuntu/pl_env/lib/python3.6/site-packages/daphne/ws_protocol.py:83:onConnect
--- <exception caught here> ---
/home/ubuntu/pl_env/lib/python3.6/site-packages/twisted/internet/defer.py:151:maybeDeferred
/home/ubuntu/pl_env/lib/python3.6/site-packages/daphne/server.py:201:create_application
/home/ubuntu/pl_env/lib/python3.6/site-packages/channels/routing.py:54:__call__
/home/ubuntu/pl_env/lib/python3.6/site-packages/channels/security/websocket.py:37:__call__
/home/ubuntu/petroline_django/orders/token_auth.py:25:__call__
/home/ubuntu/pl_env/lib/python3.6/site-packages/django/db/models/manager.py:82:manager_method
/home/ubuntu/pl_env/lib/python3.6/site-packages/django/db/models/query.py:411:get
/home/ubuntu/pl_env/lib/python3.6/site-packages/django/db/models/query.py:258:__len__
/home/ubuntu/pl_env/lib/python3.6/site-packages/django/db/models/query.py:1261:_fetch_all
/home/ubuntu/pl_env/lib/python3.6/site-packages/django/db/models/query.py:57:__iter__
/home/ubuntu/pl_env/lib/python3.6/site-packages/django/db/models/sql/compiler.py:1142:execute_sql
/home/ubuntu/pl_env/lib/python3.6/site-packages/django/utils/asyncio.py:24:inner

it says the problem is in token_auth.py, line 25. this line is token = Token.objects.get(key=token_key)

this is my token_auth.py that handles token authentication.

from channels.auth import AuthMiddlewareStack
from django.contrib.auth.models import AnonymousUser
from django.db import close_old_connections
from rest_framework.authtoken.models import Token


class TokenAuthMiddleware:
    """
    Token authorization middleware for Django Channels 2
    see:
    https://channels.readthedocs.io/en/latest/topics/authentication.html#custom-authentication
    """

    def __init__(self, inner):
        self.inner = inner

    def __call__(self, scope):
        headers = dict(scope['headers'])
        if b'authorization' in headers:
            try:
                token_name, token_key = headers[b'authorization'].decode().split()
                if token_name == 'Token':
                    # Close old database connections to prevent usage of timed out connections
                    close_old_connections()
                    token = Token.objects.get(key=token_key)
                    scope['user'] = token.user
            except Token.DoesNotExist:
                scope['user'] = AnonymousUser()
        return self.inner(scope)

TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
AlexMercer
  • 301
  • 3
  • 10

3 Answers3

5

Thanks to @ivissani answers, i fixed my TokenAuthMiddleware with some of SessionMiddleware codes.

I have opened an issue for django channels about updating docs.

@database_sync_to_async
def get_user(token_key):
    try:
        return Token.objects.get(key=token_key).user
    except Token.DoesNotExist:
        return AnonymousUser()


class TokenAuthMiddleware:
    """
    Token authorization middleware for Django Channels 2
    see:
    https://channels.readthedocs.io/en/latest/topics/authentication.html#custom-authentication
    """

    def __init__(self, inner):
        self.inner = inner

    def __call__(self, scope):
        return TokenAuthMiddlewareInstance(scope, self)


class TokenAuthMiddlewareInstance:
    def __init__(self, scope, middleware):
        self.middleware = middleware
        self.scope = dict(scope)
        self.inner = self.middleware.inner

    async def __call__(self, receive, send):
        headers = dict(self.scope['headers'])
        if b'authorization' in headers:
            token_name, token_key = headers[b'authorization'].decode().split()
            if token_name == 'Token':
                self.scope['user'] = await get_user(token_key)
        inner = self.inner(self.scope)
        return await inner(receive, send) 


TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
Community
  • 1
  • 1
AlexMercer
  • 301
  • 3
  • 10
5

Fixed by using @database_sync_to_async decorator:

(see https://github.com/MathieuB1/KOREK-backend/commit/ff6a4b542cda583a1d5abbf200a5d57ef328cae0#diff-95e545fb374a9ed7e8af8c31087a3f29)

import jwt, re
import traceback
from channels.auth import AuthMiddlewareStack
from channels.db import database_sync_to_async
from django.contrib.auth.models import AnonymousUser
from django.conf import LazySettings
from jwt import InvalidSignatureError, ExpiredSignatureError, DecodeError
from django.contrib.auth.models import User
from django.contrib.sessions.models import Session

settings = LazySettings()

from django.db import close_old_connections

@database_sync_to_async
def close_connections():
    close_old_connections()

@database_sync_to_async
def get_user(user_jwt):
    try:
        return User.objects.get(id=user_jwt)
    except User.DoesNotExist:
        return AnonymousUser()


class TokenAuthMiddleware:
    """
    Token authorization middleware for Django Channels 2
    """
    def __init__(self, inner):
        self.inner = inner

    def __call__(self, scope):
        # Close old database connections to prevent usage of timed out connections
        close_connections()

        # Login with JWT
        try:
            if scope['subprotocols'][0] != 'None':

                token = scope['subprotocols'][0]

                try:
                    user_jwt = jwt.decode(
                        token,
                        settings.SECRET_KEY,
                    )
                    scope['user'] = get_user(user_jwt['user_id'])
                    return self.inner(scope)

                except (InvalidSignatureError, KeyError, ExpiredSignatureError, DecodeError):
                    traceback.print_exc()
                    pass
                except Exception as e:
                    traceback.print_exc()
            else:
                raise
Tiago Martins Peres
  • 14,289
  • 18
  • 86
  • 145
3

Refer to this part of the documentation. There it is explained that Django 3 will raise such exception if you try to use the ORM from within an async context (which seems to be the case).

As Django Channels documentation explains solution would be to use sync_to_async as follows:

from channels.db import database_sync_to_async


class TokenAuthMiddleware:
    # more code here
    async def __call__(self, scope):
        # and some more code here
        token = await database_sync_to_async(Token.objects.get(key=token_key))()

Although please bear in mind that I haven't used this in my life, so it may fail.

Note that in the Django channels documentation it says that you need to write your query in a separate method. So if this fails try doing that.

ivissani
  • 2,614
  • 1
  • 18
  • 12
  • Try that and let me know what happens – ivissani Jan 07 '20 at 22:14
  • i think "await" can't be used in sync function. it must be used in a async. async __call__(self, scope): .... – AlexMercer Jan 08 '20 at 08:48
  • As far as I can see in the [django channels code](https://github.com/django/channels/blob/dd304566775d6f1ce3d40a4d6e2a100f33c54889/channels/sessions.py) you should be able to do `async def __call__(self, scope):...` in your middleare. Did you try it? – ivissani Jan 08 '20 at 12:31
  • `async def __call__` is not in SessionMiddleware directly. it's in SessionMiddlewareInstance. but i used codes of SessionMiddleware to rewrite my own custom authentication middleware. thank you @ivissani – AlexMercer Jan 08 '20 at 20:03