diff --git a/src/requests/models.py b/src/requests/models.py index e158b6cca2..08dcdb4455 100644 --- a/src/requests/models.py +++ b/src/requests/models.py @@ -480,6 +480,16 @@ def prepare_url(self, url, params): query = enc_params url = requote_uri(urlunparse([scheme, netloc, path, None, query, fragment])) + + # Preserve the IPv6 zone identifier separator ('%') inside a bracketed + # host. ``requote_uri`` percent-encodes a literal '%' to '%25', but + # ``urllib3`` forwards the (already-parsed) host to ``getaddrinfo()`` + # verbatim, so an encoded zone identifier like ``%25eth0`` cannot be + # resolved by the OS. See https://github.com/psf/requests/issues/6735. + if host and "%" in host: + encoded_host = host.replace("%", "%25", 1) + url = url.replace(encoded_host, host, 1) + self.url = url def prepare_headers(self, headers): diff --git a/tests/test_requests.py b/tests/test_requests.py index 6d1bef66e0..a0ad23b464 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -147,6 +147,39 @@ def test_path_is_not_double_encoded(self): assert request.path_url == "/get/test%20case" + @pytest.mark.parametrize( + "url, expected", + ( + ( + # Literal '%' zone-id separator must be preserved, otherwise + # urllib3 will forward '%25ens192' to getaddrinfo() which the + # OS cannot resolve. Regression test for #6735. + "https://[fe80::1%ens192]/redfish/v1", + "https://[fe80::1%ens192]/redfish/v1", + ), + ( + # RFC 6874 form ('%25') must be normalized back to the literal + # form expected by getaddrinfo() / urllib3. + "https://[fe80::1%25ens192]/redfish/v1", + "https://[fe80::1%ens192]/redfish/v1", + ), + ( + # Zone-id preserved alongside port/query/fragment. + "https://[fe80::5eed:8cff:fe00:0da4%ens192]:8443/api?x=1#f", + "https://[fe80::5eed:8cff:fe00:0da4%ens192]:8443/api?x=1#f", + ), + ( + # Plain IPv6 (no zone id) must be unaffected. + "https://[::1]/", + "https://[::1]/", + ), + ), + ) + def test_ipv6_zone_id_is_preserved(self, url, expected): + """See: https://github.com/psf/requests/issues/6735""" + request = requests.Request("GET", url).prepare() + assert request.url == expected + @pytest.mark.parametrize( "url, expected", (