skip to Main Content

Using Django channels to update the user on the current status of a potentially long running task, I’m facing a WebSocket DISCONNECT that I would like to trace down.

The setup looks pretty straight forward. settings.py defines the channel layer:

ASGI_APPLICATION = "config.routing.application"
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}

In routing.py we basically follow the default suggestion from the Channels doc (the pattern is simply matching a UUID, which we use as public facing identifier):

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.conf.urls import url
from lektomat.consumers import analysis

application = ProtocolTypeRouter({
    # (http->django views is added by default)
    "websocket": AuthMiddlewareStack(
        URLRouter([
            # Use a regex to match the UUID as the Django version with its '<uuid:analysis_id>' does not work for Channels.
            url(r"^ws/analysis/(?P<analysis_id>[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12})/$",
                analysis.AnalysisConsumer),
        ])),
})

The consumer does not much more than sending some initial data via a newly established websockets connection and logging info all over the place:

import logging
import json
from channels.db import database_sync_to_async
from uuid import UUID
from typing import Tuple
from django.utils import timezone
from channels.generic.websocket import AsyncWebsocketConsumer
from myapp.models import AnalysisResult

logger = logging.getLogger(__name__)


class AnalysisConsumer(AsyncWebsocketConsumer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.analysis_id = None
        self.analysis_group_name = 'analysis_'

    async def connect(self):
        self.analysis_id = self.scope['url_route']['kwargs']['analysis_id']
        self.analysis_group_name = "analysis_{}".format(self.analysis_id)
        await self.channel_layer.group_add(self.analysis_group_name, self.channel_name)
        logger.info("%s – AnalysisConsumer: connect()ed and joined group %s.", str(timezone.now()), self.analysis_group_name)
        dummy_percent = 11
        dummy_text = 'Dummy'
        logger.debug("%s – Sending initial channel update to group %s with a %d percent and status_text=%s", str(timezone.now()), self.analysis_group_name, dummy_percent, dummy_text)
        await self.send(text_data=json.dumps({
            'progress_percent': progress_percent,
            'status_text': status_text
        }))
        await self.accept()

    async def disconnect(self, code):
        logger.info("%s – AnalysisConsumer: disconnecting with code=%s (internal group name=%s).", str(timezone.now()), code, self.analysis_group_name)
        await self.channel_layer.group_discard(self.analysis_group_name, self.channel_name)
        logger.info("%s – AnalysisConsumer: disconnect(%s)ed and left room %s", str(timezone.now()), code, self.analysis_group_name)

    async def receive(self, text_data=None, bytes_data=None):
        logger.info("%s – unexpectedly received data from the websocket, text_data=%s, bytes_data=%s", str(timezone.now()), text_data, str(bytes_data))

And finally, the client’s javascript is connecting to the websocket endpoint:

var ws_scheme = window.location.protocol == "https:" ? "wss" : "ws";
var ws_uri = ws_scheme + '://' + window.location.host + '/ws/analysis/' + '{{ result_id }}' + '/';
var socket = new WebSocket(ws_uri);
socket.onopen = function open() {
    let now = new Date();
    console.info(now.toLocaleString() + ':' + now.getMilliseconds() + ' – WebSocket connection created.');
};

socket.onmessage = function(e) {
    console.log("WebSocket message received.")
    const data = JSON.parse(e.data);
    console.log("WebSocket message: " + data.status_text + " at " + data.progress_percent + " percent.");
};

socket.onclose = function(e) {
    let now = new Date();
    console.error(now.toLocaleString() + ':' + now.getMilliseconds() + ' – Analysis socket closed with event code = ' + e.code + ' and reason=' + e.reason);
};

socket.onerror = function(error) {
    let now = new Date();
    let msg = now.toLocaleString() + ':' + now.getMilliseconds() + ' – WebSocket error: ' + error;
    console.error(msg);
}

The Redis backend is up and running.

But: The websocket connection is closed right after its start. More precisely, the browser’s JS console logs (in German, faulty translation is mine):

(!) Firefox cannot connect to server at ws://localhost:8000/ws/analysis/d222ebe1-5a2a-4797-9466-24db1de5d24b/
(!) 10.8.2020, 22:30:21:317 – WebSocket error: [object Event]
    onerror http://localhost:8000/analysis/d222ebe1-5a2a-4797-9466-24db1de5d24b:149
    (Async: EventHandlerNonNull)
    <anonym> http://localhost:8000/analysis/d222ebe1-5a2a-4797-9466-24db1de5d24b:146
(!) 10.8.2020, 22:30:21:319 – Analysis socket closed with event code = 1006 and reason=
    onclose http://localhost:8000/analysis/d222ebe1-5a2a-4797-9466-24db1de5d24b:143
    (Async: EventHandlerNonNull)
    <anonym> http://localhost:8000/analysis/d222ebe1-5a2a-4797-9466-24db1de5d24b:141

The server console says:

request: <AsgiRequest: GET '/analysis/d222ebe1-5a2a-4797-9466-24db1de5d24b'>
HTTP GET /analysis/d222ebe1-5a2a-4797-9466-24db1de5d24b 200 [1.37, 127.0.0.1:51562]
WebSocket HANDSHAKING /ws/analysis/d222ebe1-5a2a-4797-9466-24db1de5d24b/ [127.0.0.1:51574]
2020-08-10 20:30:20.549519+00:00 – AnalysisConsumer: connect()ed and joined group analysis_d222ebe1-5a2a-4797-9466-24db1de5d24b.
2020-08-10 20:30:20.610167+00:00 – Sending initial channel update to group analysis_d222ebe1-5a2a-4797-9466-24db1de5d24b with a 11 percent and status_text=Dummy
WebSocket DISCONNECT /ws/analysis/d222ebe1-5a2a-4797-9466-24db1de5d24b/ [127.0.0.1:51574]

Thus, the server receives the connecting request, establishes the websocket connection and immediately disconnects. The client responds about half a second later with a mere error with code 1006 (which doesn’t tell much). The consumer’s disconnect() is never called.

Who might be initiating the WebSocket DISCONNECT? It doesn’t seem to be any of the application code. Pointers on what’s missing here are appreciated.

2

Answers


  1. This looks like the same problem I described here.

    Last message of your server before disconnect is that it is trying to send something (large?). I discovered that sending large WebSocket messages may lead to disconnect with error 1006.

    You may try to configure your WebSocket server to send data in small chunks. It helped in my case.

    Login or Signup to reply.
  2. That consumer is trying to send a message before accepting the connection.

    I just tested and Daphne throws this exception:

     File "...site-packages/daphne/ws_protocol.py", line 193, in handle_reply
     "Socket has not been accepted, so cannot send over it"
    

    And the clients receive a CloseEvent with code 1006

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search