Skip to content

SSEClient.shutdown() raises OSError: [Errno 107] Transport endpoint is not connected when the SSE socket is already disconnected #645

Description

@dexterionut

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions