simple-websocket
simple-websocket copied to clipboard
After client closes the connection, in Gunicorn logs: OSError: [Errno 9] Bad file descriptor
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).
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.