Summary
SSEClient.shutdown() in splitio/push/sse.py calls self._conn.sock.shutdown(socket.SHUT_RDWR) without guarding against a socket that still exists but is already disconnected at the OS level. When the streaming (SSE) connection is dropped by the server or the network, the streaming feedback handler tears the client down and this call raises an unhandled OSError: [Errno 107] Transport endpoint is not connected (ENOTCONN) inside a background daemon thread.
Because it originates in a background thread, application code cannot catch it. It surfaces as an unhandled exception (e.g. reported to Sentry / crash reporters) even though it is harmless — the connection we are trying to shut down is already gone.
Environment
- splitio-client version: 10.6.0
- Python: CPython 3.12.11
- Mode: streaming enabled (default), i.e.
get_factory(sdk_key) with default config
- OS: Linux
Stacktrace
Traceback (most recent call last):
File "threading.py", line 1012, in run
self._target(*self._args, **self._kwargs)
File "splitio/sync/manager.py", line 122, in _streaming_feedback_handler
self._push.stop(True)
File "splitio/push/manager.py", line 128, in stop
self._sse_client.stop(blocking)
File "splitio/push/splitsse.py", line 174, in stop
self._client.shutdown()
File "splitio/push/sse.py", line 136, in shutdown
self._conn.sock.shutdown(socket.SHUT_RDWR)
File "ssl.py", line 1290, in shutdown
super().shutdown(how)
OSError: [Errno 107] Transport endpoint is not connected
Root cause
SSEClient.shutdown() (splitio/push/sse.py):
def shutdown(self):
"""Shutdown the current connection."""
if self._conn is None or self._conn.sock is None:
_LOGGER.warning("no sse connection has been started on this SSEClient instance. Ignoring")
return
if self._shutdown_requested:
_LOGGER.warning("shutdown already requested")
return
self._shutdown_requested = True
self._conn.sock.shutdown(socket.SHUT_RDWR) # <-- raises ENOTCONN if peer already closed
The method guards for sock is None and for a repeated shutdown request, but not for the common case where the peer has already closed the connection. Calling socket.shutdown(SHUT_RDWR) on an already-disconnected socket raises OSError(107, ENOTCONN). Since this runs in Manager._streaming_feedback_handler's background thread, it becomes an unhandled thread exception.
Steps to reproduce
This is a race between the server/network closing the SSE stream and the client's own shutdown path, so it is timing-dependent. It reliably occurs in production whenever the streaming connection is dropped remotely and the feedback handler subsequently calls stop(). It can be reproduced directly at the socket level:
import socket
s = socket.create_connection(("example.com", 80))
s.close() # peer/local endpoint no longer connected
s.shutdown(socket.SHUT_RDWR) # OSError: [Errno 107] Transport endpoint is not connected
Suggested fix
Wrap the shutdown() call so an already-disconnected socket is treated as a no-op (the goal — a closed connection — is already achieved):
self._shutdown_requested = True
try:
self._conn.sock.shutdown(socket.SHUT_RDWR)
except OSError as exc:
# Socket already disconnected (e.g. ENOTCONN 107) — nothing to shut down.
_LOGGER.debug("SSE socket already disconnected during shutdown: %s", exc)
Happy to open a PR with this change plus a regression test if it's helpful.
Summary
SSEClient.shutdown()insplitio/push/sse.pycallsself._conn.sock.shutdown(socket.SHUT_RDWR)without guarding against a socket that still exists but is already disconnected at the OS level. When the streaming (SSE) connection is dropped by the server or the network, the streaming feedback handler tears the client down and this call raises an unhandledOSError: [Errno 107] Transport endpoint is not connected(ENOTCONN) inside a background daemon thread.Because it originates in a background thread, application code cannot catch it. It surfaces as an unhandled exception (e.g. reported to Sentry / crash reporters) even though it is harmless — the connection we are trying to shut down is already gone.
Environment
get_factory(sdk_key)with default configStacktrace
Root cause
SSEClient.shutdown()(splitio/push/sse.py):The method guards for
sock is Noneand for a repeated shutdown request, but not for the common case where the peer has already closed the connection. Callingsocket.shutdown(SHUT_RDWR)on an already-disconnected socket raisesOSError(107, ENOTCONN). Since this runs inManager._streaming_feedback_handler's background thread, it becomes an unhandled thread exception.Steps to reproduce
This is a race between the server/network closing the SSE stream and the client's own shutdown path, so it is timing-dependent. It reliably occurs in production whenever the streaming connection is dropped remotely and the feedback handler subsequently calls
stop(). It can be reproduced directly at the socket level:Suggested fix
Wrap the
shutdown()call so an already-disconnected socket is treated as a no-op (the goal — a closed connection — is already achieved):Happy to open a PR with this change plus a regression test if it's helpful.