miguelgrinberg / simple-websocket

Simple WebSocket server and client for Python.
MIT License
78 stars 17 forks source link

After client closes the connection, in Gunicorn logs: OSError: [Errno 9] Bad file descriptor #37

Open vsemionov opened 7 months ago

vsemionov commented 7 months ago

Using simple-websocket from git commit 32ec52 and Gunicorn 21.2.0. When a client closes the websocket, the following is logged:

[ERROR] Socket error processing request.
Traceback (most recent call last):
  File "/****/lib/python3.11/site-packages/gunicorn/workers/base_async.py", line 65, in handle
    util.reraise(*sys.exc_info())
  File "/****/lib/python3.11/site-packages/gunicorn/util.py", line 641, in reraise
    raise value
  File "/****/lib/python3.11/site-packages/gunicorn/workers/base_async.py", line 55, in handle
    self.handle_request(listener_name, req, client, addr)
  File "/****/lib/python3.11/site-packages/gunicorn/workers/ggevent.py", line 128, in handle_request
    super().handle_request(listener_name, req, sock, addr)
  File "/****/lib/python3.11/site-packages/gunicorn/workers/base_async.py", line 130, in handle_request
    util.reraise(*sys.exc_info())
  File "/****/lib/python3.11/site-packages/gunicorn/util.py", line 641, in reraise
    raise value
  File "/****/lib/python3.11/site-packages/gunicorn/workers/base_async.py", line 117, in handle_request
    resp.close()
  File "/****/lib/python3.11/site-packages/gunicorn/http/wsgi.py", line 391, in close
    self.send_headers()
  File "/****/lib/python3.11/site-packages/gunicorn/http/wsgi.py", line 322, in send_headers
    util.write(self.sock, util.to_bytestring(header_str, "latin-1"))
  File "/****/lib/python3.11/site-packages/gunicorn/util.py", line 299, in write
    sock.sendall(data)
  File "/****/lib/python3.11/site-packages/gevent/_socketcommon.py", line 702, in sendall
    return _sendall(self, data_memory, flags)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/****/lib/python3.11/site-packages/gevent/_socketcommon.py", line 378, in _sendall
    chunk_size = max(socket.getsockopt(SOL_SOCKET, SO_SNDBUF), 1024 * 1024)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/****/lib/python3.11/site-packages/gevent/_socketcommon.py", line 553, in getsockopt
    return self._sock.getsockopt(*args)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/****/lib/python3.11/site-packages/gevent/_socket3.py", line 55, in _dummy
    raise OSError(EBADF, 'Bad file descriptor')
OSError: [Errno 9] Bad file descriptor

I believe it happens because the IO thread closes the underlying OS socket before terminating: Base._thread() in ws.py

...
self.sock.close()

I believe calling this and closing the OS socket by this package is wrong and should not be done, simply because the socket is owned and managed by the web server.

The reason it only happens when the connection is closed by the client, is that when the server initiates the close, this only sets the connected attribute, but the IO thread is sleeping and only notices that after a while.

Also, I had the problem that after a server-initiated close, the browser (Chrome 121.0.6167.139) complained about receiving an invalid frame. It was because the web server sends the http headers after the websocket view is finished. To prevent this, after closing the WebSocket from the Server class, I do:

ws.sock.shutdown(socket.SHUT_WR)

In the above, ws is a Server instance, and socket is the standard python module. Doing this also removes the need for this in flask-sock. While the code in the link does prevent exceptions in logs, it also removes the websocket requests from the web server's access logs. And the exceptions are still raised and get reported in Sentry. So I recommend you do something similar to the above quoted line. Unlike calling close() on the OS socket, calling shutdown() does not cause later exceptions in werkzeug or gunicorn (I have not tested with eventlet).

miguelgrinberg commented 7 months ago

I think this is probably related to https://github.com/miguelgrinberg/flask-sock/issues/64. It appears the Gevent worker in Gunicorn does not implement the same mechanism as the threaded worker to exit out of a WebSocket connection cleanly. The Gunicorn support in this package is designed to work with the threaded worker, and it so happens that the eventlet worker also implements similar logic. I'm not sure why the gevent worker does not follow the same pattern, but my suggestion is that if you want to use gevent then you drop Gunicorn and use gevent's own WSGI server.