Search code examples
djangowebsocketdjango-channels

Using Websocket in Django View Not Working


Problem Summary

I am sending data to a front-end (React component) using Django and web-sockets. When I run the app and send the data from my console everything works. When I use a button on the front-end to trigger a Django view that runs the same function, it does not work and generates a confusing error message.

I want to be able to click a front-end button which begins sending the data to the websocket.

I am new to Django, websockets and React and so respectfully ask you to be patient.

Overview

  1. Django back-end and React front-end connected using Django Channels (web-sockets).
  2. User clicks button on front-end, which does fetch() on Django REST API end-point.
  3. [NOT WORKING] The above endpoint's view begins sending data through the web-socket.
  4. Front-end is updated with this value.

Short Error Description

The error Traceback is long, so it is included at the end of this post. It begins with:

Internal Server Error: /api/run-create

And ends with:

ConnectionResetError: [WinError 10054] An existing connection was forcibly closed by the remote host

What I've Tried

Sending Data Outside The Django View

  • The function below sends data to the web-socket.
  • Works perfectly when I run it in my console - front-end updates as expected.
  • Note: the same function causes the attached error when run from inside the Django view.
    import json
    import time
    
    import numpy as np
    import websocket
    
    
    def gen_fake_path(num_cities):
        path = list(np.random.choice(num_cities, num_cities, replace=False))
        path = [int(num) for num in path]
        return json.dumps({"path": path})
    
    
    def fake_run(num_cities, limit=1000):
        ws = websocket.WebSocket()
        ws.connect("ws://localhost:8000/ws/canvas_data")
        while limit:
            path_json = gen_fake_path(num_cities)
            print(f"Sending {path_json} (limit: {limit})")
            ws.send(path_json)
            time.sleep(3)
            limit -= 1
        print("Sending complete!")
        ws.close()
        return

Additional Detail

Relevant Files and Configuration

consumer.py

    class AsyncCanvasConsumer(AsyncWebsocketConsumer):
        async def connect(self):
            self.group_name = "dashboard"
            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(self.group_name, self.channel_name)
    
        async def receive(self, text_data=None, bytes_data=None):
            print(f"Received: {text_data}")
            data = json.loads(text_data)
            to_send = {"type": "prep", "path": data["path"]}
            await self.channel_layer.group_send(self.group_name, to_send)
    
        async def prep(self, event):
            send_json = json.dumps({"path": event["path"]})
            await self.send(text_data=send_json)

Relevant views.py

    @api_view(["POST", "GET"])
    def run_create(request):
        serializer = RunSerializer(data=request.data)
        if not serializer.is_valid():
            return Response({"Bad Request": "Invalid data..."}, status=status.HTTP_400_BAD_REQUEST)
        # TODO: Do run here.
        serializer.save()
        fake_run(num_cities, limit=1000)
        return Response(serializer.data, status=status.HTTP_200_OK)

Relevant settings.py

    WSGI_APPLICATION = 'evolving_salesman.wsgi.application'
    ASGI_APPLICATION = 'evolving_salesman.asgi.application'
    
    CHANNEL_LAYERS = {
        "default": {
            "BACKEND": "channels.layers.InMemoryChannelLayer"
        }
    }

Relevant routing.py

    websocket_url_pattern = [
        path("ws/canvas_data", AsyncCanvasConsumer.as_asgi()),
    ]

Full Error

https://pastebin.com/rnGhrgUw

EDIT: SOLUTION

The suggestion by Kunal Solanke solved the issue. Instead of using fake_run() I used the following:

    layer = get_channel_layer()
    for i in range(10):
        path = list(np.random.choice(4, 4, replace=False))
        path = [int(num) for num in path]
        async_to_sync(layer.group_send)("dashboard", {"type": "prep", "path": path})
        time.sleep(3)

Solution

  • Rather than creating a new connection from same server to itself , I'd suggest you to use the get_channel_layer utitlity .Because you are in the end increasing the server load by opening so many connections . Once you get the channel layer , you can simply do group send as we normally do to send evnets . You can read more about here

    from channels.layers import get_channel_layer
    from asgiref.sync import async_to_sync
    def media_image(request,chat_id) :
        if request.method == "POST" :
            data = {}
            if request.FILES["media_image"] is not None :
                item = Image.objects.create(owner = request.user,file=request.FILES["media_image"])
                message=Message.objects.create(item =item,user=request.user )
                chat = Chat.objects.get(id=chat_id)
                chat.messages.add(message)
                layer = get_channel_layer()
                item = {
                   "media_type": "image",
                    "url" : item.file.url,
                    "user" : request.user.username,
                    'caption':item.title
                }
                async_to_sync(layer.group_send)(
                    'chat_%s'%str(chat_id),
    #this is the channel group name,which is defined inside your consumer"
                    {
                        "type":"send_media",
                        "item" : item
                        
                    }
                )
    
            return HttpResponse("media sent")
    

    In the error log, I can see that the handshake succeded for the first iteration and failed for 2nd . You can check that by printing something in the for loop . If that's the case the handshake most probably failed due to mulitple connections . I don't know how many connections the Inmemrorycache supports from same origin,but that can be reason that the 2nd connection is getting diconnected . You can get some idea in channel docs.Try using redis if you don't want to change your code,its pretty easy if you are using linux .