Skip to content

Fix TCPConnector.close() race condition with in-flight DNS resolution#12532

Closed
prshant70 wants to merge 1 commit into
aio-libs:masterfrom
prshant70:fix/issue-12497-tcpconnector-race-condition
Closed

Fix TCPConnector.close() race condition with in-flight DNS resolution#12532
prshant70 wants to merge 1 commit into
aio-libs:masterfrom
prshant70:fix/issue-12497-tcpconnector-race-condition

Conversation

@prshant70
Copy link
Copy Markdown

Fixes #12497

Problem

TCPConnector.close() 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.

Solution

Reordered 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.

Changes

Testing

The new test verifies that in-flight DNS resolutions during close result in proper ClientConnectionError instead of AttributeError.

Fixes aio-libs#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
@prshant70 prshant70 requested a review from asvetlov as a code owner May 13, 2026 17:46
@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
4547 1 4546 63
View the top 1 failed test(s) by shortest run time
tests.test_connector::test_tcp_connector_close_with_in_flight_dns_resolution
Stack Traces | 0.01s run time
#x1B[0m#x1B[94masync#x1B[39;49;00m #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mtest_tcp_connector_close_with_in_flight_dns_resolution#x1B[39;49;00m() -> #x1B[94mNone#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
        #x1B[90m# Regression test for #12497: TCPConnector.close() race with in-flight DNS#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[94masync#x1B[39;49;00m #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mon_dns_start#x1B[39;49;00m(session, ctx, params):#x1B[90m#x1B[39;49;00m
            #x1B[94mawait#x1B[39;49;00m asyncio.sleep(#x1B[94m0#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        trace = aiohttp.TraceConfig()#x1B[90m#x1B[39;49;00m
        trace.on_dns_resolvehost_start.append(on_dns_start)#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        connector = aiohttp.TCPConnector(use_dns_cache=#x1B[94mFalse#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
        session = aiohttp.ClientSession(#x1B[90m#x1B[39;49;00m
            trace_configs=[trace],#x1B[90m#x1B[39;49;00m
            connector=connector,#x1B[90m#x1B[39;49;00m
        )#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[90m# Create a task that will suspend in the trace callback#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        task = asyncio.create_task(session.ws_connect(#x1B[33m"#x1B[39;49;00m#x1B[33mwss://example.com#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m))#x1B[90m#x1B[39;49;00m
        #x1B[94mawait#x1B[39;49;00m asyncio.sleep(#x1B[94m0#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[90m# Close the session while the task is suspended#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[94mawait#x1B[39;49;00m session.close()#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[90m# The task should fail with ClientConnectionError, not AttributeError#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[94mwith#x1B[39;49;00m pytest.raises((aiohttp.ClientConnectionError, asyncio.CancelledError)):#x1B[90m#x1B[39;49;00m
>           #x1B[94mawait#x1B[39;49;00m task#x1B[90m#x1B[39;49;00m

connector  = <aiohttp.connector.TCPConnector object at 0x0000027D3652D700>
on_dns_start = <function test_tcp_connector_close_with_in_flight_dns_resolution.<locals>.on_dns_start at 0x0000027D36A6C900>
session    = <aiohttp.client.ClientSession object at 0x0000027D33BD7E20>
task       = <Task finished name='Task-2021' coro=<<_BaseRequestContextManager without __name__>()> exception=AttributeError("'NoneType' object has no attribute 'getaddrinfo'")>
trace      = <aiohttp.tracing.TraceConfig object at 0x0000027D3652D790>

#x1B[1m#x1B[31mtests\test_connector.py#x1B[0m:1678: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
#x1B[1m#x1B[31maiohttp\client.py#x1B[0m:1516: in send
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m._coro.send(arg)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
        arg        = None
        self       = <aiohttp.client._BaseRequestContextManager object at 0x0000027D3652D9F0>
#x1B[1m#x1B[31maiohttp\client.py#x1B[0m:1124: in _ws_connect
    #x1B[0mresp = #x1B[94mawait#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.request(#x1B[90m#x1B[39;49;00m
        auth       = None
        autoclose  = True
        autoping   = True
        compress   = 0
        decode_text = True
        default_headers = {'Connection': 'Upgrade', 'Sec-WebSocket-Version': '13', 'Upgrade': 'websocket'}
        headers    = None
        heartbeat  = None
        key        = 'Sec-WebSocket-Version'
        max_msg_size = 4194304
        method     = 'GET'
        origin     = None
        params     = None
        protocols  = ()
        proxy      = None
        proxy_auth = None
        proxy_headers = None
        real_headers = <CIMultiDict('Upgrade': 'websocket', 'Connection': 'Upgrade', 'Sec-WebSocket-Version': '13', 'Sec-WebSocket-Key': 'ep/T+gHFfZ7fqeaqvUb34w==')>
        receive_timeout = None
        sec_key    = b'ep/T+gHFfZ7fqeaqvUb34w=='
        self       = <aiohttp.client.ClientSession object at 0x0000027D33BD7E20>
        server_hostname = None
        ssl        = True
        timeout    = <_SENTINEL.sentinel: 1>
        url        = 'wss://example.com'
        value      = '13'
        ws_timeout = ClientWSTimeout(ws_receive=None, ws_close=10.0)
#x1B[1m#x1B[31maiohttp\client.py#x1B[0m:769: in _request
    #x1B[0mresp = #x1B[94mawait#x1B[39;49;00m handler(req)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
        _connect_and_send_request = <function ClientSession._request.<locals>._connect_and_send_request at 0x0000027D36A6F420>
        all_cookies = <BaseCookie: >
        allow_redirects = True
        auth       = None
        auth_from_url = None
        auto_decompress = True
        chunked    = None
        compress   = False
        cookies    = None
        data       = None
        effective_middlewares = ()
        expect100  = False
        handle     = None
        handler    = <function ClientSession._request.<locals>._connect_and_send_request at 0x0000027D36A6F420>
        headers    = <CIMultiDict('Upgrade': 'websocket', 'Connection': 'Upgrade', 'Sec-WebSocket-Version': '13', 'Sec-WebSocket-Key': 'ep/T+gHFfZ7fqeaqvUb34w==')>
        history    = []
        json       = None
        max_field_size = 8190
        max_headers = 128
        max_line_size = 8190
        max_redirects = 10
        method     = 'GET'
        middlewares = None
        params     = {}
        proxy      = None
        proxy_     = None
        proxy_auth = None
        proxy_headers = None
        raise_for_status = None
        read_bufsize = 262144
        read_until_eof = False
        real_timeout = ClientTimeout(total=300, connect=None, sock_read=None, sock_connect=30, ceil_threshold=5)
        redirects  = 0
        req        = <aiohttp.client_reqrep.ClientRequest object at 0x0000027D362F1670>
        retry_persistent_connection = True
        self       = <aiohttp.client.ClientSession object at 0x0000027D33BD7E20>
        server_hostname = None
        skip_auto_headers = None
        skip_headers = None
        ssl        = True
        str_or_url = 'wss://example.com'
        timeout    = <_SENTINEL.sentinel: 1>
        timer      = <aiohttp.helpers.TimerContext object at 0x0000027D36ACFC50>
        tm         = <aiohttp.helpers.TimeoutHandle object at 0x0000027D34D2AAC0>
        trace      = <aiohttp.tracing.Trace object at 0x0000027D3652DAF0>
        trace_request_ctx = None
        traces     = [<aiohttp.tracing.Trace object at 0x0000027D3652DAF0>]
        url        = URL('wss://example.com')
        version    = HttpVersion(major=1, minor=1)
#x1B[1m#x1B[31maiohttp\client.py#x1B[0m:723: in _connect_and_send_request
    #x1B[0mconn = #x1B[94mawait#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m._connector.connect(#x1B[90m#x1B[39;49;00m
        auto_decompress = True
        max_field_size = 8190
        max_headers = 128
        max_line_size = 8190
        read_bufsize = 262144
        read_until_eof = False
        real_timeout = ClientTimeout(total=300, connect=None, sock_read=None, sock_connect=30, ceil_threshold=5)
        req        = <aiohttp.client_reqrep.ClientRequest object at 0x0000027D362F1670>
        self       = <aiohttp.client.ClientSession object at 0x0000027D33BD7E20>
        timer      = <aiohttp.helpers.TimerContext object at 0x0000027D36ACFC50>
        traces     = [<aiohttp.tracing.Trace object at 0x0000027D3652DAF0>]
#x1B[1m#x1B[31maiohttp\connector.py#x1B[0m:611: in connect
    #x1B[0mproto = #x1B[94mawait#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m._create_connection(req, traces, timeout)#x1B[90m#x1B[39;49;00m
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
        conn       = None
        key        = ConnectionKey(host='example.com', port=443, is_ssl=True, ssl=True, proxy=None, proxy_auth=None, proxy_headers_hash=None)
        placeholder = <aiohttp.connector._TransportPlaceholder object at 0x0000027D363F3CD0>
        req        = <aiohttp.client_reqrep.ClientRequest object at 0x0000027D362F1670>
        self       = <aiohttp.connector.TCPConnector object at 0x0000027D3652D700>
        timeout    = ClientTimeout(total=300, connect=None, sock_read=None, sock_connect=30, ceil_threshold=5)
        trace      = <aiohttp.tracing.Trace object at 0x0000027D3652DAF0>
        traces     = [<aiohttp.tracing.Trace object at 0x0000027D3652DAF0>]
#x1B[1m#x1B[31maiohttp\connector.py#x1B[0m:1185: in _create_connection
    #x1B[0m_, proto = #x1B[94mawait#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m._create_direct_connection(req, traces, timeout)#x1B[90m#x1B[39;49;00m
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
        req        = <aiohttp.client_reqrep.ClientRequest object at 0x0000027D362F1670>
        self       = <aiohttp.connector.TCPConnector object at 0x0000027D3652D700>
        timeout    = ClientTimeout(total=300, connect=None, sock_read=None, sock_connect=30, ceil_threshold=5)
        traces     = [<aiohttp.tracing.Trace object at 0x0000027D3652DAF0>]
#x1B[1m#x1B[31maiohttp\connector.py#x1B[0m:1449: in _create_direct_connection
    #x1B[0mhosts = #x1B[94mawait#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m._resolve_host(host, port, traces=traces)#x1B[90m#x1B[39;49;00m
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
        client_error = <class 'aiohttp.client_exceptions.ClientConnectorError'>
        fingerprint = None
        host       = 'example.com'
        port       = 443
        req        = <aiohttp.client_reqrep.ClientRequest object at 0x0000027D362F1670>
        self       = <aiohttp.connector.TCPConnector object at 0x0000027D3652D700>
        sslcontext = <ssl.SSLContext object at 0x0000027D3016FB50>
        timeout    = ClientTimeout(total=300, connect=None, sock_read=None, sock_connect=30, ceil_threshold=5)
        traces     = [<aiohttp.tracing.Trace object at 0x0000027D3652DAF0>]
#x1B[1m#x1B[31maiohttp\connector.py#x1B[0m:1065: in _resolve_host
    #x1B[0mres = #x1B[94mawait#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m._resolver.resolve(host, port, family=#x1B[96mself#x1B[39;49;00m._family)#x1B[90m#x1B[39;49;00m
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
        host       = 'example.com'
        port       = 443
        self       = <aiohttp.connector.TCPConnector object at 0x0000027D3652D700>
        trace      = <aiohttp.tracing.Trace object at 0x0000027D3652DAF0>
        traces     = [<aiohttp.tracing.Trace object at 0x0000027D3652DAF0>]
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <aiohttp.resolver.AsyncResolver object at 0x0000027D3652D7F0>
host = 'example.com', port = 443, family = <AddressFamily.AF_UNSPEC: 0>

    #x1B[0m#x1B[94masync#x1B[39;49;00m #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mresolve#x1B[39;49;00m(#x1B[90m#x1B[39;49;00m
        #x1B[96mself#x1B[39;49;00m, host: #x1B[96mstr#x1B[39;49;00m, port: #x1B[96mint#x1B[39;49;00m = #x1B[94m0#x1B[39;49;00m, family: socket.AddressFamily = socket.AF_INET#x1B[90m#x1B[39;49;00m
    ) -> #x1B[96mlist#x1B[39;49;00m[ResolveResult]:#x1B[90m#x1B[39;49;00m
        #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
>           resp = #x1B[94mawait#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m._resolver.getaddrinfo(#x1B[90m#x1B[39;49;00m
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
                host,#x1B[90m#x1B[39;49;00m
                port=port,#x1B[90m#x1B[39;49;00m
                #x1B[96mtype#x1B[39;49;00m=socket.SOCK_STREAM,#x1B[90m#x1B[39;49;00m
                family=family,#x1B[90m#x1B[39;49;00m
                flags=_AI_ADDRCONFIG,#x1B[90m#x1B[39;49;00m
            )#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE           AttributeError: 'NoneType' object has no attribute 'getaddrinfo'#x1B[0m

family     = <AddressFamily.AF_UNSPEC: 0>
host       = 'example.com'
port       = 443
self       = <aiohttp.resolver.AsyncResolver object at 0x0000027D3652D7F0>

#x1B[1m#x1B[31maiohttp\resolver.py#x1B[0m:108: AttributeError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 13, 2026

Merging this PR will improve performance by 8.56%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

#### 🎉 Hooray! `pytest-codspeed` just leveled up to 5.0.1!

A heads-up, this is a breaking change and it might affect your current performance baseline a bit. But here's the exciting part - it's packed with new, cool features and promises improved result stability 🥳!
Curious about what's new? Visit our releases page to delve into all the awesome details about this new version.

⚡ 1 improved benchmark
✅ 71 untouched benchmarks
⏩ 69 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
test_read_large_binary_websocket_messages 48.9 µs 45 µs +8.56%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing prshant70:fix/issue-12497-tcpconnector-race-condition (dd7ab1f) with master (65b42bb)2

Open in CodSpeed

Footnotes

  1. 69 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on master (2e03c13) during the generation of this report, so 65b42bb was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Dreamsorcerer
Copy link
Copy Markdown
Member

The test fails and there's already a fix in #12498.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants