diff --git a/CHANGES/8455.feature.rst b/CHANGES/8455.feature.rst new file mode 100644 index 00000000000..267e5243afa --- /dev/null +++ b/CHANGES/8455.feature.rst @@ -0,0 +1 @@ +Added :exc:`aiohttp.ClientConnectorDNSError` for differentiating DNS resolution errors from other connector errors -- by :user:`mstojcevich`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 52cb1d59ff3..3fb6686c322 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -221,6 +221,7 @@ Manuel Miranda Marat Sharafutdinov Marc Mueller Marco Paolini +Marcus Stojcevich Mariano Anaya Mariusz Masztalerczuk Marko Kohtala diff --git a/aiohttp/__init__.py b/aiohttp/__init__.py index b65dd45000b..246a9202b4e 100644 --- a/aiohttp/__init__.py +++ b/aiohttp/__init__.py @@ -8,6 +8,7 @@ ClientConnectionError, ClientConnectionResetError, ClientConnectorCertificateError, + ClientConnectorDNSError, ClientConnectorError, ClientConnectorSSLError, ClientError, @@ -127,6 +128,7 @@ "ClientConnectionError", "ClientConnectionResetError", "ClientConnectorCertificateError", + "ClientConnectorDNSError", "ClientConnectorError", "ClientConnectorSSLError", "ClientError", diff --git a/aiohttp/client.py b/aiohttp/client.py index 596d94bd8bf..93dec00a49c 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -42,6 +42,7 @@ ClientConnectionError, ClientConnectionResetError, ClientConnectorCertificateError, + ClientConnectorDNSError, ClientConnectorError, ClientConnectorSSLError, ClientError, @@ -105,6 +106,7 @@ "ClientConnectionError", "ClientConnectionResetError", "ClientConnectorCertificateError", + "ClientConnectorDNSError", "ClientConnectorError", "ClientConnectorSSLError", "ClientError", diff --git a/aiohttp/client_exceptions.py b/aiohttp/client_exceptions.py index 94991c42477..2cf6cf88328 100644 --- a/aiohttp/client_exceptions.py +++ b/aiohttp/client_exceptions.py @@ -30,6 +30,7 @@ "ClientConnectorError", "ClientProxyConnectionError", "ClientSSLError", + "ClientConnectorDNSError", "ClientConnectorSSLError", "ClientConnectorCertificateError", "ConnectionTimeoutError", @@ -206,6 +207,14 @@ def __str__(self) -> str: __reduce__ = BaseException.__reduce__ +class ClientConnectorDNSError(ClientConnectorError): + """DNS resolution failed during client connection. + + Raised in :class:`aiohttp.connector.TCPConnector` if + DNS resolution fails. + """ + + class ClientProxyConnectionError(ClientConnectorError): """Proxy connection error. diff --git a/aiohttp/connector.py b/aiohttp/connector.py index 6e3c9e18db8..1bdd14b7e25 100644 --- a/aiohttp/connector.py +++ b/aiohttp/connector.py @@ -39,6 +39,7 @@ from .client_exceptions import ( ClientConnectionError, ClientConnectorCertificateError, + ClientConnectorDNSError, ClientConnectorError, ClientConnectorSSLError, ClientHttpProxyError, @@ -1319,7 +1320,7 @@ async def _create_direct_connection( raise # in case of proxy it is not ClientProxyConnectionError # it is problem of resolving proxy ip itself - raise ClientConnectorError(req.connection_key, exc) from exc + raise ClientConnectorDNSError(req.connection_key, exc) from exc last_exc: Optional[Exception] = None addr_infos = self._convert_hosts_to_addr_infos(hosts) diff --git a/docs/client_reference.rst b/docs/client_reference.rst index 1b582932523..c48e87e14cb 100644 --- a/docs/client_reference.rst +++ b/docs/client_reference.rst @@ -2228,6 +2228,12 @@ Connection errors Derived from :exc:`ClientOSError` +.. class:: ClientConnectorDNSError + + DNS resolution error. + + Derived from :exc:`ClientConnectorError` + .. class:: ClientProxyConnectionError Derived from :exc:`ClientConnectorError` @@ -2309,6 +2315,8 @@ Hierarchy of exceptions * :exc:`ClientProxyConnectionError` + * :exc:`ClientConnectorDNSError` + * :exc:`ClientSSLError` * :exc:`ClientConnectorCertificateError` diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index f1b9c89ff97..30ceebddc97 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -3116,7 +3116,38 @@ async def test_aiohttp_request_ctx_manager_not_found() -> None: assert False, "never executed" # pragma: no cover -async def test_yield_from_in_session_request(aiohttp_client) -> None: +async def test_raising_client_connector_dns_error_on_dns_failure() -> None: + """Verify that the exception raised when a DNS lookup fails is specific to DNS.""" + with mock.patch( + "aiohttp.connector.TCPConnector._resolve_host", autospec=True, spec_set=True + ) as mock_resolve_host: + mock_resolve_host.side_effect = OSError(None, "DNS lookup failed") + with pytest.raises(aiohttp.ClientConnectorDNSError, match="DNS lookup failed"): + async with aiohttp.request("GET", "http://wrong-dns-name.com"): + assert False, "never executed" + + +async def test_aiohttp_request_coroutine(aiohttp_server: AiohttpServer) -> None: + async def handler(request: web.Request) -> web.Response: + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + server = await aiohttp_server(app) + + not_an_awaitable = aiohttp.request("GET", server.make_url("/")) + with pytest.raises( + TypeError, + match="^object _SessionRequestContextManager " + "can't be used in 'await' expression$", + ): + await not_an_awaitable # type: ignore[misc] + + await not_an_awaitable._coro # coroutine 'ClientSession._request' was never awaited + await server.close() + + +async def test_yield_from_in_session_request(aiohttp_client: AiohttpClient) -> None: # a test for backward compatibility with yield from syntax async def handler(request): return web.Response()