https://github.com/python/cpython/commit/f88565bcd00ac258379c4b0d381d5580a40ac8a7
commit: f88565bcd00ac258379c4b0d381d5580a40ac8a7
branch: main
author: Andrew Svetlov <[email protected]>
committer: asvetlov <[email protected]>
date: 2026-11-04T12:05:27+01:00
summary:

gh-93821: Keep asyncio proactor servers accepting after a connection reset 
(#161238)

Co-authored-by: Claude Opus 5.5 <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-11-03-12-00-00.gh-issue-93821.aCcRsT.rst
M Lib/asyncio/proactor_events.py
M Lib/asyncio/windows_events.py
M Lib/test/test_asyncio/test_proactor_events.py
M Lib/test/test_asyncio/test_windows_events.py

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.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to