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.
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