From b6f0ab9b687e7035669b9bbe352b1d3cbfd7af28 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 29 Oct 2026 11:08:19 +0100 Subject: [PATCH] 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 Co-authored-by: Claude Opus 5.5 --- Lib/asyncio/proactor_events.py | 5 +++ Lib/asyncio/selector_events.py | 4 +++ Lib/test/test_asyncio/test_server.py | 32 +++++++++++++++++++ ...-10-28-20-36-44.gh-issue-109564.WqoyNE.rst | 4 +++ 4 files changed, 45 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-28-20-36-44.gh-issue-109564.WqoyNE.rst diff --git a/Lib/asyncio/proactor_events.py b/Lib/asyncio/proactor_events.py index da1af53b1224f4c..1b2266901d90345 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 c335eaee7a7146c..942aac80524c034 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 581ea47d2dec976..09566f30a9e45a7 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 000000000000000..66821f5cdc6e919 --- /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.