From fc660d107a3cdae9c551491b5497538e83146b74 Mon Sep 17 00:00:00 2001 From: Marcus Stojcevich <129109254+mstojcevich-cisco@users.noreply.github.com> Date: Thu, 10 Oct 2024 13:40:13 -0400 Subject: [PATCH] Add ClientConnectorDNSError for differentiating DNS errors from others (#8456) Co-authored-by: J. Nick Koston (cherry picked from commit b09d7cc07607d01badf8051f5b8feb2a2ed070c8) --- CHANGES/8455.feature.rst | 1 + CONTRIBUTORS.txt | 1 + aiohttp/__init__.py | 2 ++ aiohttp/client.py | 2 ++ aiohttp/client_exceptions.py | 9 +++++++++ aiohttp/connector.py | 3 ++- docs/client_reference.rst | 8 ++++++++ tests/test_client_functional.py | 33 ++++++++++++++++++++++++++++++++- 8 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 CHANGES/8455.feature.rst 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 e34aab90cf5..ef0d7d81429 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -223,6 +223,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 c5f13c6dc49..a08e5406900 100644 --- a/aiohttp/__init__.py +++ b/aiohttp/__init__.py @@ -8,6 +8,7 @@ ClientConnectionError, ClientConnectionResetError, ClientConnectorCertificateError, + ClientConnectorDNSError, ClientConnectorError, ClientConnectorSSLError, ClientError, @@ -128,6 +129,7 @@ "ClientConnectionError", "ClientConnectionResetError", "ClientConnectorCertificateError", + "ClientConnectorDNSError", "ClientConnectorError", "ClientConnectorSSLError", "ClientError", diff --git a/aiohttp/client.py b/aiohttp/client.py index 6d6660e306a..1be2629dcd9 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -42,6 +42,7 @@ ClientConnectionError, ClientConnectionResetError, ClientConnectorCertificateError, + ClientConnectorDNSError, ClientConnectorError, ClientConnectorSSLError, ClientError, @@ -109,6 +110,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 da503bded53..1be9d6d1201 100644 --- a/aiohttp/connector.py +++ b/aiohttp/connector.py @@ -38,6 +38,7 @@ from .client_exceptions import ( ClientConnectionError, ClientConnectorCertificateError, + ClientConnectorDNSError, ClientConnectorError, ClientConnectorSSLError, ClientHttpProxyError, @@ -1328,7 +1329,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 8495ecd9d8e..06b580b6338 100644 --- a/docs/client_reference.rst +++ b/docs/client_reference.rst @@ -2272,6 +2272,12 @@ Connection errors Derived from :exc:`ClientOSError` +.. class:: ClientConnectorDNSError + + DNS resolution error. + + Derived from :exc:`ClientConnectorError` + .. class:: ClientProxyConnectionError Derived from :exc:`ClientConnectorError` @@ -2353,6 +2359,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 d39addc29a1..80cf56f8118 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -3248,7 +3248,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()