Skip to content

Lowercase ASCII labels in the stdlib IDNA host fallback#1780

Draft
gaoflow wants to merge 1 commit into
aio-libs:masterfrom
gaoflow:fix-idna-fallback-ascii-case
Draft

Lowercase ASCII labels in the stdlib IDNA host fallback#1780
gaoflow wants to merge 1 commit into
aio-libs:masterfrom
gaoflow:fix-idna-fallback-ascii-case

Conversation

@gaoflow

@gaoflow gaoflow commented Jul 1, 2026

Copy link
Copy Markdown

What do these changes do?

_idna_encode() falls back to the stdlib "idna" codec when the strict idna package rejects a host (for example a host with an underscore DNS label such as _dmarc). The stdlib codec leaves already-ASCII labels untouched, so those labels kept whatever case they were given while the non-ASCII labels were still normalised. The host is now lowercased before the fallback encode, matching both the ASCII fast path and the idna.encode(..., uts46=True) result used on the success path.

Are there changes in behavior for the user?

Yes. A host that mixes a non-ASCII label with an ASCII label the strict idna package rejects now has its ASCII labels lowercased, so raw_host is canonical and the URL round-trips. For example URL("http://_DMARC.café/").raw_host is now _dmarc.xn--caf-dma instead of _DMARC.xn--caf-dma; the old value changed again on a second parse, so the URL was not idempotent.

Is it a substantial burden for the maintainers to support this?

No. The change is a single .lower() on the fallback path, plus a regression test.

Related issue number

No linked issue; found while checking host normalisation idempotence.

Checklist

  • I think the code is well written
  • Unit tests for the changes exist
  • Documentation reflects the changes
  • If you provide code modification, please add yourself to CONTRIBUTORS.txt (N/A: no such file in the repo)
  • Add a new news fragment into the CHANGES/ folder
Agent run details (optional, for reviewers)

Repro (on master):

>>> from yarl import URL
>>> URL("http://_DMARC.café/").raw_host
'_DMARC.xn--caf-dma'
>>> URL(str(URL("http://_DMARC.café/"))).raw_host   # not idempotent
'_dmarc.xn--caf-dma'

Tests: the new test_idna_fallback_lowercases_ascii_labels fails on master and passes with the fix. Full suite: 1448 passed, 102 skipped, 4 xfailed.

Lint: ruff check yarl/_url.py tests/test_url.py clean.

Spelling: the CHANGES/ fragment words were checked with enchant against en_US plus docs/spelling_wordlist.txt; a full make doc-spelling docs build was not run in this environment.

Drafted with Claude Code (Claude Opus 4.8); pending review by @gaoflow before this is taken out of draft.

The stdlib IDNA-2003 codec leaves already-ASCII labels untouched, so a
host that mixes a non-ASCII label with an ASCII label the strict idna
package rejects kept the ASCII label in its original case. That made
raw_host and the rendered URL differ when the URL was parsed a second
time. Lowercasing the host before the fallback encode matches the
ASCII fast path and the idna.encode(uts46=True) result.
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided There is a change note present in this PR label Jul 1, 2026
@codspeed-hq

codspeed-hq Bot commented Jul 1, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 97 untouched benchmarks


Comparing gaoflow:fix-idna-fallback-ascii-case (369c536) with master (94274c2)

Open in CodSpeed

@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.90%. Comparing base (94274c2) to head (369c536).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1780      +/-   ##
==========================================
+ Coverage   99.86%   99.90%   +0.04%     
==========================================
  Files          21       21              
  Lines        4310     4315       +5     
  Branches      252      252              
==========================================
+ Hits         4304     4311       +7     
+ Misses          4        2       -2     
  Partials        2        2              
Flag Coverage Δ
CI-GHA 99.83% <100.00%> (+0.04%) ⬆️
OS-Linux 99.83% <100.00%> (+0.04%) ⬆️
OS-Windows 98.58% <100.00%> (+0.04%) ⬆️
OS-macOS 98.77% <100.00%> (+0.04%) ⬆️
Py-3.10 99.74% <100.00%> (+0.04%) ⬆️
Py-3.11 99.74% <100.00%> (+0.04%) ⬆️
Py-3.12 99.74% <100.00%> (+0.04%) ⬆️
Py-3.13 99.79% <100.00%> (+0.04%) ⬆️
Py-3.14 99.78% <100.00%> (+0.04%) ⬆️
Py-3.14t 99.78% <100.00%> (+0.04%) ⬆️
Py-pypy-3.11 99.42% <100.00%> (+0.04%) ⬆️
VM-macos-latest 98.77% <100.00%> (+0.04%) ⬆️
VM-ubuntu-latest 99.83% <100.00%> (+0.04%) ⬆️
VM-windows-latest 98.58% <100.00%> (+0.04%) ⬆️
pytest 99.83% <100.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided There is a change note present in this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant