pallets / pallets/quart

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

Open
#281 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
3.7k
Forks
206
PR merge metrics
No merged PRs in 30d

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read src/quart/asgi.py at ASGIWebsocketConnection around the websocket.disconnect handling near line 183, then trace how disconnects and cancellation reach application code. Define an application-facing behavior that distinguishes client or server disconnects from cancellation and exposes the disconnect code, with reason included only if supported; done means the selected behavior is specified and covered by tests.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.