Pin the DNS walk against real urllib3 types, state the git.py divergence

This commit is contained in:
J. Nick Koston
2026-08-20 11:29:17 -05:00
parent ee82f77487
commit 793f9228f7
2 changed files with 20 additions and 11 deletions
+9 -5
View File
@@ -38,11 +38,15 @@ def _is_permanent_dns_failure(e: BaseException) -> bool:
if id(exc) in seen:
continue
if isinstance(exc, socket.gaierror):
# Everything except EAI_AGAIN is treated as permanent. Codes
# like EAI_NONAME can occur during a brief network outage too,
# but a 6s backoff rarely outlives one, and permanent means
# callers fall back to their cache immediately instead of
# sleeping first (the offline-build case).
# Everything except EAI_AGAIN is treated as permanent. This is
# deliberately narrower than git.py, which retries NXDOMAIN
# too: codes like EAI_NONAME can occur during a brief network
# outage, but a 6s backoff rarely outlives one, and permanent
# means callers with a cached copy fall back to it immediately
# instead of sleeping first (the offline-build case). The
# trade-off is that a hard resolver failure on a first
# download fails without retrying, same as before retries
# existed.
return exc.errno != socket.EAI_AGAIN
seen.add(id(exc))
stack.extend(
+11 -6
View File
@@ -43,18 +43,23 @@ class TestIsTransientDownloadError:
offline builds must fall back to their cache without sleeping first.
requests can surface the gaierror via the cause chain or via
urllib3's MaxRetryError.reason attribute."""
from urllib3.exceptions import MaxRetryError, NameResolutionError
gai = socket.gaierror(socket.EAI_NONAME, "nodename nor servname provided")
chained = req.ConnectionError("resolution failed")
chained.__cause__ = gai
assert not is_transient_download_error(chained)
class _FakeMaxRetryError(Exception):
def __init__(self, reason: BaseException) -> None:
super().__init__("max retries exceeded")
self.reason = reason
wrapped = req.ConnectionError(_FakeMaxRetryError(gai))
# The real urllib3 shape, raised the way urllib3 raises it: the
# gaierror is NameResolutionError.__cause__ (set at the raise
# site), which MaxRetryError carries in its reason attribute.
try:
raise NameResolutionError("example.invalid", None, gai) from gai
except NameResolutionError as nre:
wrapped = req.ConnectionError(
MaxRetryError(None, "http://example.invalid/", reason=nre)
)
assert not is_transient_download_error(wrapped)
# A garden-variety connection reset stays transient.