From 4eb358863b3797af1e5a6712b02a01f4a7fe985d Mon Sep 17 00:00:00 2001 From: goingforstudying-ctrl Date: Tue, 2 Jun 2026 19:33:02 -0400 Subject: [PATCH] fix(connector): resolve race condition in TCPConnector.close() (#12787) --- CHANGES/12497.bugfix.rst | 1 + CONTRIBUTORS.txt | 1 + aiohttp/connector.py | 7 ++++-- tests/test_connector.py | 46 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 CHANGES/12497.bugfix.rst diff --git a/CHANGES/12497.bugfix.rst b/CHANGES/12497.bugfix.rst new file mode 100644 index 00000000000..7fd5883ccbd --- /dev/null +++ b/CHANGES/12497.bugfix.rst @@ -0,0 +1 @@ +Fixed a race condition in :py:class:`~aiohttp.TCPConnector` where closing the connector while a DNS resolution was in-flight could raise :py:exc:`AttributeError` instead of :py:exc:`~aiohttp.ClientConnectionError` -- by :user:`goingforstudying-ctrl`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index d13a189129a..2aaa0a02403 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -153,6 +153,7 @@ Gary Wilson Jr. Gene Hoffman Gennady Andreyev Georges Dubus +goingforstudying-ctrl Greg Holt Gregory Haynes Grigoriy Soldatov diff --git a/aiohttp/connector.py b/aiohttp/connector.py index 3f73b1f6418..6e70b3a28a2 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()): @@ -1062,6 +1062,9 @@ async def _resolve_host( for trace in traces: await trace.send_dns_resolvehost_start(host) + if self._closed: + raise ClientConnectionError("Connector is closed") + res = await self._resolver.resolve(host, port, family=self._family) if traces: diff --git a/tests/test_connector.py b/tests/test_connector.py index a246bd38b18..0b1cbcff03e 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -4698,3 +4698,49 @@ async def test_connect_tunnel_connection_release() -> None: # Clean up to avoid resource warning conn.close() + + +async def test_tcp_connector_close_race_condition() -> None: + """Test closing TCPConnector while DNS resolution is in-flight.""" + loop = asyncio.get_running_loop() + resolve_started = loop.create_future() + close_started = loop.create_future() + + class FakeResolver(AbstractResolver): + async def resolve( + self, host: str, port: int = 0, family: int = socket.AF_INET + ) -> list[ResolveResult]: + resolve_started.set_result(None) + await close_started + return [ + { + "hostname": host, + "host": host, + "port": port, + "family": family, + "proto": 0, + "flags": socket.AI_NUMERICHOST, + } + ] + + async def close(self) -> None: + assert False + + connector = TCPConnector(use_dns_cache=False, resolver=FakeResolver()) + + async def resolve_host() -> None: + # The in-flight resolve should complete normally since close() + # happens after the resolver returns + result = await connector._resolve_host("localhost", 80) + assert len(result) == 1 + + async def close_connector() -> None: + await resolve_started + close_started.set_result(None) + await connector.close() + + await asyncio.gather(resolve_host(), close_connector()) + + # After close, new resolves should raise ClientConnectionError + with pytest.raises(aiohttp.ClientConnectionError, match="Connector is closed"): + await connector._resolve_host("localhost", 80)