Search code examples
reactjsdjangowebsocketcors

React and Django connection with channel setup - WebSocket connection failed


I'ved tried to setup Django with channels to provide notification to React.

https://github.com/axilaris/react-django-channels <-- I have put my project code here.

in backend/backend/settings.py

INSTALLED_APPS = [
..
    'daphne',
    'channels',
]        

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels.layers.InMemoryChannelLayer'
    }
}

ASGI_APPLICATION = 'user_api.routing.application'

in backend/backend/asgi.py (I didnt touch anything)

import os
from django.core.asgi import get_asgi_application
from django.urls import path

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')

application = get_asgi_application()

in backend/user_api/routing.py

from channels.routing import ProtocolTypeRouter, URLRouter
from django.urls import path
from . import consumers

from django.core.asgi import get_asgi_application
django_asgi_app = get_asgi_application()

application = ProtocolTypeRouter({
    "http": django_asgi_app,
    "websocket": URLRouter([
        path("ws/notifications/", consumers.NotificationConsumer.as_asgi()),
    ]),
})

in backend/user_api/consumers.py

from channels.generic.websocket import AsyncWebsocketConsumer
import json
    
class NotificationConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        print("XXX connect")
        await self.accept()

    async def disconnect(self, close_code):
        print("XXX disconnect")
        pass

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

        await self.send(text_data=json.dumps({
            'message': message
        }))

    async def notification_message(self, event):
        print("XXX notification_message")
        await self.send(text_data=json.dumps(event["text"]))        

Finally in React, App.js

useEffect(() => {

    const ws = new WebSocket('ws://localhost:8000/ws/notification/');

    ws.onopen = () => {
      console.log('Connected to notification websocket');
    };

    ws.onmessage = e => {
      const data = JSON.parse(e.data);
      setMessage(data.message);
    };

    ws.onerror = e => {
      console.error('WebSocket error', e);
    };

    ws.onclose = e => {
      console.error('WebSocket closed', e);
    };

    return () => {
      ws.close();
    };

  }, []);

In views.py (when press login submit, it should trigger a notification websocket to React)

class UserLogin(APIView):
    permission_classes = (permissions.AllowAny,)
    authentication_classes = (SessionAuthentication,)
    ##
    def post(self, request):
        print("YYY UserLogin")
        logging.debug("XXX UserLogin")

        channel_layer = get_channel_layer()
        async_to_sync(channel_layer.group_send)(
            'notifications',  
            {
                'type': 'notification_message',
                'text': 'test send',
            }
        )

        return Response({"email": "[email protected]"}, status=status.HTTP_200_OK)

Note that React is running on port 3000 and Django is on port 8000

% npm start <-- React

% python manage.py runserver <-- Django

logs from django and react https://gist.github.com/axilaris/3e3498ae670514c45ba6a36d8511c797

react logs

App.js:79 WebSocket connection to 'ws://localhost:8000/ws/notifications/' failed: WebSocket is closed before the connection is established.
App.js:71 WebSocket error  Event {isTrusted: true, type: 'error', target: WebSocket, currentTarget: WebSocket, eventPhase: 2, …}
App.js:75 WebSocket closed  CloseEvent {isTrusted: true, wasClean: false, code: 1006, reason: '', type: 'close', …}
localhost/:1 Error while trying to use the following icon from the Manifest: http://localhost:3000/logo192.png (Download error or resource isn't a valid image)

App.js:62 Connected to notification websocket

django logs

System check identified 1 issue (0 silenced).
March 28, 2024 - 11:01:17
Django version 4.1.5, using settings 'backend.settings'
Starting ASGI/Daphne version 4.1.0 development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
WebSocket HANDSHAKING /ws/notifications/ [127.0.0.1:57763]
INFO:django.channels.server:WebSocket HANDSHAKING /ws/notifications/ [127.0.0.1:57763]
WebSocket DISCONNECT /ws/notifications/ [127.0.0.1:57763]
INFO:django.channels.server:WebSocket DISCONNECT /ws/notifications/ [127.0.0.1:57763]
XXX connect
XXX disconnect
WebSocket HANDSHAKING /ws/notifications/ [127.0.0.1:57767]
INFO:django.channels.server:WebSocket HANDSHAKING /ws/notifications/ [127.0.0.1:57767]
XXX connect
WebSocket CONNECT /ws/notifications/ [127.0.0.1:57767]
INFO:django.channels.server:WebSocket CONNECT /ws/notifications/ [127.0.0.1:57767]
YYY UserView
HTTP GET /api/user 200 [0.02, 127.0.0.1:57761]
INFO:django.channels.server:HTTP GET /api/user 200 [0.02, 127.0.0.1:57761]
YYY UserView
HTTP GET /api/user 200 [0.00, 127.0.0.1:57761]
INFO:django.channels.server:HTTP GET /api/user 200 [0.00, 127.0.0.1:57761]
YYY UserLogout
HTTP POST /api/logout 200 [0.01, 127.0.0.1:57761]
INFO:django.channels.server:HTTP POST /api/logout 200 [0.01, 127.0.0.1:57761]
YYY UserLogin
HTTP POST /api/login 200 [0.00, 127.0.0.1:57761]
INFO:django.channels.server:HTTP POST /api/login 200 [0.00, 127.0.0.1:57761]

UPDATE: I think I got something working, like daphne is started up and there seems to a connection but aborted. pressing the login button should send the websocket notification but I dont see any message received in React side.


Solution

  • Reference 1

    Problem :-

    Look below.

    application = ProtocolTypeRouter({
        "websocket": URLRouter([
            path("ws/notifications/", consumers.NotificationConsumer.as_asgi()),
        ]),
    })
    

    It's notifications.

    And

    const ws = new WebSocket('ws://localhost:8000/ws/notification/');
    
    

    It's notification.

    Path should be same path.

    Use as below.

    Answer :-

    application = ProtocolTypeRouter({
        "websocket": URLRouter([
            path("ws/notifications/", consumers.NotificationConsumer.as_asgi()),
        ]),
    })
    
    

    And

    const ws = new WebSocket('ws://localhost:8000/ws/notifications/');
    
    

    Reference 2

    Info :-

    1) If you have configured django-channels your console output opon python manage.py runserver should be something like below.

    Watching for file changes with StatReloader
    Performing system checks...
    
    System check identified no issues (0 silenced).
    
    You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
    Run 'python manage.py migrate' to apply them.
    August 19, 2022 - 10:20:28
    Django version 4.1, using settings 'mysite.settings'
    
    Starting ASGI/Daphne version 3.0.2 development server at http://127.0.0.1:8000/
    
    Quit the server with CONTROL-C.
    

    Look for Starting ASGI/Daphne version x.x.x.

    2) Daphne is required in development server.

    Look here

    As I understand, you need to reconnect on error. App should be trying to connect always, if not connected already.

    Like this.

    Function channels_connect(){
    
    On_Connect{
    Messages 
    } 
    
    On_error{
    //reconnect here 
    
    channels_connect();
    
    } 
    
    } 
    

    Consumer should be able to refer specific users inside views.py. So, unique groups are required for every user. User must be added to their group upon connection accept.

    Inside browser javascript code.

    ws = new WebSocket(`ws://localhost:8000/ws/notifications/${user_group_id}/`);
    
    

    Consumer path.

    re_path(r'ws/notifications/(?P<user_group_id>[^/]+)/$', consumers.NotificationConsumer.as_asgi()),
    

    Inside consumer.

    async def connect(self):
        print("XXX connect")
        self.user_group_id = self.scope['url_route']['kwargs']['user_group_id']
        await self.channel_layer.group_add(self.user_group_id, self.channel_name)
        await self.accept()
    
    
    1. old development step code for reference (from chat)
    class NotificationConsumer(AsyncWebsocketConsumer):
        async def connect(self):
        print("XXX connect")
        await self.channel_layer.group_add("notifications", self.channel_name)
        await self.accept()
    

    in views.py

    from channels.layers import get_channel_layer
    from asgiref.sync import async_to_sync
    
    channel_layer = get_channel_layer()
    async_to_sync(channel_layer.group_send)(
    'notifications', # Here 'notifications' is the group name that you have used in your consumer
    {
    'type': 'notification_message',
    'text': 'test send',
    }
    )