Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES/5357.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fixed resolving ``localhost`` on Windows to fall back without ``AI_ADDRCONFIG``
when the first lookup fails, so ``localhost`` still works without an active
network.
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ Nándor Mátravölgyi
Oisin Aylward
Olaf Conradi
Omkar Kabde
oppnc
Pahaz Blinov
Panagiotis Kolokotronis
Pankaj Pandey
Expand Down
56 changes: 42 additions & 14 deletions aiohttp/resolver.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import socket
import sys
import weakref
from typing import Any, Final, Optional

Expand All @@ -22,6 +23,11 @@
_AI_ADDRCONFIG = socket.AI_ADDRCONFIG
if hasattr(socket, "AI_MASK"):
_AI_ADDRCONFIG &= socket.AI_MASK
_IS_WINDOWS = sys.platform == "win32"


def _is_windows_localhost(host: str) -> bool:
return _IS_WINDOWS and host.rstrip(".").casefold() == "localhost"


class ThreadedResolver(AbstractResolver):
Expand All @@ -37,13 +43,24 @@ def __init__(self, loop: asyncio.AbstractEventLoop | None = None) -> None:
async def resolve(
self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_INET
) -> list[ResolveResult]:
infos = await self._loop.getaddrinfo(
host,
port,
type=socket.SOCK_STREAM,
family=family,
flags=_AI_ADDRCONFIG,
)
try:
infos = await self._loop.getaddrinfo(
host,
port,
type=socket.SOCK_STREAM,
family=family,
flags=_AI_ADDRCONFIG,
)
except socket.gaierror:
if not _is_windows_localhost(host):
raise
infos = await self._loop.getaddrinfo(
host,
port,
type=socket.SOCK_STREAM,
family=family,
flags=0,
)

hosts: list[ResolveResult] = []
for family, _, proto, _, address in infos:
Expand Down Expand Up @@ -114,13 +131,24 @@ async def resolve(
self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_INET
) -> list[ResolveResult]:
try:
resp = await self._resolver.getaddrinfo(
host,
port=port,
type=socket.SOCK_STREAM,
family=family,
flags=_AI_ADDRCONFIG,
)
try:
resp = await self._resolver.getaddrinfo(
host,
port=port,
type=socket.SOCK_STREAM,
family=family,
flags=_AI_ADDRCONFIG,
)
except aiodns.error.DNSError:
if not _is_windows_localhost(host):
raise
resp = await self._resolver.getaddrinfo(
host,
port=port,
type=socket.SOCK_STREAM,
family=family,
flags=0,
)
except aiodns.error.DNSError as exc:
msg = exc.args[1] if len(exc.args) >= 1 else "DNS lookup failed"
raise OSError(None, msg) from exc
Expand Down
102 changes: 101 additions & 1 deletion tests/test_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from collections.abc import Awaitable, Callable, Collection, Generator
from ipaddress import ip_address
from typing import Any, NamedTuple
from unittest.mock import Mock, create_autospec, patch
from unittest.mock import AsyncMock, Mock, call, create_autospec, patch

import pytest

Expand Down Expand Up @@ -233,6 +233,44 @@ async def test_async_resolver_negative_lookup(loop: asyncio.AbstractEventLoop) -
await resolver.close()


@pytest.mark.skipif(not getaddrinfo, reason="aiodns >=3.2.0 required")
@pytest.mark.usefixtures("check_no_lingering_resolvers")
async def test_async_resolver_retries_localhost_without_addrconfig_on_windows(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr("aiohttp.resolver._IS_WINDOWS", True)
with patch("aiodns.DNSResolver") as mock:
mock().getaddrinfo.side_effect = [
aiodns.error.DNSError(1, "addrconfig failed"),
fake_aiodns_getaddrinfo_ipv4_result(["127.0.0.1"]),
]
resolver = AsyncResolver()
try:
real = await resolver.resolve("localhost", family=socket.AF_UNSPEC)
finally:
await resolver.close()

assert real[0]["host"] == "127.0.0.1"
mock().getaddrinfo.assert_has_calls(
[
call(
"localhost",
family=socket.AF_UNSPEC,
flags=socket.AI_ADDRCONFIG,
port=0,
type=socket.SOCK_STREAM,
),
call(
"localhost",
family=socket.AF_UNSPEC,
flags=0,
port=0,
type=socket.SOCK_STREAM,
),
]
)


@pytest.mark.skipif(not getaddrinfo, reason="aiodns >=3.2.0 required")
@pytest.mark.usefixtures("check_no_lingering_resolvers")
async def test_async_resolver_no_hosts_in_getaddrinfo(
Expand All @@ -255,6 +293,68 @@ async def test_threaded_resolver_positive_lookup() -> None:
ipaddress.ip_address(real[0]["host"])


async def test_threaded_resolver_retries_localhost_without_addrconfig_on_windows(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr("aiohttp.resolver._IS_WINDOWS", True)
loop = Mock()
loop.getaddrinfo = AsyncMock(
side_effect=[
socket.gaierror(),
[(socket.AF_INET, None, socket.SOCK_STREAM, None, ("127.0.0.1", 0))],
]
)
resolver = ThreadedResolver()
resolver._loop = loop

real = await resolver.resolve("localhost", family=socket.AF_UNSPEC)

assert real[0]["host"] == "127.0.0.1"
loop.getaddrinfo.assert_has_calls(
[
call(
"localhost",
0,
type=socket.SOCK_STREAM,
family=socket.AF_UNSPEC,
flags=socket.AI_ADDRCONFIG,
),
call(
"localhost",
0,
type=socket.SOCK_STREAM,
family=socket.AF_UNSPEC,
flags=0,
),
]
)


@pytest.mark.parametrize(
("host", "is_windows"),
(("localhost", False), ("www.python.org", True)),
)
async def test_threaded_resolver_keeps_addrconfig_without_localhost_windows_fallback(
host: str, is_windows: bool, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr("aiohttp.resolver._IS_WINDOWS", is_windows)
loop = Mock()
loop.getaddrinfo = AsyncMock(side_effect=socket.gaierror())
resolver = ThreadedResolver()
resolver._loop = loop

with pytest.raises(socket.gaierror):
await resolver.resolve(host, family=socket.AF_UNSPEC)

loop.getaddrinfo.assert_called_once_with(
host,
0,
type=socket.SOCK_STREAM,
family=socket.AF_UNSPEC,
flags=socket.AI_ADDRCONFIG,
)


async def test_threaded_resolver_positive_ipv6_link_local_lookup() -> None:
loop = Mock()
loop.getaddrinfo = fake_ipv6_addrinfo(["fe80::1"])
Expand Down
Loading