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
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.
That consumer is trying to send a message before accepting the connection.
I just tested and Daphne throws this exception:
And the clients receive a CloseEvent with code 1006