diff --git a/newsfragments/1249.bugfix.rst b/newsfragments/1249.bugfix.rst new file mode 100644 index 0000000000..92c96d2d1b --- /dev/null +++ b/newsfragments/1249.bugfix.rst @@ -0,0 +1 @@ +`trio.socket.getaddrinfo` no longer retries in a worker thread or calls the custom hostname resolver when the caller already passed both ``AI_NUMERICHOST`` and ``AI_NUMERICSERV``; the ``EAI_NONAME`` error from the non-blocking attempt is raised directly. diff --git a/src/trio/_socket.py b/src/trio/_socket.py index 504874553a..1b4c320e64 100644 --- a/src/trio/_socket.py +++ b/src/trio/_socket.py @@ -207,11 +207,13 @@ async def getaddrinfo( # If host and port are numeric, then getaddrinfo doesn't block and we can # skip the whole thread thing, which seems worthwhile. So we try first # with the _NUMERIC_ONLY flags set, and then only spawn a thread if that - # fails with EAI_NONAME: + # fails with EAI_NONAME. If the caller already asked for _NUMERIC_ONLY, + # a retry would make the exact same call, so let the error propagate: def numeric_only_failure(exc: BaseException) -> bool: return ( isinstance(exc, _stdlib_socket.gaierror) and exc.errno == _stdlib_socket.EAI_NONAME + and flags & _NUMERIC_ONLY != _NUMERIC_ONLY ) async with _try_sync(numeric_only_failure): diff --git a/src/trio/_tests/test_socket.py b/src/trio/_tests/test_socket.py index 03918b0e1f..d9df1f51a7 100644 --- a/src/trio/_tests/test_socket.py +++ b/src/trio/_tests/test_socket.py @@ -1128,6 +1128,33 @@ async def getnameinfo( assert await tsocket.getaddrinfo("host", "port") == "x" +async def test_getaddrinfo_numeric_only_flags_skip_resolver() -> None: + # If the caller already passed AI_NUMERICHOST|AI_NUMERICSERV, the fast + # path call is identical to the one a worker thread or custom resolver + # would make, so its EAI_NONAME failure must propagate directly. + calls: list[tuple[object, ...]] = [] + + class RecordingResolver: + async def getaddrinfo(self, *args: object) -> list[object]: + calls.append(args) + return [] + + async def getnameinfo(self, *args: object) -> tuple[str, str]: + raise NotImplementedError + + resolver = RecordingResolver() + assert await resolver.getaddrinfo(b"example.invalid", 80) == [] + seen = len(calls) + tsocket.set_custom_hostname_resolver(resolver) # type: ignore[arg-type] + try: + with pytest.raises(tsocket.gaierror) as excinfo: + await tsocket.getaddrinfo("example.invalid", 80, flags=_NUMERIC_ONLY) + assert excinfo.value.errno == tsocket.EAI_NONAME + assert len(calls) == seen + finally: + tsocket.set_custom_hostname_resolver(None) + + async def test_custom_socket_factory() -> None: class CustomSocketFactory: def socket(