Repository: urllib3/urllib3

Issue:
DNS lookup error exception
I am using `requests` to check domain status for `k8s.io` domains (https://github.com/kubernetes/k8s.io/pull/30) and I can't find a good way to check for DNS lookup error like `ERR_NAME_NOT_RESOLVED` in Chrome. Exceptions output seems platform specific:

```
    Traceback (most recent call last):
      File "check.py", line 11, in <module>
        status = requests.head('http://' + site)
      File "C:\Python27\lib\site-packages\requests\api.py", line 93, in head
        return request('head', url, **kwargs)
      File "C:\Python27\lib\site-packages\requests\api.py", line 53, in request
        return session.request(method=method, url=url, **kwargs)
      File "C:\Python27\lib\site-packages\requests\sessions.py", line 468, in request
        resp = self.send(prep, **send_kwargs)
      File "C:\Python27\lib\site-packages\requests\sessions.py", line 576, in send
        r = adapter.send(request, **kwargs)
      File "C:\Python27\lib\site-packages\requests\adapters.py", line 437, in send
        raise ConnectionError(e, request=request)
    requests.exceptions.ConnectionError: HTTPConnectionPool(host='jenkins.k8s.io', port=80):
        Max retries exceeded with url: / (Caused by
        NewConnectionError('<requests.packages.urllib3.connection.HTTPConnection object at
        0x029E43B0>: Failed to establish a new connection: [Errno 11001] getaddrinfo failed',))
```

So I was redirected here from https://github.com/kennethreitz/requests/issues/3630#issuecomment-255031285 to see if `urllib3` could expose more fine-grained DNSLookupError exception and maybe remove MaxRetryError wrapper.
## 



Discussion:
Ok, so.

We're certainly not going to remove the `MaxRetryError`: that's not a winner. But we can probably be more clear about the specific error by hoisting the `getaddrinfo` call on line 75 of `urllib3/util/connection.py` out of the `for` statement and shoving it in a `try...except` that raises an appropriate urllib3 exception.

That would at least give you a specific exception to try to haul out. `MaxRetryError` has a `reason` attribute which you can use to fetch the underlying exception, so you can from that point take the exception.

Note, however, that this change will take a while to propagate into a released version of Requests. In the interim, you may find it more helpful to check for DNS errors by simply calling `socket.getaddrinfo` yourself before making the request.

Too late. =) I've already done exception handling through string parsing - https://github.com/kennethreitz/requests/issues/3630#issuecomment-255100951

So how about reusing Chrome's `ERR_NAME_NOT_RESOLVED` constant which seems to have cross-platform value -105? https://cs.chromium.org/chromium/src/net/base/net_error_list.h?sq=package:chromium&dr=C&rcl=1477077889&l=130

Or it could be `NXDOMAIN` error code 3 - https://support.opendns.com/hc/en-us/articles/227986827-FAQ-What-are-common-DNS-return-or-response-codes-

I don't know that we gain anything from using a constant: it's just not that Pythonic an approach. Right now we're propagating the error code returned from getaddrinfo, which is the bare minimum of effort. If we're going to put in more effort we should do it Pythonically, which means instead of using an error code we should use a different exception type.

So, is `DNSLookupError` a good name for this extension?

Maybe, or perhaps `NameResolutionError`. Either would be reasonable.

The PR is closed automatically, because I am MIA, but if anyone wants to continue fixing this issue, the code is still here - https://github.com/urllib3/urllib3/pull/1008

I came here, because I got another upvote on Stack Overflow.

Resolving patch:
diff --git a/src/urllib3/connection.py b/src/urllib3/connection.py
--- a/src/urllib3/connection.py
+++ b/src/urllib3/connection.py
@@ -44,6 +44,7 @@ class BaseSSLError(BaseException):  # type: ignore
 from .exceptions import (
     ConnectTimeoutError,
     HTTPSProxyError,
+    NameResolutionError,
     NewConnectionError,
     SystemTimeWarning,
 )
@@ -205,7 +206,8 @@ def _new_conn(self) -> socket.socket:
                 source_address=self.source_address,
                 socket_options=self.socket_options,
             )
-
+        except socket.gaierror as e:
+            raise NameResolutionError(self.host, e)
         except SocketTimeout:
             raise ConnectTimeoutError(
                 self,
diff --git a/src/urllib3/connectionpool.py b/src/urllib3/connectionpool.py
--- a/src/urllib3/connectionpool.py
+++ b/src/urllib3/connectionpool.py
@@ -30,6 +30,7 @@
     InsecureRequestWarning,
     LocationValueError,
     MaxRetryError,
+    NameResolutionError,
     NewConnectionError,
     ProtocolError,
     ProxyError,
@@ -738,12 +739,15 @@ def urlopen(  # type: ignore
             SSLError,
             CertificateError,
             HTTPSProxyError,
+            NameResolutionError,
         ) as e:
             # Discard the connection for these exceptions. It will be
             # replaced during the next _get_conn() call.
             clean_exit = False
             if isinstance(e, (BaseSSLError, CertificateError)):
                 e = SSLError(e)
+            elif isinstance(e, NameResolutionError):
+                pass
             elif isinstance(e, (OSError, NewConnectionError)) and self.proxy:
                 e = ProxyError("Cannot connect to proxy.", e)
             elif isinstance(e, (OSError, HTTPException)):
diff --git a/src/urllib3/exceptions.py b/src/urllib3/exceptions.py
--- a/src/urllib3/exceptions.py
+++ b/src/urllib3/exceptions.py
@@ -1,3 +1,4 @@
+import socket
 import warnings
 from http.client import IncompleteRead as httplib_IncompleteRead
 from typing import TYPE_CHECKING, Callable, List, Optional, Tuple, Union
@@ -376,3 +377,11 @@ class UnrewindableBodyError(HTTPError):
     """urllib3 encountered an error when trying to rewind a body"""
 
     pass
+
+
+class NameResolutionError(HTTPError, socket.gaierror):
+    """Raised when host name resolution fails."""
+
+    def __init__(self, host: str, reason: socket.gaierror):
+        message = f"Failed to resolve '{host}' ({reason})"
+        HTTPError.__init__(self, message)


Test patch:
diff --git a/test/__init__.py b/test/__init__.py
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -41,7 +41,7 @@
 VALID_SOURCE_ADDRESSES = [(("::1", 0), True), (("127.0.0.1", 0), False)]
 # RFC 5737: 192.0.2.0/24 is for testing only.
 # RFC 3849: 2001:db8::/32 is for documentation only.
-INVALID_SOURCE_ADDRESSES = [("192.0.2.255", 0), ("2001:db8::1", 0)]
+INVALID_SOURCE_ADDRESSES = [(("192.0.2.255", 0), False), (("2001:db8::1", 0), True)]
 
 # We use timeouts in three different ways in our tests
 #
diff --git a/test/test_util.py b/test/test_util.py
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -762,6 +762,20 @@ def test_create_connection_error(self, getaddrinfo):
         with pytest.raises(OSError, match="getaddrinfo returns an empty list"):
             create_connection(("example.com", 80))
 
+    @patch("socket.getaddrinfo")
+    def test_dnsresolver_forced_error(self, getaddrinfo):
+        getaddrinfo.side_effect = socket.gaierror()
+        with pytest.raises(socket.gaierror):
+            # dns is valid but we force the error just for the sake of the test
+            create_connection(("example.com", 80))
+
+    def test_dnsresolver_expected_error(self):
+        with pytest.raises(socket.gaierror):
+            # windows: [Errno 11001] getaddrinfo failed in windows
+            # linux: [Errno -2] Name or service not known
+            # macos: [Errno 8] nodename nor servname provided, or not known
+            create_connection(("badhost.invalid", 80))
+
     @pytest.mark.parametrize(
         "input,params,expected",
         (
diff --git a/test/with_dummyserver/test_connectionpool.py b/test/with_dummyserver/test_connectionpool.py
--- a/test/with_dummyserver/test_connectionpool.py
+++ b/test/with_dummyserver/test_connectionpool.py
@@ -22,6 +22,7 @@
     DecodeError,
     EmptyPoolError,
     MaxRetryError,
+    NameResolutionError,
     NewConnectionError,
     ReadTimeoutError,
     UnrewindableBodyError,
@@ -417,7 +418,7 @@ def test_bad_connect(self):
         with HTTPConnectionPool("badhost.invalid", self.port) as pool:
             with pytest.raises(MaxRetryError) as e:
                 pool.request("GET", "/", retries=5)
-            assert type(e.value.reason) == NewConnectionError
+            assert type(e.value.reason) == NameResolutionError
 
     def test_keepalive(self):
         with HTTPConnectionPool(self.host, self.port, block=True, maxsize=1) as pool:
@@ -709,13 +710,19 @@ def test_source_address(self):
                 r = pool.request("GET", "/source_address")
                 assert r.data == addr[0].encode()
 
-    def test_source_address_error(self):
-        for addr in INVALID_SOURCE_ADDRESSES:
-            with HTTPConnectionPool(
-                self.host, self.port, source_address=addr, retries=False
-            ) as pool:
+    @pytest.mark.parametrize(
+        "invalid_source_address, is_ipv6", INVALID_SOURCE_ADDRESSES
+    )
+    def test_source_address_error(self, invalid_source_address, is_ipv6):
+        with HTTPConnectionPool(
+            self.host, self.port, source_address=invalid_source_address, retries=False
+        ) as pool:
+            if is_ipv6:
+                with pytest.raises(NameResolutionError):
+                    pool.request("GET", f"/source_address?{invalid_source_address}")
+            else:
                 with pytest.raises(NewConnectionError):
-                    pool.request("GET", f"/source_address?{addr}")
+                    pool.request("GET", f"/source_address?{invalid_source_address}")
 
     def test_stream_keepalive(self):
         x = 2
@@ -1024,7 +1031,7 @@ def test_disabled_retry(self):
         with HTTPConnectionPool(
             "thishostdoesnotexist.invalid", self.port, timeout=0.001
         ) as pool:
-            with pytest.raises(NewConnectionError):
+            with pytest.raises(NameResolutionError):
                 pool.request("GET", "/test", retries=False)
 
     def test_read_retries(self):
diff --git a/test/with_dummyserver/test_proxy_poolmanager.py b/test/with_dummyserver/test_proxy_poolmanager.py
--- a/test/with_dummyserver/test_proxy_poolmanager.py
+++ b/test/with_dummyserver/test_proxy_poolmanager.py
@@ -16,7 +16,7 @@
     ConnectTimeoutError,
     HTTPSProxyError,
     MaxRetryError,
-    ProxyError,
+    NameResolutionError,
     ProxySchemeUnknown,
     ProxySchemeUnsupported,
     SSLError,
@@ -146,7 +146,7 @@ def test_proxy_conn_fail(self):
 
             with pytest.raises(MaxRetryError) as e:
                 http.request("GET", f"{self.http_url}/")
-            assert type(e.value.reason) == ProxyError
+            assert type(e.value.reason) == NameResolutionError
 
     def test_https_conn_failed(self):
         """