quart icon indicating copy to clipboard operation
quart copied to clipboard

Make `code` and maybe `reason` of received websocket disconnect event accessible to application

Open jannschu opened this issue 2 years ago • 0 comments

Currently the code number of the websocket.disconnect event is discarded in ASGIWebsocketConnection making it inaccessible to the application.

It is currently not distinguishable whether

  • the client closed the connection and for what reason,
  • the server closed the connection, for example for shutdown, or
  • the task was cancelled, i.e. CancelledError is risen, for some other reason (distinction is probably not relevant for most applications).

It would be helpful to distinguish these cases in application code and also read the code value that was given (which would handle case 1 and case 2).

I saw that ASGI currently does not yet specify the reason value for the received disconnect (see related https://github.com/django/asgiref/issues/234). I personally do not need the reason it but I noticed it to be missing in my tests.

Hacky workaround and first thoughts

Currently I hack this feature into my application by using a custom asgi_websocket_class value for my app:

class ASGIWebsocketConnectionWithDisconnectEvent(ASGIWebsocketConnection):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.scope["disconnect_event"] = None

    async def handle_messages(self, receive) -> None:
        while True:
            event = await receive()
            if event["type"] == "websocket.receive":
                message = event.get("bytes") or event["text"]
                await websocket_received.send_async(message)
                await self.queue.put(message)
            elif event["type"] == "websocket.disconnect":
                self.scope["disconnect_event"] = event
                return

app = Quart(__name__)
app.asgi_websocket_class = ASGIWebsocketConnectionWithDisconnectEvent

Something like that, i.e. in general checking for the websocket to be closed on a CancelledError, would work for me. Modifying the scope dict is only a workaround of course. My implementation does not cover the third case above. To avoid the except+if pattern

try:
    ...
except asyncio.CancelledError:
    if websocket_closed:
        ...

a dedicated exception type risen on send and receive seems nice on first sight. On the other hand this will then only be risen on awaits on the websocket's send or receive whereas the cancellation strategy will stop more awaits. What one prefers is probably application dependent.

jannschu avatar Oct 07 '23 18:10 jannschu