diff --git a/httpx/_models.py b/httpx/_models.py index 7bad1c9eb9..c981c740bf 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -108,6 +108,11 @@ def __init__( raw_scheme, raw_host, port, raw_path = url scheme = raw_scheme.decode("ascii") host = raw_host.decode("ascii") + if host and ":" in host and host[0] != "[": + # it's an IPv6 address, so it should be enclosed in "[" and "]" + # ref: https://tools.ietf.org/html/rfc2732#section-2 + # ref: https://tools.ietf.org/html/rfc3986#section-3.2.2 + host = f"[{host}]" port_str = "" if port is None else f":{port}" path = raw_path.decode("ascii") url = f"{scheme}://{host}{port_str}{path}" @@ -186,8 +191,17 @@ def host(self) -> str: url = httpx.URL("http://中国.icom.museum") assert url.host == "xn--fiqs8s.icom.museum" + + url = httpx.URL("https://[::ffff:192.168.0.1]") + assert url.host == "::ffff:192.168.0.1" """ - return self._uri_reference.host or "" + host: str = self._uri_reference.host + + if host and ":" in host and host[0] == "[": + # it's an IPv6 address + host = host.lstrip("[").rstrip("]") + + return host or "" @property def port(self) -> typing.Optional[int]: @@ -336,6 +350,11 @@ def copy_with(self, **kwargs: typing.Any) -> "URL": # Consolidate host and port into netloc. host = kwargs.pop("host", self.host) or "" port = kwargs.pop("port", self.port) + + if host and ":" in host and host[0] != "[": + # it's an IPv6 address, so it should be hidden under bracket + host = f"[{host}]" + kwargs["netloc"] = f"{host}:{port}" if port is not None else host if "userinfo" in kwargs or "netloc" in kwargs: diff --git a/tests/models/test_url.py b/tests/models/test_url.py index fcd81ba50c..9d67618b5b 100644 --- a/tests/models/test_url.py +++ b/tests/models/test_url.py @@ -287,3 +287,37 @@ def test_url_with_url_encoded_path(): assert url.path == "/path to somewhere" assert url.query == b"" assert url.raw_path == b"/path%20to%20somewhere" + + +def test_ipv6_url(): + url = httpx.URL("http://[::ffff:192.168.0.1]:5678/") + + assert url.host == "::ffff:192.168.0.1" + assert url.netloc == "[::ffff:192.168.0.1]:5678" + + +@pytest.mark.parametrize( + "url_str", + [ + "http://127.0.0.1:1234", + "http://example.com:1234", + "http://[::ffff:127.0.0.1]:1234", + ], +) +@pytest.mark.parametrize("new_host", ["[::ffff:192.168.0.1]", "::ffff:192.168.0.1"]) +def test_ipv6_url_copy_with_host(url_str, new_host): + url = httpx.URL(url_str).copy_with(host=new_host) + + assert url.host == "::ffff:192.168.0.1" + assert url.netloc == "[::ffff:192.168.0.1]:1234" + assert str(url) == "http://[::ffff:192.168.0.1]:1234" + + +@pytest.mark.parametrize("host", [b"[::ffff:192.168.0.1]", b"::ffff:192.168.0.1"]) +def test_ipv6_url_from_raw_url(host): + raw_url = (b"https", host, 443, b"/") + url = httpx.URL(raw_url) + + assert url.host == "::ffff:192.168.0.1" + assert url.netloc == "[::ffff:192.168.0.1]:443" + assert str(url) == "https://[::ffff:192.168.0.1]:443/"