From dd7ab1ffb1b1259d72447aacfcff66e77d63b756 Mon Sep 17 00:00:00 2001 From: Prashant Mishra Date: Wed, 13 May 2026 23:11:22 +0530 Subject: [PATCH] Fix TCPConnector.close() race condition with in-flight DNS resolution Fixes #12497 The TCPConnector.close() method had a race condition where the resolver was closed before marking the connector as closed. This allowed in-flight DNS resolutions to resume and attempt to use a None resolver, causing AttributeError instead of the expected ClientConnectionError. The fix reorders the operations to ensure _closed is set to True (via super().close()) before closing the resolver. This way, any resumed DNS resolutions can properly detect the closed state and bail out. - Reorder close sequence: mark connector closed before closing resolver - Add regression test for in-flight DNS resolution during close --- aiohttp/connector.py | 4 ++-- tests/test_connector.py | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/aiohttp/connector.py b/aiohttp/connector.py index 547f9719d39..766513faac9 100644 --- a/aiohttp/connector.py +++ b/aiohttp/connector.py @@ -1005,10 +1005,10 @@ async def close(self, *, abort_ssl: bool = False) -> None: - If ssl_shutdown_timeout=0: connections are aborted - If ssl_shutdown_timeout>0: graceful shutdown is performed """ - if self._resolver_owner: - await self._resolver.close() # Use abort_ssl param if explicitly set, otherwise use ssl_shutdown_timeout default await super().close(abort_ssl=abort_ssl or self._ssl_shutdown_timeout == 0) + if self._resolver_owner: + await self._resolver.close() def _close_immediately(self, *, abort_ssl: bool = False) -> list[Awaitable[object]]: for fut in chain.from_iterable(self._throttle_dns_futures.values()): diff --git a/tests/test_connector.py b/tests/test_connector.py index 87d71b999f8..7a08bfcb9fb 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -1652,6 +1652,32 @@ async def test_tcp_connector_close_resolver() -> None: m_resolver.close.assert_awaited_once() +async def test_tcp_connector_close_with_in_flight_dns_resolution() -> None: + # Regression test for #12497: TCPConnector.close() race with in-flight DNS + async def on_dns_start(session, ctx, params): + await asyncio.sleep(0) + + trace = aiohttp.TraceConfig() + trace.on_dns_resolvehost_start.append(on_dns_start) + + connector = aiohttp.TCPConnector(use_dns_cache=False) + session = aiohttp.ClientSession( + trace_configs=[trace], + connector=connector, + ) + + # Create a task that will suspend in the trace callback + task = asyncio.create_task(session.ws_connect("wss://example.com")) + await asyncio.sleep(0) + + # Close the session while the task is suspended + await session.close() + + # The task should fail with ClientConnectionError, not AttributeError + with pytest.raises((aiohttp.ClientConnectionError, asyncio.CancelledError)): + await task + + async def test_dns_error(make_client_request: _RequestMaker) -> None: connector = aiohttp.TCPConnector() with mock.patch.object(