Skip to content

TCPConnector.close() race: AttributeError on in-flight resolve with aiodns #12497

@masonwilde

Description

@masonwilde

Describe the bug

TCPConnector.close() nullifies the AsyncResolver's internal _resolver reference before marking the connector as closed (_closed = True). When aiodns is installed (activating AsyncResolver as the default), an in-flight connection suspended in a trace callback resumes and calls self._resolver.resolve() on a None object:

AttributeError: 'NoneType' object has no attribute 'getaddrinfo'

The teardown order in TCPConnector.close() is:

async def close(self, *, abort_ssl: bool = False) -> None:
    if self._resolver_owner:
        await self._resolver.close()       # (A) resolver._resolver = None
    await super().close(abort_ssl=...)     # (B) _closed = True

Between (A) and (B), _closed is still False and session.closed returns False, so callers have no way to detect the broken state. This only affects the non-cached resolve path (use_dns_cache=False) since the cached path's resolve task is tracked in _resolve_host_tasks and explicitly cancelled by _close_immediately.

To Reproduce

import asyncio
import sys
import aiohttp

async def main() -> None:
    async def on_dns_start(session, ctx, params):
        await asyncio.sleep(0)

    trace = aiohttp.TraceConfig()
    trace.on_dns_resolvehost_start.append(on_dns_start)

    session = aiohttp.ClientSession(
        trace_configs=[trace],
        connector=aiohttp.TCPConnector(use_dns_cache=False),
    )

    task = asyncio.create_task(session.ws_connect("wss://example.com"))
    await asyncio.sleep(0)  # let task advance to trace callback yield
    await session.close()

    try:
        await task
    except AttributeError as e:
        print(f"REPRODUCED: {e}")
    except Exception as e:
        print(f"{type(e).__name__}: {e}")

if __name__ == "__main__":
    print(f"Python {sys.version.split()[0]}, aiohttp {aiohttp.__version__}")
    asyncio.run(main())

Expected behavior

The connection attempt should fail with ClientConnectionError("Connector is closed"), not AttributeError.

Logs/tracebacks

Python 3.11.15, aiohttp 3.13.5
  REPRODUCED: 'NoneType' object has no attribute 'getaddrinfo'

Python Version

$ python --version
Python 3.11.15

and

$ python --version
Python 3.14.4

aiohttp Version

$ python -m pip show aiohttp
Name: aiohttp
Version: 3.13.5

multidict Version

$ python -m pip show multidict
Name: multidict
Version: 6.7.1

propcache Version

$ python -m pip show propcache
Name: propcache
Version: 0.5.2

yarl Version

$ python -m pip show yarl
Name: yarl
Version: 1.23.0

OS

Linux (Arch/CachyOS)

Related component

Client

Additional context

This is the same race identified in #11987, which was closed without a fix. This issue provides a deterministic reproduction and a proposed fix.

Discovered via intermittent AttributeError crashes in livekit-agents during STT stream reconnection. The STT node schedules a session.ws_connect() retry when its websocket closes unexpectedly. If this happens right before a call ends, the retry task kicks off just before session.close() runs during agent shutdown. The STT code checks session.closed before reconnecting, but that flag is still False when the resolver is already gone.

Code of Conduct

  • I agree to follow the aio-libs Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions