Description
Describe the bug
With aiohttp versions from 3.10 onwards, HTTP client requests to IPv6 link-local addresses with Zone Identifiers no longer work.
Previous versions (3.7 and 3.9 tested) work when using zone Identifiers.
I can't see any information about this change in behaviour in the changelogs so I think it is a bug.
IPv6 Zone identifiers in address literals/URIs are described in RFC6874. They specify a specific network interface to use when communicating with scoped IPv6 addresses wiki. On windows the interface is identified by the ifIndex from Get-NetAdapter in Powershell, in Linux it is the interface name.
In previous versions a request to a URL of the form https://[fe80::255:daff:fe40:6158%5]:8092/SerialNumber
would work (although it should be noted that this is not actually RFC compliant as the % literal is not escaped as %25).
In current versions the request times out without sending any packets (checked in wireshark) and a aiohttp.client_exceptions.ClientConnectorError
is raised.
To Reproduce
- Set up an IPv6 capable device as a server on your local network and record the IPv6 link local address (starts
fe80::
). - Identify the zone index of the network interface attached to the local network with the above device. On windows you can use Get-NetAdapter and record the ifIndex, on Linux use ifconfig to get the interface name (e.g eth1).
- Implement the client as below, modifying the IPv6 address and zone identifier to match those recorded above, and modifying the rest of the URL to match an endpoint on your server.
- Run the client to send the request.
import asyncio
import aiohttp
async def get_serial():
url = "https://[fe80::255:daff:fe40:6158%5]:8092/SerialNumber"
headers = {'Content-Type': 'application/json'}
print(f"Attempting to GET {url}")
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers ) as resp:
if resp.status != 200:
raise ResponseError(f"Received unexpected response {resp.status} from {url}")
parsed = await resp.json()
print(f"Server has serial number {parsed['SerialNumber']}")
if __name__ == "__main__":
asyncio.run(get_serial())
Expected behavior
A HTTP request is sent to the server and the response is received correctly.
Logs/tracebacks
> uv run -p 3.13 .\aiohttp-testcase.py
Reading inline script metadata from `aiohttp-testcase.py`
Attempting to GET https://[fe80::255:daff:fe40:6158%5]:8092/SerialNumber
Traceback (most recent call last):
File "C:\Users\PhilipD\AppData\Local\uv\cache\archive-v0\zZOczEdLHPBOvMpn0PUvM\Lib\site-packages\aiohttp\connector.py", line 1115, in _wrap_create_connection
sock = await aiohappyeyeballs.start_connection(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<5 lines>...
)
^
File "C:\Users\PhilipD\AppData\Local\uv\cache\archive-v0\zZOczEdLHPBOvMpn0PUvM\Lib\site-packages\aiohappyeyeballs\impl.py", line 104, in start_connection
raise first_exception
File "C:\Users\PhilipD\AppData\Local\uv\cache\archive-v0\zZOczEdLHPBOvMpn0PUvM\Lib\site-packages\aiohappyeyeballs\impl.py", line 82, in start_connection
sock = await _connect_sock(
^^^^^^^^^^^^^^^^^^^^
current_loop, exceptions, addrinfo, local_addr_infos
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "C:\Users\PhilipD\AppData\Local\uv\cache\archive-v0\zZOczEdLHPBOvMpn0PUvM\Lib\site-packages\aiohappyeyeballs\impl.py", line 174, in _connect_sock
await loop.sock_connect(sock, address)
File "C:\Users\PhilipD\AppData\Roaming\uv\python\cpython-3.13.1-windows-x86_64-none\Lib\asyncio\proactor_events.py", line 726, in sock_connect
return await self._proactor.connect(sock, address)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\PhilipD\AppData\Roaming\uv\python\cpython-3.13.1-windows-x86_64-none\Lib\asyncio\windows_events.py", line 804, in _poll
value = callback(transferred, key, ov)
File "C:\Users\PhilipD\AppData\Roaming\uv\python\cpython-3.13.1-windows-x86_64-none\Lib\asyncio\windows_events.py", line 600, in finish_connect
ov.getresult()
~~~~~~~~~~~~^^
OSError: [WinError 121] The semaphore timeout period has expired
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "C:\git\Aligo\DS10G\yocto\scripts\nsg-upgrade\aiohttp-testcase.py", line 34, in <module>
asyncio.run(get_serial())
~~~~~~~~~~~^^^^^^^^^^^^^^
File "C:\Users\PhilipD\AppData\Roaming\uv\python\cpython-3.13.1-windows-x86_64-none\Lib\asyncio\runners.py", line 194, in run
return runner.run(main)
~~~~~~~~~~^^^^^^
File "C:\Users\PhilipD\AppData\Roaming\uv\python\cpython-3.13.1-windows-x86_64-none\Lib\asyncio\runners.py", line 118, in run
return self._loop.run_until_complete(task)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
File "C:\Users\PhilipD\AppData\Roaming\uv\python\cpython-3.13.1-windows-x86_64-none\Lib\asyncio\base_events.py", line 720, in run_until_complete
return future.result()
~~~~~~~~~~~~~^^
File "C:\git\Aligo\DS10G\yocto\scripts\nsg-upgrade\aiohttp-testcase.py", line 26, in get_serial
async with session.get(url, headers=headers, ssl=ssl_context ) as resp:
~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\PhilipD\AppData\Local\uv\cache\archive-v0\zZOczEdLHPBOvMpn0PUvM\Lib\site-packages\aiohttp\client.py", line 1425, in __aenter__
self._resp: _RetType = await self._coro
^^^^^^^^^^^^^^^^
File "C:\Users\PhilipD\AppData\Local\uv\cache\archive-v0\zZOczEdLHPBOvMpn0PUvM\Lib\site-packages\aiohttp\client.py", line 703, in _request
conn = await self._connector.connect(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
req, traces=traces, timeout=real_timeout
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "C:\Users\PhilipD\AppData\Local\uv\cache\archive-v0\zZOczEdLHPBOvMpn0PUvM\Lib\site-packages\aiohttp\connector.py", line 548, in connect
proto = await self._create_connection(req, traces, timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\PhilipD\AppData\Local\uv\cache\archive-v0\zZOczEdLHPBOvMpn0PUvM\Lib\site-packages\aiohttp\connector.py", line 1056, in _create_connection
_, proto = await self._create_direct_connection(req, traces, timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\PhilipD\AppData\Local\uv\cache\archive-v0\zZOczEdLHPBOvMpn0PUvM\Lib\site-packages\aiohttp\connector.py", line 1400, in _create_direct_connection
raise last_exc
File "C:\Users\PhilipD\AppData\Local\uv\cache\archive-v0\zZOczEdLHPBOvMpn0PUvM\Lib\site-packages\aiohttp\connector.py", line 1369, in _create_direct_connection
transp, proto = await self._wrap_create_connection(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "C:\Users\PhilipD\AppData\Local\uv\cache\archive-v0\zZOczEdLHPBOvMpn0PUvM\Lib\site-packages\aiohttp\connector.py", line 1130, in _wrap_create_connection
raise client_error(req.connection_key, exc) from exc
aiohttp.client_exceptions.ClientConnectorError: Cannot connect to host fe80::255:daff:fe40:6158%5:8092 ssl:<ssl.SSLContext object at 0x0000029E21368B00> [The semaphore timeout period has expired]
Python Version
3.13.1
aiohttp Version
3.11.11
multidict Version
6.1.0
propcache Version
0.2.1
yarl Version
1.18.3
OS
Windows 11
Related component
Client
Additional context
This behaviour is observed using IPv6 link local addresses which are on the local network. The internet and proxies etc are not involved.
My specific test case uses https as our server requires SSL with a self signed certificate but I don't think this is related to the issue as no network packets are ever being sent.
Code of Conduct
- I agree to follow the aio-libs Code of Conduct