From 9bac1927ea90830623471d376c53e9c191664b18 Mon Sep 17 00:00:00 2001 From: Abdulmumin Yaqeen Date: Fri, 22 May 2026 17:27:04 +0100 Subject: [PATCH 1/3] Fix URL.host decoding for nested IDNA labels --- src/httpx2/httpx2/_urls.py | 7 +++++-- tests/httpx2/models/test_url.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/httpx2/httpx2/_urls.py b/src/httpx2/httpx2/_urls.py index 057ca150..d2e2b48a 100644 --- a/src/httpx2/httpx2/_urls.py +++ b/src/httpx2/httpx2/_urls.py @@ -184,8 +184,11 @@ def host(self) -> str: """ host: str = self._uri_reference.host - if host.startswith("xn--"): - host = idna.decode(host) + if any(label.startswith("xn--") for label in host.split(".")): + try: + host = idna.decode(host) + except idna.IDNAError: + pass return host diff --git a/tests/httpx2/models/test_url.py b/tests/httpx2/models/test_url.py index 856c6523..7f216ecc 100644 --- a/tests/httpx2/models/test_url.py +++ b/tests/httpx2/models/test_url.py @@ -754,6 +754,14 @@ def test_url_merge_params_manipulation(): "https", 4433, ), + ( + "https://www.égalité-femmes-hommes.gouv.fr", + "https://www.xn--galit-femmes-hommes-9ybf.gouv.fr", + "www.égalité-femmes-hommes.gouv.fr", + b"www.xn--galit-femmes-hommes-9ybf.gouv.fr", + "https", + None, + ), ], ids=[ "http_with_port", @@ -762,6 +770,7 @@ def test_url_merge_params_manipulation(): "https_with_port", "http_with_custom_port", "https_with_custom_port", + "idna_label_after_ascii_label", ], ) def test_idna_url(given, idna, host, raw_host, scheme, port): @@ -783,6 +792,11 @@ def test_url_escaped_idna_host(): assert url.raw_host == b"xn--fiqs8s.icom.museum" +def test_url_malformed_idna_label_after_ascii_label(): + url = httpx2.URL("https://a.b.c.xn--pokxncvks/") + assert url.host == "a.b.c.xn--pokxncvks" + + def test_url_invalid_idna_host(): with pytest.raises(httpx2.InvalidURL) as exc: httpx2.URL("https://☃.com/") From 908ed28c1f5ef09ff5333cb60876f12770ceeb89 Mon Sep 17 00:00:00 2001 From: Abdulmumin Yaqeen Date: Sat, 23 May 2026 21:15:18 +0100 Subject: [PATCH 2/3] Decode IDNA host labels individually --- src/httpx2/httpx2/_urls.py | 15 ++++++++++----- tests/httpx2/models/test_url.py | 5 +++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/httpx2/httpx2/_urls.py b/src/httpx2/httpx2/_urls.py index d2e2b48a..8c86ca0f 100644 --- a/src/httpx2/httpx2/_urls.py +++ b/src/httpx2/httpx2/_urls.py @@ -184,11 +184,16 @@ def host(self) -> str: """ host: str = self._uri_reference.host - if any(label.startswith("xn--") for label in host.split(".")): - try: - host = idna.decode(host) - except idna.IDNAError: - pass + if "xn--" in host: + labels = [] + for label in host.split("."): + if label.startswith("xn--"): + try: + label = idna.decode(label) + except idna.IDNAError: + pass + labels.append(label) + host = ".".join(labels) return host diff --git a/tests/httpx2/models/test_url.py b/tests/httpx2/models/test_url.py index 7f216ecc..22b1dacb 100644 --- a/tests/httpx2/models/test_url.py +++ b/tests/httpx2/models/test_url.py @@ -797,6 +797,11 @@ def test_url_malformed_idna_label_after_ascii_label(): assert url.host == "a.b.c.xn--pokxncvks" +def test_url_mixed_valid_and_malformed_idna_labels(): + url = httpx2.URL("https://xn--fiqs8s.xn--pokxncvks/") + assert url.host == "中国.xn--pokxncvks" + + def test_url_invalid_idna_host(): with pytest.raises(httpx2.InvalidURL) as exc: httpx2.URL("https://☃.com/") From a5a6834345444d7f7bc0f45af9cd72f8fbe58e53 Mon Sep 17 00:00:00 2001 From: Abdulmumin Yaqeen Date: Tue, 26 May 2026 05:45:02 +0100 Subject: [PATCH 3/3] Format URL IDNA tests --- tests/httpx2/models/test_url.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/httpx2/models/test_url.py b/tests/httpx2/models/test_url.py index e6df7d53..5c7425dd 100644 --- a/tests/httpx2/models/test_url.py +++ b/tests/httpx2/models/test_url.py @@ -803,6 +803,7 @@ def test_url_mixed_valid_and_malformed_idna_labels() -> None: url = httpx2.URL("https://xn--fiqs8s.xn--pokxncvks/") assert url.host == "中国.xn--pokxncvks" + def test_url_invalid_idna_host() -> None: with pytest.raises(httpx2.InvalidURL) as exc: httpx2.URL("https://☃.com/")