Django 3.0 + Channels + ASGI + TokenAuthMiddleware - django

I upgraded to Django 3.0 and now I get this error when using websockets + TokenAuthMiddleware:
SynchronousOnlyOperation
You cannot call this from an async context - use a thread or sync_to_async.

The problem is that you can't access synchronous code from an asynchronous context. Here is a TokenAuthMiddleware for Django 3.0:
# myproject.myapi.utils.py
from channels.auth import AuthMiddlewareStack
from channels.db import database_sync_to_async
from django.contrib.auth.models import AnonymousUser
from rest_framework.authtoken.models import Token
#database_sync_to_async
def get_user(headers):
try:
token_name, token_key = headers[b'authorization'].decode().split()
if token_name == 'Token':
token = Token.objects.get(key=token_key)
return token.user
except Token.DoesNotExist:
return AnonymousUser()
class TokenAuthMiddleware:
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
return TokenAuthMiddlewareInstance(scope, self)
class TokenAuthMiddlewareInstance:
"""
Yeah, this is black magic:
https://github.com/django/channels/issues/1399
"""
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:
self.scope['user'] = await get_user(headers)
inner = self.inner(self.scope)
return await inner(receive, send)
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
Use it like this:
# myproject/routing.py
from myapi.utils import TokenAuthMiddlewareStack
from myapi.websockets import WSAPIConsumer
application = ProtocolTypeRouter({
"websocket": TokenAuthMiddlewareStack(
URLRouter([
path("api/v1/ws", WSAPIConsumer),
]),
),
})
application = SentryAsgiMiddleware(application)

As #tapion stated this solution doesn't work anymore since channels 3.x
Newer solution can be a little bit tweaked:
class TokenAuthMiddleware:
def __init__(self, inner):
self.inner = inner
async def __call__(self, scope, receive, send):
headers = dict(scope['headers'])
if b'authorization' in headers:
scope['user'] = await get_user(headers)
return await self.inner(scope, receive, send)

Related

problem changing from WebsocketConsumer to AsyncWebsocketConsumer

i'm making a chat app with django,
everything is fine and working with the WebsocketConsumer and async_to_sync, but when i change to the AsyncWebSocket i get an error :
django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.
any idea or suggestion on why is that happening or what i can do here ? thanks for your help.
this is my consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.generic.websocket import WebsocketConsumer
#from asgiref.sync import async_to_sync
from django.utils import timezone
from .models import Message
from django.shortcuts import render, redirect, get_object_or_404
from courses.models import Course
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.user = self.scope['user']
self.id = self.scope['url_route']['kwargs']['course_id']
self.room_group_name = 'chat_%s' % self.id
# join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name)
# accept connection
await self.accept()
async def disconnect(self, close_code):
# leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name)
# receive message from WebSocket
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
now = timezone.now()
course = get_object_or_404(Course,id=self.id)
messages = Message.objects.create(author=self.scope['user'], content=message,course=course)
# send message to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
'user': self.user.username,
'datetime': now.isoformat(),
}
)
# receive message from room group
async def chat_message(self, event):
# Send message to WebSocket
await self.send(text_data=json.dumps(event))
UPDATE
I found the Solution
those 2 lines are the rcause of the error:
course = get_object_or_404(Course,id=self.id)
messages = Message.objects.create(author=self.scope['user'],
content=message,course=course)
i'm using ththem to save messages in the database, aparently it's not possible in the async mode.
so according to the docs this is the modification i added :
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
then my save method:
#database_sync_to_async
def save_message(self,message):
course = get_object_or_404(Course,id=self.id)
messages = Message.objects.create(author=self.scope['user'],
content=message,course=course)
return messages
and finally replace the 2 line (the cause of problem) with this line in the receive() method :
await self.save_message(message)

TypeError: can not serialize 'User' object in Chat Application

I want to integrate a chat app with my instagram-like project. My primary goal is to provide the users of this website with the possibility to chat with each other real-time. I have the following code but I keep getting the error:
TypeError: can not serialize 'User' object
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
import json
from .models import Message
from django.contrib.auth.models import User
class ChatConsumer(WebsocketConsumer):
def fetch_messages(self, data):
messages = Message.last_10_messages()
content = {
'messages': self.messages_to_json(messages)
}
self.send_message(content)
def new_message(self, data):
author = data['from']
author_user = User.objects.get(username = author)
message = Message.objects.create(author=author_user, content=data['message'])
content ={
'command' : 'new_message',
'message': self.message_to_json(message)
}
return self.send_chat_message(content)
def messages_to_json(self, messages):
result = []
for message in messages:
result.append(self.message_to_json(message))
return result
def message_to_json(self, message):
return {
'author' : message.author,
'content' : message.content,
'timestamp': str(message.timestamp)
}
commands = {
'fetch_messages': fetch_messages,
'new_message' : new_message
}
def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = 'chat_%s' % self.room_name
# Join room group
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)
self.accept()
def disconnect(self, close_code):
# Leave room group
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name,
self.channel_name
)
def receive(self, text_data):
data = json.loads(text_data)
self.commands[data['command']](self, data)
def send_chat_message(self, data):
message = data['message']
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': message
}
)
def send_message(self, message):
self.send(text_data=json.dumps(message))
def chat_message(self, event):
message = event['message']
self.send(text_data=json.dumps(message))
The views.py:
from django.shortcuts import render
from django.utils.safestring import mark_safe
import json
def index(request):
return render(request, 'chat/index.html', {})
def room(request, room_name):
return render(request, 'chat/room.html', {
'room_name_json': mark_safe(json.dumps(room_name)),
'username' : mark_safe(json.dumps(request.user.username))
})
The routing.py
from django.urls import re_path, path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
The asgi.py in the project root:
import os
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
import chat.routing
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns
)
),
})
I am following the tutorial on the official website of Django Channels. I am also trying to customise my consumer to save it on database and the model for that is as following:
from django.db import models
from django.contrib.auth.models import User
class Message(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='author')
content = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.author.username
def last_10_messages(self):
return Message.objects.order_by('-timestamp').all()[:10]
self.channel_layer.group_send requires the message dict to contain the following values only:
Byte strings
Unicode strings
Integers (within the signed 64 bit range)
Floating point numbers (within the IEEE 754 double precision range)
Lists (tuples should be encoded as lists)
Dicts (keys must be unicode strings)
Booleans
None
Source: https://channels.readthedocs.io/en/latest/channel_layer_spec.html#messages

Django Channels - Custom Authentication Middleware raises: 'coroutine' object is not callable

I have created a custom token authentication middleware.
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import AnonymousUser
from django.db import close_old_connections
from asgiref.sync import sync_to_async
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
sync_to_async(close_old_connections)()
headers = dict(scope['headers'])
try:
token_name, token_key = headers[b'sec-websocket-protocol'].decode().split(', ')
if token_name == 'Token':
token = sync_to_async(Token.objects.get, thread_sensitive=True)(key=token_name)
scope['user'] = token.user
else:
scope['user'] = AnonymousUser()
except Token.DoesNotExist:
scope['user'] = AnonymousUser()
return self.inner(scope)
When I run it, an exception happens when I run scope['user'] = token.user
[Failure instance: Traceback: <class 'AttributeError'>: 'coroutine' object has no attribute 'user'
I tried awaiting the Token query like this:
token = await sync_to_async(Token.objects.get, thread_sensitive=True)(key=token_name)
and I added async in front of the __call__ function, but then the following error is raised before any of the code inside the __call__ function runs:
[Failure instance: Traceback: <class 'TypeError'>: 'coroutine' object is not callable
I am using Django v3.0.6 and Django Channels v2.4.0
Here is the solution that worked for me:
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import AnonymousUser
from channels.db import database_sync_to_async
#database_sync_to_async
def get_user(token):
try:
return Token.objects.get(key=token).user
except Token.DoesNotExist:
return AnonymousUser()
class TokenAuthMiddleware:
"""
Token authorization middleware for Django Channels 2
"""
def __init__(self, inner):
# Store the ASGI application we were passed
self.inner = inner
def __call__(self, scope):
return TokenAuthMiddlewareInstance(scope, self)
class TokenAuthMiddlewareInstance:
"""
Inner class that is instantiated once per scope.
"""
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'])
token_name, token_key = headers[b'sec-websocket-protocol'].decode().split(', ')
if token_name == 'Token':
self.scope['user'] = await get_user(token_key)
else:
self.scope['user'] = AnonymousUser()
# Instantiate our inner application
inner = self.inner(self.scope)
return await inner(receive, send)
Just wrap your function in database_sync_to_async, it will handle the connections for you
class TokenAuthMiddleware:
"""
Token authorization middleware for Django Channels 2
"""
def __init__(self, inner):
self.inner = inner
async def __call__(self, scope):
# Close old database connections to prevent usage of timed out connections
sync_to_async(close_old_connections)()
headers = dict(scope['headers'])
try:
token_name, token_key = headers[b'sec-websocket-protocol'].decode().split(', ')
if token_name == 'Token':
user = await self.get_user(token)
scope['user'] = user
else:
scope['user'] = AnonymousUser()
except Token.DoesNotExist:
scope['user'] = AnonymousUser()
#database_sync_to_async
def get_user(self, token):
token = Token.ojbects.get(key=token)
return token.user
In case it helps anyone, I was having the same issue along with others. I updated django channels and then changed my routing from
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer),
]
to
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
You need django channels 3.0+ to do this (https://channels.readthedocs.io/en/stable/releases/3.0.0.html). Then you can follow https://channels.readthedocs.io/en/stable/topics/authentication.html#django-authentication to setup your custom middleware.
Reference:
Django Channel Custom Authentication Middleware __call__() missing 2 required positional arguments: 'receive' and 'send'

Django Channels middleware use ORM

I have troubles checking the user token inside of middleware. I'm getting token from cookies and then I need to query database to check if this token exists and belongs to user that made a request.
routing.py
from channels.routing import ProtocolTypeRouter, URLRouter
import game.routing
from authentication.utils import TokenAuthMiddlewareStack
application = ProtocolTypeRouter({
# (http->django views is added by default)
'websocket': TokenAuthMiddlewareStack(
URLRouter(
game.routing.websocket_urlpatterns
)
),
})
middleware.py
from rest_framework.authentication import TokenAuthentication
from rest_framework.exceptions import AuthenticationFailed
from rest_auth.models import TokenModel
from channels.auth import AuthMiddlewareStack
from django.contrib.auth.models import AnonymousUser
from django.db import close_old_connections
...
class TokenAuthMiddleware:
"""
Token authorization middleware for Django Channels 2
"""
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
close_old_connections()
headers = dict(scope['headers'])
if b'Authorization' in headers[b'cookie']:
try:
cookie_str = headers[b'cookie'].decode('utf-8')
try: # no cookie Authorization=Token in the request
token_str = [x for x in cookie_str.split(';') if re.search(' Authorization=Token', x)][0].strip()
except IndexError:
scope['user'] = AnonymousUser()
return self.inner(scope)
token_name, token_key = token_str.replace('Authorization=', '').split()
if token_name == 'Token':
token = TokenModel.objects.get(key=token_key)
scope['user'] = token.user
except TokenModel.DoesNotExist:
scope['user'] = AnonymousUser()
return self.inner(scope)
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
And this gives me
django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.
I also tried the following approaches
async def __call__(self, scope):
...
if token_name == 'Token':
token = await self.get_token(token_key)
scope['user'] = token.user
...
# approach 1
#sync_to_async
def get_token(self, token_key):
return TokenModel.objects.get(key=token_key)
# approach 2
#database_sync_to_async
def get_token(self, token_key):
return TokenModel.objects.get(key=token_key)
Those approaches give the following error
[Failure instance: Traceback: <class 'TypeError'>: 'coroutine' object is not callable
/Users/nikitatonkoshkur/Documents/work/svoya_igra/venv/lib/python3.8/site-packages/autobahn/websocket/protocol.py:2847:processHandshake
/Users/nikitatonkoshkur/Documents/work/svoya_igra/venv/lib/python3.8/site-packages/txaio/tx.py:366:as_future
/Users/nikitatonkoshkur/Documents/work/svoya_igra/venv/lib/python3.8/site-packages/twisted/internet/defer.py:151:maybeDeferred
/Users/nikitatonkoshkur/Documents/work/svoya_igra/venv/lib/python3.8/site-packages/daphne/ws_protocol.py:72:onConnect
--- <exception caught here> ---
/Users/nikitatonkoshkur/Documents/work/svoya_igra/venv/lib/python3.8/site-packages/twisted/internet/defer.py:151:maybeDeferred
/Users/nikitatonkoshkur/Documents/work/svoya_igra/venv/lib/python3.8/site-packages/daphne/server.py:206:create_application
]```
I am not sure if it will work or not,but you can try
First write the get_token function outside the class.
# approach 1
#sync_to_async
def get_token(self, token_key):
return TokenModel.objects.get(key=token_key)
then in your async function write get_token() instead of self.get_token()
async def __call__(self, scope):
...
if token_name == 'Token':
token = await get_token(token_key)
scope['user'] = token.user
...
Having looked at the source code of django-channels, especially at how SessionMiddleware works and uses django ORM, I ended up writing my TokenMiddleware in a similar fashion.
class TokenAuthMiddlewareInstance:
"""
Token authorization middleware for Django Channels 2
"""
def __init__(self, scope, middleware):
self.middleware = middleware
self.scope = dict(scope)
self.inner = self.middleware.inner
async def __call__(self, receive, send):
close_old_connections()
headers = dict(self.scope.get('headers', {}))
if b'cookie' in headers:
cookie_dict = parse_cookie(headers[b'cookie'].decode("ascii"))
token = cookie_dict.get('Authorization', '')
token_name, token_key = token.replace('Authorization=', '').split()
if token_name == 'Token':
self.scope['user'] = await self.get_user_from_token(token_key)
inner = self.inner(self.scope)
return await inner(receive, send)
inner = self.inner(self.scope)
self.scope['user'] = AnonymousUser()
return await inner(receive, send)
#staticmethod
#database_sync_to_async
def get_user_from_token(token_key):
try:
return TokenModel.objects.select_related('user').get(key=token_key).user
except TokenModel.DoesNotExist:
return AnonymousUser()
class TokenAuthMiddleware:
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
return TokenAuthMiddlewareInstance(scope, self)
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))

Send notification on post_save signal in django

I have a model called deposit and I am trying to send real time notification when a new row is added in table. Is it possible to do so using django-channels ?
You can use the save method of your Django model, like so:
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
class Info(models.Model):
# your model fields
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
super(Info, self).save(force_insert, force_update, using, update_fields)
# send info to channel
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
'infochannel',
{
'type': 'infochannel.message',
'device_id': str(self.device_id)
}
)
And in the consumer:
from channels.generic.websocket import AsyncWebsocketConsumer
class DataConsumer(AsyncWebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.group_name = ''
async def connect(self):
# we are using one fixed group
self.group_name = 'infochannel'
await self.channel_layer.group_add(
self.group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
await self.channel_layer.group_discard('infochannel', self.channel_name)
async def infochannel_message(self, event):
# Send message to websocket group
await self.send(text_data=event['device_id'])
Where device_id is a field on my model, and of course you also have to set up routing, redis_channels, and so on.
You can simply add a signal for a post_save
#receiver(post_save, sender=Deposit)
def signal_deposit_save(sender, instance, created, **kwargs):
if created: # This means it is a new row
# Send notification using django-channels