django / channels

Developer-friendly asynchrony for Django
https://channels.readthedocs.io
BSD 3-Clause "New" or "Revised" License
6.08k stars 801 forks source link

ProtocolTypeRouter.__call__() missing 1 required positional argument: 'send' #2050

Closed style77 closed 1 year ago

style77 commented 1 year ago

I have Django application with websockets (channels). When i run this application with gunicorn core.asgi:application i experience this error

Traceback (most recent call last):
Sep 18 02:20:16 PM    File "/opt/render/project/src/.venv/lib/python3.11/site-packages/gunicorn/workers/sync.py", line 135, in handle
Sep 18 02:20:16 PM      self.handle_request(listener, req, client, addr)
Sep 18 02:20:16 PM    File "/opt/render/project/src/.venv/lib/python3.11/site-packages/gunicorn/workers/sync.py", line 178, in handle_request
Sep 18 02:20:16 PM      respiter = self.wsgi(environ, resp.start_response)
Sep 18 02:20:16 PM                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Sep 18 02:20:16 PM  TypeError: ProtocolTypeRouter.__call__() missing 1 required positional argument: 'send'

I tried looking at all of the stackoverflow or gh issues pages that were in similiar situation but nothing helped (they mostly talk about adding .as_asgi() which i already done). Also it's important to add that when i run my app with python manage.py runserver It does work fine, only on gunicorn it does not.

chat/routing.py

from django.urls import path
from .consumers import ChatConsumer

websocket_urlpatterns = [
    path(r"ws/chat/<str:streamer_username>/", ChatConsumer.as_asgi()),
]

core/asgi.py

import os

from django.core.asgi import get_asgi_application

from channels.security.websocket import AllowedHostsOriginValidator
from channels.routing import ProtocolTypeRouter, URLRouter

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "streamx.settings")
asgi_application = get_asgi_application()

from chat import routing  # noqa: E402
from chat.middleware import JwtAuthMiddlewareStack  # noqa: E402

application = ProtocolTypeRouter(
    {
        "http": asgi_application,
        "websocket": AllowedHostsOriginValidator(
            JwtAuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns))
        ),
    }
)

chat/consumers.py

import json
from django.contrib.auth import get_user_model
from asgiref.sync import sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer

from .models import ChatMessage
from collections import defaultdict

user_model = get_user_model()

class ChatConsumer(AsyncWebsocketConsumer):
    connected_clients = defaultdict(set)

    async def connect(self):
        self.group_name = self.scope["url_route"]["kwargs"]["streamer_username"]
        self.user = self.scope["user"]

        await self.channel_layer.group_add(self.group_name, str(self.channel_name))
        await self.accept()

        ChatConsumer.connected_clients[self.group_name].add(self.user.username)
        await self.send_connected_clients()
        await self.send_group_connected_clients()

    async def disconnect(self, close_code):
        await self.channel_layer.group_discard(self.group_name, self.channel_name)

        ChatConsumer.connected_clients[self.group_name].remove(self.user.username)
        await self.send_group_connected_clients()

    async def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json["message"]

        message_obj = await self.create_message(self.user, self.group_name, message)
        if not message_obj:
            return

        # Prevent anonymous users from sending messages
        if self.user.is_anonymous:
            return

        await self.channel_layer.group_send(
            self.group_name,
            {
                "type": "send_message",
                "message": message,
                "user": {
                    "username": self.user.username,
                    "id": self.user.id,
                },
            },
        )

    @sync_to_async
    def create_message(self, author, channel, content):
        channel_user = user_model.objects.get(username=channel)
        if channel_user is None:
            return None

        return ChatMessage.objects.create(
            author=author, channel=channel_user, content=content
        )

    async def send_message(self, event):
        await self.send(
            text_data=json.dumps(
                {"message": event["message"], "username": event["user"]["username"]}
            )
        )

    async def send_connected_clients(self):
        await self.send(
            text_data=json.dumps(
                {
                    "connected_clients": list(
                        ChatConsumer.connected_clients[self.group_name]
                    ),
                }
            )
        )

    async def send_group_connected_clients(self):
        await self.channel_layer.group_send(
            self.group_name,
            {
                "type": "group_connected_clients",
            },
        )

    async def group_connected_clients(self, event):
        await self.send_connected_clients()
carltongibson commented 1 year ago

You have to run Channels apps with an ASGI server, such as Daphne. (The early steps of the tutorial cover this if you need to refresh)