diff --git a/Lib/asyncio/proactor_events.py b/Lib/asyncio/proactor_events.py index 8350e0276046884..db90ceefb44911d 100644 --- a/Lib/asyncio/proactor_events.py +++ b/Lib/asyncio/proactor_events.py @@ -159,19 +159,27 @@ def _call_connection_lost(self, exc): try: self._protocol.connection_lost(exc) finally: - # XXX If there is a pending overlapped read on the other - # end then it may fail with ERROR_NETNAME_DELETED if we - # just close our end. First calling shutdown() seems to - # cure it, but maybe using DisconnectEx() would be better. - if hasattr(self._sock, 'shutdown') and self._sock.fileno() != -1: - self._sock.shutdown(socket.SHUT_RDWR) - self._sock.close() - self._sock = None - server = self._server - if server is not None: - server._detach(self) - self._server = None - self._called_connection_lost = True + try: + # XXX If there is a pending overlapped read on the other + # end then it may fail with ERROR_NETNAME_DELETED if we + # just close our end. First calling shutdown() seems to + # cure it, but maybe using DisconnectEx() would be better. + if (hasattr(self._sock, 'shutdown') + and self._sock.fileno() != -1): + try: + self._sock.shutdown(socket.SHUT_RDWR) + except ConnectionError: + # The peer has already reset the connection. + pass + finally: + # Always release the socket, even if shutdown() failed. + self._sock.close() + self._sock = None + server = self._server + if server is not None: + server._detach(self) + self._server = None + self._called_connection_lost = True def get_write_buffer_size(self): size = self._pending_write diff --git a/Lib/test/test_asyncio/test_proactor_events.py b/Lib/test/test_asyncio/test_proactor_events.py index 2f229887ad62cc8..4c49ee455c22e78 100644 --- a/Lib/test/test_asyncio/test_proactor_events.py +++ b/Lib/test/test_asyncio/test_proactor_events.py @@ -336,6 +336,36 @@ def test_call_connection_lost(self): self.assertTrue(self.protocol.connection_lost.called) self.assertTrue(self.sock.close.called) + def test_call_connection_lost_shutdown_reset(self): + # gh-160429: a connection reset by the peer must not prevent + # the socket from being closed and the transport from being + # detached from its server. + server = mock.Mock() + tr = _ProactorSocketTransport(self.loop, self.sock, self.protocol, + server=server) + self.sock.shutdown.side_effect = ConnectionResetError + tr._call_connection_lost(None) + self.protocol.connection_lost.assert_called_once_with(None) + self.sock.shutdown.assert_called_once_with(socket.SHUT_RDWR) + self.sock.close.assert_called_once_with() + server._detach.assert_called_once_with(tr) + self.assertIsNone(tr._sock) + self.assertIsNone(tr._server) + self.assertTrue(tr._called_connection_lost) + + def test_call_connection_lost_shutdown_error(self): + # Unexpected shutdown() errors still propagate, but the socket is + # closed anyway. + server = mock.Mock() + tr = _ProactorSocketTransport(self.loop, self.sock, self.protocol, + server=server) + self.sock.shutdown.side_effect = OSError + with self.assertRaises(OSError): + tr._call_connection_lost(None) + self.sock.close.assert_called_once_with() + server._detach.assert_called_once_with(tr) + self.assertTrue(tr._called_connection_lost) + def test_write_eof(self): tr = self.socket_transport() self.assertTrue(tr.can_write_eof()) diff --git a/Misc/NEWS.d/next/Library/2026-10-24-15-30-00.gh-issue-160429.Rk3vQd.rst b/Misc/NEWS.d/next/Library/2026-10-24-15-30-00.gh-issue-160429.Rk3vQd.rst new file mode 100644 index 000000000000000..3bfa3e9b49e21d4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-24-15-30-00.gh-issue-160429.Rk3vQd.rst @@ -0,0 +1,3 @@ +Fix :mod:`asyncio` proactor transports skipping the closing of the socket +and the detaching from the server when the socket shutdown fails because +the peer has already reset the connection.