miguelgrinberg/microdot

Detect websocket connection close

Closed this issue · 8 comments

Hello,

First. thank you for your work on this awesome framework !

Is there a way to detect that a websocket connection has been closed by the client ?

Here is my context: my code is broadcasting information to all websocket clients. But when a client has closed its connection, ws.send() raises an exception (which is the expected behaviour).

Here is an example of my case:

from microdot_asyncio import Microdot
from microdot_asyncio_websocket import with_websocket
import json

app = Microdot()

websocket_connections = set()

@app.route('/websocket')
@with_websocket
async def websocket(request, ws):
    websocket_connections.add(ws)
    await ws.send("Hello, plase type a message and press enter...")

    while True:
        message = await ws.receive()
        for connection in websocket_connections:
            try:
                # FIXME: an exception is raised "OSError: [Errno 9] EBADF" when a client has disconnected
                await connection.send(json.dumps(str(message)))
            except Exception as e:
                print('Failed sending data to websocket: {} ({})'.format(connection, e))

I could remove the closed connection in the block except, but I call ws.send in other parts of the code too (so I don't want to manage closing connection in every block except).

I could wrap ws.send() in a custom function, eg. def send(ws, data). Yet is there a simpler way to remove a websocket connection that is closed by the client, as soon as it is closed by the client ?

This has not much to do with WebSocket, but with how TCP/IP works. Unfortunately if the client does not close the connection and just goes away there is nothing that will reactively happen in the server, so you will find out that the client is gone the next time you attempt to send something to them.

My Socket.IO server suffers from the same issue, by the way.

Thank you for your reply. It makes sense and I can absolutely create a wrapper function to ws.send() that will factorize the removal of the "dead" websocket connections from my list websocket_connections when except OSError as e. Eg:

async def send(ws, data):
    try:
        return await connection.send(json.dumps({'values': values}))
    except Exception as e:
        if str(e) == "[Errno 9] EBADF":
            print('Removing closed websocket connection: {} ({})'.format(connection, e))
            websocket_connections.remove(ws)
        else:
            raise e

The only downside I can see is that list websocket_connections can become too big for ESP32 memory, eg. if many (thousands) websocket clients are connecting and disconnecting before except OSError as e is triggered (eg. when no message is sent for a "long time").

For information, the framework microWebSrv does provide a callback _closedCallback() that is called when a connection is closed by the client, eg:

def _closedCallback(webSocket) :
    websocket_connections.remove(webSocket)

I haven't looked at how it is done yet, I wonder how they implement it.

the framework microWebSrv does provide a callback _closedCallback() that is called when a connection is closed by the client

The client must gracefully close the connection for this to work. Note that I mentioned this above, if the client closes, then the server receives a notification. Microdot uses a sync approach to notifications, so the best it can do is to raise OSError when the close message is received, usually while your code is calling the ws.receive() function.

But in any case, what I thought you were asking about is when the client just disappears without closing, which is a case that cannot be discovered without attempting to read or write on the socket.

I remember microWebSrv ClosedCallback() being called when typing ctrl-c from wscat.

Would it be possible to keep this issue open until we have a closer look at the implementation and make sure it is not doable reasonably ?

As I explained above, this WebSocket implementation does not use callbacks, that is not the model that is implemented. You can implement a callback system on top of this library if you so wish.

Also, wscat issues a graceful close when it is interrupted with Ctrl-C. The callback solution that you seem to like would not work for clients that leave without gracefully closing the connection.

As wscat issues a graceful close when it is interrupted with Ctrl-C, can it be detected by Microdot ? Then

@app.ws_close_connection
def ws_close_connection(ws):
    ws_connections.remove(ws)

Does it make sense ?

No, it doesn't. How do you plan on stopping the websocket handler after this? As I said, this library is not built with callbacks in mind.

The equivalent non-callback solution is this:

@app.route('/websocket')
@with_websocket
async def websocket(request, ws):
    ws_connections.add(ws)
    try:
        # do what you need to do here
    except OSError:
        ws_connections.remove(ws)

Now I see what you mean, thank you for your help !