From 3a629f0fac71d84e486923c280f8a1d6765131a1 Mon Sep 17 00:00:00 2001 From: Julian Soreavis <263610811+soreavis@users.noreply.github.com> Date: Sun, 19 Jul 2026 20:58:52 +0200 Subject: [PATCH] gh-44672: Stop pre-resolving the FTP host in urllib.request FTPHandler.ftp_open() resolved the host with socket.gethostbyname() before connecting. gethostbyname() is IPv4-only, so an ftp:// URL for an IPv6-only host failed there even when the server was reachable. As diagnosed on the tracker in 2009, ftp_open() has no need to resolve the host itself: it is passed to connect_ftp(), then to ftpwrapper, and on to ftplib.FTP.connect(). That uses socket.create_connection(), which resolves via getaddrinfo() and is IPv6-aware. Remove the pre-resolution and let the host flow through unresolved. An unresolvable host now raises socket.gaierror inside ftplib; gaierror is an OSError and therefore in ftplib.all_errors, so ftp_open()'s existing handler still turns it into URLError and callers see the same error type as before. --- Lib/test/test_urllib2.py | 41 ++++++++++++++++++- Lib/urllib/request.py | 4 -- ...6-07-19-20-57-04.gh-issue-44672.nFrMYT.rst | 4 ++ 3 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-19-20-57-04.gh-issue-44672.nFrMYT.rst diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py index d2fd111f6d9de02..2bec48a33f53bd8 100644 --- a/Lib/test/test_urllib2.py +++ b/Lib/test/test_urllib2.py @@ -787,7 +787,7 @@ def connect_ftp(self, user, passwd, host, port, dirs, # ftp authentication not yet implemented by FTPHandler self.assertEqual(h.user, user) self.assertEqual(h.passwd, passwd) - self.assertEqual(h.host, socket.gethostbyname(host)) + self.assertEqual(h.host, host) self.assertEqual(h.port, port) self.assertEqual(h.dirs, dirs) self.assertEqual(h.ftpwrapper.filename, filename) @@ -797,6 +797,45 @@ def connect_ftp(self, user, passwd, host, port, dirs, self.assertEqual(int(headers["Content-length"]), len(data)) r.close() + def test_ftp_no_hostname_resolution(self): + # gh-44672: the host must reach connect_ftp unresolved so that + # ftplib resolves it via getaddrinfo(), which supports IPv6. + class MockFTPWrapper: + def retrfile(self, filename, filetype): + return io.StringIO(""), 0 + + def close(self): + pass + + class NullFTPHandler(urllib.request.FTPHandler): + def connect_ftp(self, user, passwd, host, port, dirs, timeout): + self.host = host + return MockFTPWrapper() + + h = NullFTPHandler() + req = Request("ftp://localhost/foo/bar.html") + req.timeout = None + with mock.patch.object(socket, "gethostbyname", + side_effect=AssertionError( + "ftp_open must not pre-resolve the host")): + r = h.ftp_open(req) + r.close() + self.assertEqual(h.host, "localhost") + + def test_ftp_gaierror(self): + # gh-44672: without pre-resolution the lookup failure now surfaces + # from ftplib as socket.gaierror; it must still become a URLError. + class GaiErrorFTPHandler(urllib.request.FTPHandler): + def connect_ftp(self, user, passwd, host, port, dirs, timeout): + raise socket.gaierror(-2, "Name or service not known") + + h = GaiErrorFTPHandler() + req = Request("ftp://nonexistent.invalid/") + req.timeout = None + with self.assertRaises(urllib.error.URLError) as cm: + h.ftp_open(req) + self.assertIsInstance(cm.exception.__cause__, socket.gaierror) + @support.requires_resource("network") def test_ftp_error(self): class ErrorFTPHandler(urllib.request.FTPHandler): diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index 660301fef612588..05696498987a085 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -1526,10 +1526,6 @@ def ftp_open(self, req): user = user or '' passwd = passwd or '' - try: - host = socket.gethostbyname(host) - except OSError as msg: - raise URLError(msg) path, attrs = _splitattr(req.selector) dirs = path.split('/') dirs = list(map(unquote, dirs)) diff --git a/Misc/NEWS.d/next/Library/2026-07-19-20-57-04.gh-issue-44672.nFrMYT.rst b/Misc/NEWS.d/next/Library/2026-07-19-20-57-04.gh-issue-44672.nFrMYT.rst new file mode 100644 index 000000000000000..a74488e0b02ccc3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-19-20-57-04.gh-issue-44672.nFrMYT.rst @@ -0,0 +1,4 @@ +:func:`urllib.request.urlopen` can now fetch ``ftp://`` URLs whose host is +IPv6-only. :class:`~urllib.request.FTPHandler` no longer pre-resolves the host +with the IPv4-only :func:`socket.gethostbyname`; the unresolved host is passed to +:mod:`ftplib`, which resolves it with :func:`socket.getaddrinfo`.