Raise NameResolutionError instead of socket.gaierror
The new exception still derives from `socket.gaierror`, so this isn't a breaking change.
MODIFY
src/urllib3/connection.py
src/urllib3/connection.py
@@ -44,6 +44,7 @@ from ._version import __version__
from .exceptions import (
ConnectTimeoutError,
HTTPSProxyError,
+ NameResolutionError,
NewConnectionError,
SystemTimeWarning,
)
@@ -205,7 +206,8 @@ class HTTPConnection(_HTTPConnection):
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,

MODIFY
src/urllib3/connectionpool.py
src/urllib3/connectionpool.py
@@ -30,6 +30,7 @@ from .exceptions import (
InsecureRequestWarning,
LocationValueError,
MaxRetryError,
+ NameResolutionError,
NewConnectionError,
ProtocolError,
ProxyError,
@@ -738,12 +739,15 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
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)):

MODIFY
src/urllib3/exceptions.py
src/urllib3/exceptions.py
+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)

MODIFY
test/__init__.py
test/__init__.py
@@ -41,7 +41,7 @@ else:
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
#

MODIFY
test/test_util.py
test/test_util.py
@@ -762,6 +762,20 @@ class TestUtil:
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",
(

MODIFY
test/with_dummyserver/test_connectionpool.py
test/with_dummyserver/test_connectionpool.py
@@ -22,6 +22,7 @@ from urllib3.exceptions import (
DecodeError,
EmptyPoolError,
MaxRetryError,
+ NameResolutionError,
NewConnectionError,
ReadTimeoutError,
UnrewindableBodyError,
@@ -417,7 +418,7 @@ class TestConnectionPool(HTTPDummyServerTestCase):
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 @@ class TestConnectionPool(HTTPDummyServerTestCase):
r = pool.request("GET", "/source_address")
assert r.data == addr[0].encode()
- def test_source_address_error(self):
- for addr in INVALID_SOURCE_ADDRESSES:
+ @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=addr, retries=False
+ 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 @@ class TestRetry(HTTPDummyServerTestCase):
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):

MODIFY
test/with_dummyserver/test_proxy_poolmanager.py
test/with_dummyserver/test_proxy_poolmanager.py
@@ -16,7 +16,7 @@ from urllib3.exceptions import (
ConnectTimeoutError,
HTTPSProxyError,
MaxRetryError,
- ProxyError,
+ NameResolutionError,
ProxySchemeUnknown,
ProxySchemeUnsupported,
SSLError,
@@ -146,7 +146,7 @@ class TestHTTPProxyManager(HTTPDummyProxyTestCase):
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):
"""

Raise NameResolutionError instead of socket.gaierror (#2305)

The new exception still derives from `socket.gaierror`, so this isn't a breaking change.