fix(websocket): preserve invalid listener detection

This commit is contained in:
Xubin Ren
2026-09-01 11:14:34 +08:00
parent bd9b74e2c7
commit 22cbfc2fb5
2 changed files with 33 additions and 2 deletions
+1 -1
View File
@@ -554,7 +554,7 @@ class WebSocketChannel(BaseChannel):
try: try:
return bool(sock.getsockopt(socket.SOL_SOCKET, socket.SO_ACCEPTCONN)) return bool(sock.getsockopt(socket.SOL_SOCKET, socket.SO_ACCEPTCONN))
except OSError as exc: except OSError as exc:
if exc.errno in (errno.ENOPROTOOPT, errno.EOPNOTSUPP, errno.EINVAL): if exc.errno in (errno.ENOPROTOOPT, errno.EOPNOTSUPP):
return True return True
raise raise
@@ -28,6 +28,21 @@ class _StubSocket:
return self._value return self._value
class _StubServer:
"""Minimal server stand-in for the production listener-health boundary."""
def __init__(self, sock: _StubSocket, *, serving: bool = True):
self._sock = sock
self._serving = serving
@property
def sockets(self) -> tuple[_StubSocket, ...]:
return (self._sock,)
def is_serving(self) -> bool:
return self._serving
@pytest.fixture @pytest.fixture
def listening_socket() -> socket.socket: def listening_socket() -> socket.socket:
sock = socket.socket() sock = socket.socket()
@@ -57,7 +72,7 @@ def test_closed_socket_is_not_accepting() -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"unsupported_errno", "unsupported_errno",
[errno.ENOPROTOOPT, errno.EOPNOTSUPP, errno.EINVAL], [errno.ENOPROTOOPT, errno.EOPNOTSUPP],
) )
def test_unsupported_sockopt_falls_back_to_fd_liveness(unsupported_errno: int) -> None: def test_unsupported_sockopt_falls_back_to_fd_liveness(unsupported_errno: int) -> None:
"""macOS/BSD reject ``SO_ACCEPTCONN`` even on healthy listeners. """macOS/BSD reject ``SO_ACCEPTCONN`` even on healthy listeners.
@@ -70,6 +85,14 @@ def test_unsupported_sockopt_falls_back_to_fd_liveness(unsupported_errno: int) -
assert WebSocketChannel._socket_is_accepting(sock) is True assert WebSocketChannel._socket_is_accepting(sock) is True
def test_listener_health_uses_unsupported_sockopt_fallback() -> None:
"""The fallback must be wired into the health check that controls readiness."""
sock = _StubSocket(fileno=3, error=OSError(errno.ENOPROTOOPT, "Protocol not available"))
server: Any = _StubServer(sock)
assert WebSocketChannel._listener_is_serving(server) is True
def test_unexpected_oserror_propagates() -> None: def test_unexpected_oserror_propagates() -> None:
sock = _StubSocket(fileno=3, error=OSError(errno.EBADF, "Bad file descriptor")) sock = _StubSocket(fileno=3, error=OSError(errno.EBADF, "Bad file descriptor"))
@@ -79,6 +102,14 @@ def test_unexpected_oserror_propagates() -> None:
assert excinfo.value.errno == errno.EBADF assert excinfo.value.errno == errno.EBADF
def test_listener_health_rejects_invalid_socket_state() -> None:
"""``EINVAL`` can mean that a live socket isn't actually listening."""
sock = _StubSocket(fileno=3, error=OSError(errno.EINVAL, "Invalid argument"))
server: Any = _StubServer(sock)
assert WebSocketChannel._listener_is_serving(server) is False
def test_unsupported_sockopt_still_rejects_dead_fd() -> None: def test_unsupported_sockopt_still_rejects_dead_fd() -> None:
"""The portability fallback must not mask an already-closed listener.""" """The portability fallback must not mask an already-closed listener."""
sock = _StubSocket(fileno=-1, error=OSError(errno.ENOPROTOOPT, "Protocol not available")) sock = _StubSocket(fileno=-1, error=OSError(errno.ENOPROTOOPT, "Protocol not available"))