From 41989816a1387f9dcd8618d832bd64dd035deb38 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 3 Nov 2026 09:06:02 +0100 Subject: [PATCH] gh-93821: Keep asyncio proactor servers accepting after a connection reset Co-Authored-By: Claude Opus 5.5 --- Lib/asyncio/proactor_events.py | 7 ++++ Lib/asyncio/windows_events.py | 12 ++++++- Lib/test/test_asyncio/test_proactor_events.py | 20 +++++++++++ Lib/test/test_asyncio/test_windows_events.py | 34 +++++++++++++++++++ ...6-11-03-12-00-00.gh-issue-93821.aCcRsT.rst | 4 +++ 5 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-11-03-12-00-00.gh-issue-93821.aCcRsT.rst diff --git a/Lib/asyncio/proactor_events.py b/Lib/asyncio/proactor_events.py index 21abff88b5bf40..6d2bcba1a1b375 100644 --- a/Lib/asyncio/proactor_events.py +++ b/Lib/asyncio/proactor_events.py @@ -892,6 +892,13 @@ def loop(f=None): if self.is_closed(): return f = self._proactor.accept(sock) + except (ConnectionResetError, ConnectionAbortedError): + # The peer closed the connection before it was accepted. + # The listening socket is still usable: keep accepting. + if self._debug: + logger.debug("Accept failed on socket %r", + sock, exc_info=True) + self.call_soon(loop) except OSError as exc: if sock.fileno() != -1: self.call_exception_handler({ diff --git a/Lib/asyncio/windows_events.py b/Lib/asyncio/windows_events.py index a67c21b3f46c4c..6c200fabb2e8e6 100644 --- a/Lib/asyncio/windows_events.py +++ b/Lib/asyncio/windows_events.py @@ -527,7 +527,14 @@ def accept(self, listener): ov.AcceptEx(listener.fileno(), conn.fileno()) def finish_accept(trans, key, ov): - ov.getresult() + try: + ov.getresult() + except OSError as exc: + conn.close() + # The peer reset the connection before it was accepted. + if exc.winerror == _overlapped.ERROR_NETNAME_DELETED: + raise ConnectionResetError(*exc.args) + raise # Use SO_UPDATE_ACCEPT_CONTEXT so getsockname() etc work. buf = struct.pack('@P', listener.fileno()) conn.setsockopt(socket.SOL_SOCKET, @@ -542,6 +549,9 @@ async def accept_coro(future, conn): except exceptions.CancelledError: conn.close() raise + except ConnectionResetError: + # Reported to the caller through the future. + pass future = self._register(ov, listener, finish_accept) coro = accept_coro(future, conn) diff --git a/Lib/test/test_asyncio/test_proactor_events.py b/Lib/test/test_asyncio/test_proactor_events.py index d25c5fa47d1dc1..7a4dd7eba329a0 100644 --- a/Lib/test/test_asyncio/test_proactor_events.py +++ b/Lib/test/test_asyncio/test_proactor_events.py @@ -868,6 +868,26 @@ def test_create_server(self, m_log): self.assertTrue(self.sock.close.called) self.assertTrue(m_log.error.called) + def test_create_server_connection_reset(self): + # gh-93821: a peer resetting its connection before it is accepted + # must not close the listening socket. + pf = mock.Mock() + call_soon = self.loop.call_soon = mock.Mock() + self.loop.call_exception_handler = mock.Mock() + + self.loop._start_serving(pf, self.sock) + loop = call_soon.call_args[0][0] + + for exc in ConnectionResetError, ConnectionAbortedError: + with self.subTest(exc=exc): + call_soon.reset_mock() + fut = mock.Mock() + fut.result.side_effect = exc() + loop(fut) + self.assertFalse(self.sock.close.called) + self.assertFalse(self.loop.call_exception_handler.called) + call_soon.assert_called_once_with(loop) + def test_create_server_cancel(self): pf = mock.Mock() call_soon = self.loop.call_soon = mock.Mock() diff --git a/Lib/test/test_asyncio/test_windows_events.py b/Lib/test/test_asyncio/test_windows_events.py index bb4ba74f19a17f..073e740e2f04d7 100644 --- a/Lib/test/test_asyncio/test_windows_events.py +++ b/Lib/test/test_asyncio/test_windows_events.py @@ -1,6 +1,7 @@ import os import signal import socket +import struct import sys import time import threading @@ -19,6 +20,7 @@ from asyncio import windows_utils from test import support from test.support import os_helper +from test.support import socket_helper from test.test_asyncio import utils as test_utils @@ -115,6 +117,38 @@ def test_close(self): self.assertEqual(f.result(), b'') b.close() + def test_accept_connection_reset(self): + # gh-93821: a connection reset while it waits in the listen queue + # makes AcceptEx fail with ERROR_NETNAME_DELETED. The server must + # keep accepting. + async def handle(reader, writer): + writer.close() + + async def main(): + server = await asyncio.start_server(handle, socket_helper.HOSTv4, 0) + port = server.sockets[0].getsockname()[1] + # Both connects complete while the loop is blocked here: the + # first is taken by the pending AcceptEx, the second waits in + # the listen queue and is reset there. + first = socket.create_connection((socket_helper.HOSTv4, port)) + second = socket.create_connection((socket_helper.HOSTv4, port)) + second.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, + struct.pack('HH', 1, 0)) + second.close() + try: + await asyncio.sleep(0.5) + reader, writer = await asyncio.wait_for( + asyncio.open_connection(socket_helper.HOSTv4, port), + support.SHORT_TIMEOUT) + writer.close() + await writer.wait_closed() + finally: + first.close() + server.close() + await server.wait_closed() + + self.loop.run_until_complete(main()) + def test_double_bind(self): ADDRESS = r'\\.\pipe\test_double_bind-%s' % os.getpid() server1 = windows_events.PipeServer(ADDRESS) diff --git a/Misc/NEWS.d/next/Library/2026-11-03-12-00-00.gh-issue-93821.aCcRsT.rst b/Misc/NEWS.d/next/Library/2026-11-03-12-00-00.gh-issue-93821.aCcRsT.rst new file mode 100644 index 00000000000000..a8a0da86e6abdb --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-11-03-12-00-00.gh-issue-93821.aCcRsT.rst @@ -0,0 +1,4 @@ +Fix :class:`asyncio.ProactorEventLoop` servers that stopped accepting new +connections on Windows when a client reset its connection before it was +accepted (``WinError 64``, ``ERROR_NETNAME_DELETED``). The listening socket is +now kept open and the server continues to accept connections.