Repository: aio-libs/aiohttp
Pull request: Add ClientConnectorDNSError for differentiating DNS errors from others
## What do these changes do?

Adds a ClientConnectorError that's specific to DNS resolution errors. See #8455

## Are there changes in behavior for the user?

Existing usages should still keep working the same as before since this extends from ClientConnectorError.
Users can now catch ClientConnectorDNSError if they're looking for DNS errors specifically.

## Is it a substantial burden for the maintainers to support this?

<!--
Stop right there! Pause. Just for a minute... Can you think of anything
obvious that would complicate the ongoing development of this project?

Try to consider if you'd be able to maintain it throughout the next
5 years. Does it seem viable? Tell us your thoughts! We'd very much
love to hear what the consequences of merging this patch might be...

This will help us assess if your change is something we'd want to
entertain early in the review process. Thank you in advance!
-->

I can't see this being difficult to maintain since the Resolver abstraction already exists to determine when errors are related to DNS or some other part of the connection.

## Related issue number

<!-- Are there any issues opened that will be resolved by merging this change? -->
<!-- Remember to prefix with 'Fixes' if it should close the issue (e.g. 'Fixes #123'). -->

Fixes #8455

## Checklist

- [X] I think the code is well written
- [X] Unit tests for the changes exist
- [X] Documentation reflects the changes
- [X] If you provide code modification, please add yourself to `CONTRIBUTORS.txt`
  * The format is &lt;Name&gt; &lt;Surname&gt;.
  * Please keep alphabetical order, the file is sorted by names.
- [X] Add a new news fragment into the `CHANGES/` folder
  * name it `<issue_or_pr_num>.<type>.rst` (e.g. `588.bugfix.rst`)
  * if you don't have an issue number, change it to the pull request
    number after creating the PR
    * `.bugfix`: A bug fix for something the maintainers deemed an
      improper undesired behavior that got corrected to match
      pre-agreed expectations.
    * `.feature`: A new behavior, public APIs. That sort of stuff.
    * `.deprecation`: A declaration of future API removals and breaking
      changes in behavior.
    * `.breaking`: When something public is removed in a breaking way.
      Could be deprecated in an earlier release.
    * `.doc`: Notable updates to the documentation structure or build
      process.
    * `.packaging`: Notes for downstreams about unobvious side effects
      and tooling. Changes in the test invocation considerations and
      runtime assumptions.
    * `.contrib`: Stuff that affects the contributor experience. e.g.
      Running tests, building the docs, setting up the development
      environment.
    * `.misc`: Changes that are hard to assign to any of the above
      categories.
  * Make sure to use full sentences with correct case and punctuation,
    for example:
    ```rst
    Fixed issue with non-ascii contents in doctest text files
    -- by :user:`contributor-gh-handle`.
    ```

    Use the past tense or the present tense a non-imperative mood,
    referring to what's changed compared to the last released version
    of this project.


Problem statement:
Separate exception for DNS errors
### Is your feature request related to a problem?

Currently the aiohttp client raises a `ClientConnectorError` when DNS resolution fails and there's no clean way to differentiate this `ClientConnectorError` from ones that happen at other points of the connection.

### Describe the solution you'd like

I'd like another Exception, maybe `ClientConnectorDNSError` that inherits from `ClientConnectorError` and is raised when DNS resolution fails.

### Describe alternatives you've considered

I've considered inspecting the ClientConnectorError to determine whether it's a DNS error, but it's tough to do so without making my calling code aware of the implementation details of the underlying DNS resolver.

### Related component

Client

### Additional context

_No response_

### Code of Conduct

- [X] I agree to follow the aio-libs Code of Conduct

Hints from issue discussion:

Commit message: Update CONTRIBUTORS.txt

Patch to review:
diff --git a/CHANGES/8455.feature.rst b/CHANGES/8455.feature.rst
new file mode 100644
index 00000000000..fa2e41ff132
--- /dev/null
+++ b/CHANGES/8455.feature.rst
@@ -0,0 +1 @@
+Added `ClientConnectorDNSError` for differentiating DNS resolution errors from other connector errors.
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index c97977fe8ae..ac799f2498e 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -230,6 +230,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 af5243106c0..dc11fefc949 100644
--- a/aiohttp/__init__.py
+++ b/aiohttp/__init__.py
@@ -8,6 +8,7 @@
     ClientConnectionError,
     ClientConnectionResetError,
     ClientConnectorCertificateError,
+    ClientConnectorDNSError,
     ClientConnectorError,
     ClientConnectorSSLError,
     ClientError,
@@ -120,6 +121,7 @@
     "ClientConnectionError",
     "ClientConnectionResetError",
     "ClientConnectorCertificateError",
+    "ClientConnectorDNSError",
     "ClientConnectorError",
     "ClientConnectorSSLError",
     "ClientError",
diff --git a/aiohttp/client.py b/aiohttp/client.py
index 8fce18b1a18..cb89ec839a9 100644
--- a/aiohttp/client.py
+++ b/aiohttp/client.py
@@ -44,6 +44,7 @@
     ClientConnectionError,
     ClientConnectionResetError,
     ClientConnectorCertificateError,
+    ClientConnectorDNSError,
     ClientConnectorError,
     ClientConnectorSSLError,
     ClientError,
@@ -110,6 +111,7 @@
     "ClientConnectionError",
     "ClientConnectionResetError",
     "ClientConnectorCertificateError",
+    "ClientConnectorDNSError",
     "ClientConnectorError",
     "ClientConnectorSSLError",
     "ClientError",
diff --git a/aiohttp/client_exceptions.py b/aiohttp/client_exceptions.py
index f9711bc2e71..b09c24d09e7 100644
--- a/aiohttp/client_exceptions.py
+++ b/aiohttp/client_exceptions.py
@@ -29,6 +29,7 @@
     "ClientConnectorError",
     "ClientProxyConnectionError",
     "ClientSSLError",
+    "ClientConnectorDNSError",
     "ClientConnectorSSLError",
     "ClientConnectorCertificateError",
     "ConnectionTimeoutError",
@@ -173,6 +174,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 e54ac015e19..8c76f997309 100644
--- a/aiohttp/connector.py
+++ b/aiohttp/connector.py
@@ -39,6 +39,7 @@
 from .client_exceptions import (
     ClientConnectionError,
     ClientConnectorCertificateError,
+    ClientConnectorDNSError,
     ClientConnectorError,
     ClientConnectorSSLError,
     ClientHttpProxyError,
@@ -1222,7 +1223,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 347c7d5da60..8cb6c91c2bc 100644
--- a/docs/client_reference.rst
+++ b/docs/client_reference.rst
@@ -2254,6 +2254,12 @@ Connection errors
 
    Derived from :exc:`ClientOSError`
 
+.. class:: ClientConnectorDNSError
+
+   DNS resolution error.
+
+   Derived from :exc:`ClientConnectorError`
+
 .. class:: ClientProxyConnectionError
 
    Derived from :exc:`ClientConnectorError`
@@ -2335,6 +2341,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 a0ae8ceaad9..b40409c5f4c 100644
--- a/tests/test_client_functional.py
+++ b/tests/test_client_functional.py
@@ -3315,6 +3315,23 @@ async def test_aiohttp_request_ctx_manager_not_found() -> None:
             assert False, "never executed"  # pragma: no cover
 
 
+async def test_aiohttp_request_ctx_manager_not_found_exception_is_dns_specific() -> (
+    None
+):
+    # The error raised should be specific to DNS
+    with pytest.raises(aiohttp.ClientConnectorDNSError):
+        async with aiohttp.request("GET", "http://wrong-dns-name.com"):
+            assert False, "never executed"  # pragma: no cover
+
+
+async def test_aiohttp_request_connector_error_non_dns() -> None:
+    # A non-dns counterpart to test_aiohttp_request_ctx_manager_not_found_exception_is_dns_specific
+    with pytest.raises(aiohttp.ClientConnectorError) as excinfo:
+        async with aiohttp.request("GET", "http://localhost:1/"):
+            assert False, "never executed"  # pragma: no cover
+    assert not isinstance(excinfo.value, aiohttp.ClientConnectorDNSError)
+
+
 async def test_aiohttp_request_coroutine(aiohttp_server: AiohttpServer) -> None:
     async def handler(request: web.Request) -> web.Response:
         return web.Response()


Code review comments:
diff_hunk: @@ -3315,6 +3315,23 @@ async def test_aiohttp_request_ctx_manager_not_found() -> None:
             assert False, "never executed"  # pragma: no cover
 
 
+async def test_aiohttp_request_ctx_manager_not_found_exception_is_dns_specific() -> (
+    None
+):
+    # The error raised should be specific to DNS
+    with pytest.raises(aiohttp.ClientConnectorDNSError):
+        async with aiohttp.request("GET", "http://wrong-dns-name.com"):
+            assert False, "never executed"  # pragma: no cover
+
+
+async def test_aiohttp_request_connector_error_non_dns() -> None:
+    # A non-dns counterpart to test_aiohttp_request_ctx_manager_not_found_exception_is_dns_specific
+    with pytest.raises(aiohttp.ClientConnectorError) as excinfo:
+        async with aiohttp.request("GET", "http://localhost:1/"):
+            assert False, "never executed"  # pragma: no cover
+    assert not isinstance(excinfo.value, aiohttp.ClientConnectorDNSError)
path: tests/test_client_functional.py
text: @user1:
This test is redundant as there are already plenty of tests that raise for this case

diff_hunk: @@ -3315,6 +3315,23 @@ async def test_aiohttp_request_ctx_manager_not_found() -> None:
             assert False, "never executed"  # pragma: no cover
 
 
+async def test_aiohttp_request_ctx_manager_not_found_exception_is_dns_specific() -> (
+    None
+):
+    # The error raised should be specific to DNS
+    with pytest.raises(aiohttp.ClientConnectorDNSError):
+        async with aiohttp.request("GET", "http://wrong-dns-name.com"):
+            assert False, "never executed"  # pragma: no cover
+
path: tests/test_client_functional.py
text: @user1:
This test should mock the resolver out so it does not do network I/O