StephenBrown2: Looks like rfc3986.IRIReference will in fact do exactly what we want, including handling colons in the authority. I've rebased on master to resolve the conflict with hsts... and switched to using `rfc3986.api.iri_reference(url).encode()` which will handle the IDNA Domain and return a URLReference object as expected.

Ref: convenience function - https://github.com/python-hyper/rfc3986/blob/df21c4b09078580c1cb10c728dc18c3111fcf603/src/rfc3986/api.py#L41
Relevant bit of IRIReference.encode() - https://github.com/python-hyper/rfc3986/blob/df21c4b09078580c1cb10c728dc18c3111fcf603/src/rfc3986/iri.py#L97-L129
tomchristie: I guess we *might* also choose to switch our dependency listing to `rfc3986[idna]`, instead of listing the two packages seperately?

That'd stop devs from having to do a "wait where are you actually using the `idna` package?" double take.
tomchristie: Yas - good stuff!

diff --git a/httpx/models.py b/httpx/models.py
index df5d071..32e412f 100644
--- a/httpx/models.py
+++ b/httpx/models.py
@@ -89,16 +89,10 @@ class URL:
         params: QueryParamTypes = None,
     ) -> None:
         if isinstance(url, str):
-            self._uri_reference = rfc3986.api.uri_reference(url)
+            self._uri_reference = rfc3986.api.iri_reference(url).encode()
         else:
             self._uri_reference = url._uri_reference
 
-        # Handle IDNA domain names.
-        if self._uri_reference.authority:
-            idna_authority = self._uri_reference.authority.encode("idna").decode("ascii")
-            if idna_authority != self._uri_reference.authority:
-                self._uri_reference = self._uri_reference.copy_with(authority=idna_authority)
-
         # Normalize scheme and domain name.
         if self.is_absolute_url:
             self._uri_reference = self._uri_reference.normalize()

Use IDNA 2008 instead of IDNA 2003
Using `str.encode("idna")` uses IDNA 2003 which isn't recommended for modern use. We should be using IDNA 2008 provided by the `idna` module (which is a dependency of `httpx` but I don't think we're using it anywhere?)
diff --git a/tests/models/test_url.py b/tests/models/test_url.py
index 7c865f5..a556ed8 100644
--- a/tests/models/test_url.py
+++ b/tests/models/test_url.py
@@ -1,13 +1,65 @@
 import pytest
+import rfc3986
 
 from httpx import URL
 from httpx.exceptions import InvalidURL
 
 
-def test_idna_url():
-    url = URL("http://中国.icom.museum:80/")
-    assert url == URL("http://xn--fiqs8s.icom.museum:80/")
-    assert url.host == "xn--fiqs8s.icom.museum"
+@pytest.mark.parametrize(
+    "given,idna,host,scheme,port",
+    [
+        (
+            "http://中国.icom.museum:80/",
+            "http://xn--fiqs8s.icom.museum:80/",
+            "xn--fiqs8s.icom.museum",
+            "http",
+            80,
+        ),
+        (
+            "http://Königsgäßchen.de",
+            "http://xn--knigsgchen-b4a3dun.de",
+            "xn--knigsgchen-b4a3dun.de",
+            "http",
+            80,
+        ),
+        ("https://faß.de", "https://xn--fa-hia.de", "xn--fa-hia.de", "https", 443),
+        (
+            "https://βόλος.com:443",
+            "https://xn--nxasmm1c.com:443",
+            "xn--nxasmm1c.com",
+            "https",
+            443,
+        ),
+        (
+            "http://ශ්‍රී.com:444",
+            "http://xn--10cl1a0b660p.com:444",
+            "xn--10cl1a0b660p.com",
+            "http",
+            444,
+        ),
+        (
+            "https://نامه‌ای.com:4433",
+            "https://xn--mgba3gch31f060k.com:4433",
+            "xn--mgba3gch31f060k.com",
+            "https",
+            4433,
+        ),
+    ],
+    ids=[
+        "http_with_port",
+        "unicode_tr46_compat",
+        "https_without_port",
+        "https_with_port",
+        "http_with_custom_port",
+        "https_with_custom_port",
+    ],
+)
+def test_idna_url(given, idna, host, scheme, port):
+    url = URL(given)
+    assert url == URL(idna)
+    assert url.host == host
+    assert url.scheme == scheme
+    assert url.port == port
 
 
 def test_url():

pip install -e .
parse_log_pytest
requirements.txt
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==25.3.0
autoflake==2.3.1
babel==2.17.0
backrefs==5.8
black==25.1.0
brotlipy==0.7.0
certifi==2025.1.31
cffi==1.17.1
chardet==3.0.4
charset-normalizer==3.4.1
click==8.1.8
colorama==0.4.6
coverage==7.8.0
cryptography==44.0.2
exceptiongroup==1.2.2
flake8==7.2.0
flake8-bugbear==24.12.12
flake8-comprehensions==3.16.0
ghp-import==2.1.0
h11==0.8.1
h2==3.2.0
hpack==3.0.0
hstspreload==2025.1.1
-e git+https://github.com/encode/httpx.git@66754ad0c58d61d34489294530351aa4b17e217f#egg=httpx
hyperframe==5.2.0
idna==2.10
importlib_metadata==8.6.1
iniconfig==2.1.0
isort==6.0.1
Jinja2==3.1.6
Markdown==3.7
MarkupSafe==3.0.2
mccabe==0.7.0
mergedeep==1.3.4
mkdocs==1.6.1
mkdocs-get-deps==0.2.0
mkdocs-material==9.6.10
mkdocs-material-extensions==1.3.1
mypy==1.15.0
mypy-extensions==1.0.0
packaging==24.2
paginate==0.5.7
pathspec==0.12.1
platformdirs==4.3.7
pluggy==1.5.0
pycodestyle==2.13.0
pycparser==2.22
pyflakes==3.3.2
Pygments==2.19.1
pymdown-extensions==10.14.3
pytest==8.3.5
pytest-asyncio==0.26.0
pytest-cov==6.0.0
python-dateutil==2.9.0.post0
PyYAML==6.0.2
pyyaml_env_tag==0.1
requests==2.32.3
rfc3986==1.5.0
six==1.17.0
tomli==2.2.1
trustme==1.2.1
typing_extensions==4.13.0
urllib3==2.3.0
uvicorn==0.34.0
watchdog==6.0.0
zipp==3.21.0

name: httpx
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==25.3.0
      - autoflake==2.3.1
      - babel==2.17.0
      - backrefs==5.8
      - black==25.1.0
      - brotlipy==0.7.0
      - certifi==2025.1.31
      - cffi==1.17.1
      - chardet==3.0.4
      - charset-normalizer==3.4.1
      - click==8.1.8
      - colorama==0.4.6
      - coverage==7.8.0
      - cryptography==44.0.2
      - exceptiongroup==1.2.2
      - flake8==7.2.0
      - flake8-bugbear==24.12.12
      - flake8-comprehensions==3.16.0
      - ghp-import==2.1.0
      - h11==0.8.1
      - h2==3.2.0
      - hpack==3.0.0
      - hstspreload==2025.1.1
      - hyperframe==5.2.0
      - idna==2.10
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - isort==6.0.1
      - jinja2==3.1.6
      - markdown==3.7
      - markupsafe==3.0.2
      - mccabe==0.7.0
      - mergedeep==1.3.4
      - mkdocs==1.6.1
      - mkdocs-get-deps==0.2.0
      - mkdocs-material==9.6.10
      - mkdocs-material-extensions==1.3.1
      - mypy==1.15.0
      - mypy-extensions==1.0.0
      - packaging==24.2
      - paginate==0.5.7
      - pathspec==0.12.1
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pycodestyle==2.13.0
      - pycparser==2.22
      - pyflakes==3.3.2
      - pygments==2.19.1
      - pymdown-extensions==10.14.3
      - pytest==8.3.5
      - pytest-asyncio==0.26.0
      - pytest-cov==6.0.0
      - python-dateutil==2.9.0.post0
      - pyyaml==6.0.2
      - pyyaml-env-tag==0.1
      - requests==2.32.3
      - rfc3986==1.5.0
      - six==1.17.0
      - tomli==2.2.1
      - trustme==1.2.1
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - uvicorn==0.34.0
      - watchdog==6.0.0
      - zipp==3.21.0
prefix: /opt/conda/envs/httpx

tests/models/test_url.py::test_idna_url[unicode_tr46_compat] tests/models/test_url.py::test_idna_url[https_without_port] tests/models/test_url.py::test_idna_url[https_with_port] tests/models/test_url.py::test_idna_url[http_with_custom_port] tests/models/test_url.py::test_idna_url[https_with_custom_port]
tests/models/test_url.py::test_idna_url[http_with_port] tests/models/test_url.py::test_url tests/models/test_url.py::test_url_eq_str tests/models/test_url.py::test_url_params tests/models/test_url.py::test_url_join tests/models/test_url.py::test_url_join_rfc3986 tests/models/test_url.py::test_url_set tests/models/test_url.py::test_hsts_preload_converted_to_https
httpx/models.py
httpx/models.py