Skip to content

gh-44672: Stop pre-resolving the FTP host in urllib.request#154192

Open
soreavis wants to merge 1 commit into
python:mainfrom
soreavis:gh-44672-ftp-no-preresolve
Open

gh-44672: Stop pre-resolving the FTP host in urllib.request#154192
soreavis wants to merge 1 commit into
python:mainfrom
soreavis:gh-44672-ftp-no-preresolve

Conversation

@soreavis

@soreavis soreavis commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

urllib.request.FTPHandler.ftp_open() pre-resolves the host with socket.gethostbyname() before connecting. That function is IPv4-only, so an ftp:// URL pointing at an IPv6-only host fails at the resolve step with a name-resolution error even though the server is reachable over IPv6.

As diagnosed on the tracker (bpo-1675455) back in 2009, ftp_open() has no need to resolve the host itself: it hands the host to connect_ftp(), then to ftpwrapper, and finally to ftplib.FTP.connect(), which connects through socket.create_connection() and resolves via getaddrinfo(). That path is already IPv6-aware. This change removes the pre-resolution so the host flows through unresolved and IPv6-only hosts work.

The one behavior to preserve is the exception type for a bad host. Previously an unresolvable host raised socket.gaierror from gethostbyname() and was turned into URLError. With the pre-resolution gone, the same gaierror is raised deeper, inside ftplib. Because socket.gaierror is a subclass of OSError and therefore a member of ftplib.all_errors, ftp_open()'s existing handler still catches it and re-raises it as URLError, so callers see exactly the same exception type as before.

Tests: test_ftp now asserts the original hostname reaches connect_ftp() rather than a pre-resolved IP; test_ftp_no_hostname_resolution tripwires socket.gethostbyname to prove ftp_open() never calls it; and test_ftp_gaierror asserts the gaierror -> URLError conversion still holds. All three fail on main and pass with this change.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant