https://github.com/python/cpython/commit/0dde052012b61ba33133abb907ffd77701f2657f
commit: 0dde052012b61ba33133abb907ffd77701f2657f
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: asvetlov <[email protected]>
date: 2026-10-29T10:40:48Z
summary:

[3.15] gh-109564: asyncio: drop connections accepted by a closed server 
(GH-160851) (#160878)

gh-109564: asyncio: drop connections accepted by a closed server (GH-160851)

If Server.close() ran after a connection was accepted but before its
transport was created, Server._attach() hit an assertion, the error was
reported to the exception handler, and the half-built transport later
raised TypeError from __del__.  Close such connections quietly instead,
in both the selector and proactor accept paths.
(cherry picked from commit 5364bdce6b6f2b0e055057aef1002ef07b91a497)

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

files:
A Misc/NEWS.d/next/Library/2026-10-28-20-36-44.gh-issue-109564.WqoyNE.rst
M Lib/asyncio/proactor_events.py
M Lib/asyncio/selector_events.py
M Lib/test/test_asyncio/test_server.py

diff --git a/Lib/asyncio/proactor_events.py b/Lib/asyncio/proactor_events.py
index da1af53b1224f4..1b2266901d9034 100644
--- a/Lib/asyncio/proactor_events.py
+++ b/Lib/asyncio/proactor_events.py
@@ -874,6 +874,11 @@ def loop(f=None):
                         logger.debug("%r got a new connection from %r: %r",
                                      server, addr, conn)
                     protocol = protocol_factory()
+                    if server is not None and not server.is_serving():
+                        # The server was closed after the connection was
+                        # accepted.
+                        conn.close()
+                        return
                     if sslcontext is not None:
                         self._make_ssl_transport(
                             conn, protocol, sslcontext, server_side=True,
diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py
index c335eaee7a7146..942aac80524c03 100644
--- a/Lib/asyncio/selector_events.py
+++ b/Lib/asyncio/selector_events.py
@@ -225,6 +225,10 @@ async def _accept_connection2(
         transport = None
         try:
             protocol = protocol_factory()
+            if server is not None and not server.is_serving():
+                # The server was closed after the connection was accepted.
+                conn.close()
+                return
             waiter = self.create_future()
             if sslcontext:
                 transport = self._make_ssl_transport(
diff --git a/Lib/test/test_asyncio/test_server.py 
b/Lib/test/test_asyncio/test_server.py
index 581ea47d2dec97..09566f30a9e45a 100644
--- a/Lib/test/test_asyncio/test_server.py
+++ b/Lib/test/test_asyncio/test_server.py
@@ -5,6 +5,7 @@
 import threading
 import unittest
 
+from test import support
 from test.support import socket_helper
 from test.test_asyncio import utils as test_utils
 from test.test_asyncio import functional as func_tests
@@ -187,6 +188,37 @@ async def serve(rd, wr):
         loop.call_soon(wr.close)
         await srv.wait_closed()
 
+    async def test_close_before_transport_created(self):
+        # gh-109564: Closing the server after a connection was accepted,
+        # but before its transport was created, must drop the connection
+        # quietly.
+        connected = False
+
+        class Proto(asyncio.Protocol):
+            def connection_made(self, transport):
+                nonlocal connected
+                connected = True
+
+        def factory():
+            srv.close()
+            return Proto()
+
+        errors = []
+        loop = asyncio.get_running_loop()
+        loop.set_exception_handler(lambda loop, ctx: errors.append(ctx))
+        srv = await loop.create_server(factory, socket_helper.HOSTv4, 0)
+        self.addCleanup(srv.close)
+
+        addr = srv.sockets[0].getsockname()
+        (rd, wr) = await asyncio.open_connection(addr[0], addr[1])
+        self.addCleanup(wr.close)
+        async with asyncio.timeout(support.SHORT_TIMEOUT):
+            self.assertEqual(await rd.read(), b'')
+            await srv.wait_closed()
+
+        self.assertFalse(connected)
+        self.assertEqual(errors, [])
+
     async def test_close_clients(self):
         async def serve(rd, wr):
             try:
diff --git 
a/Misc/NEWS.d/next/Library/2026-10-28-20-36-44.gh-issue-109564.WqoyNE.rst 
b/Misc/NEWS.d/next/Library/2026-10-28-20-36-44.gh-issue-109564.WqoyNE.rst
new file mode 100644
index 00000000000000..66821f5cdc6e91
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-28-20-36-44.gh-issue-109564.WqoyNE.rst
@@ -0,0 +1,4 @@
+Fix a race in :mod:`asyncio` where closing a server after it accepted a
+connection, but before the connection's transport was created, reported an
+:exc:`AssertionError` to the exception handler. Such connections are now
+closed quietly.

_______________________________________________
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