Latest Results
Apply RFC 9110 to an empty http host in RFC 3986 mode (#1971)
<!-- Thank you for your contribution! -->
## What do these changes do?
Follows aio-libs/yarl#1970.
RFC 9110 section 4.2.1 makes a recipient reject an http URI with an
empty host, and section 4.2.2 does the same for https. yarl's RFC 3986
mode applied that only to an authority with userinfo or a port
(`http://user@/`), and accepted `http://`, `http:///path` and
`http://?q`. RFC mode now rejects an empty authority for `http`,
`https`, `ws`, `wss` and `ftp`, the schemes `_check_missing_host()`
already treats as needing a host, which is what WHATWG mode does after
#1970.
The RFC 3986 oracle in `tools/conformance/rfc3986_oracle.py` followed
only the generic RFC 3986 syntax, although the report lists RFC 9110
among its standards. It now gives no result for an http or https URI
with an authority and an empty host. A URI without an authority
(`http:/p`) is still left to the generic syntax; requiring an authority
for http as the full RFC 9110 grammar does would put 12 rows into "yarl
differs, RFC 3986+3987 and WHATWG agree" and is not done here.
The guard #1970 added in `_moved_parts()` for an RFC mode `http://`
moved into WHATWG mode is removed: RFC mode can no longer hold such a
URL, other than with `encoded=True`, which is not validated in either
mode.
Conformance report: six rows moved to "RFC 3986+3987, WHATWG and yarl in
both modes agree": `http://user:pass@/`, `http://user@/www.example.com`,
`http://@/www.example.com`, `http://a:b@/www.example.com` (from "RFC
mode differs from RFC 3986+3987, WHATWG mode follows WHATWG"), and
`http://?`, `http://#` (from "yarl follows each standard in its mode").
In six rows with base `http://example.org/` only the oracle's answer
changed, from `http:///…` to a failure: `///example.org/../path`,
`///example.org/../../`, `///example.org/../path/../../`,
`///example.org/../path/../../path` (still "RFC mode differs") and
`///test`, `///example.org/path` (still "Both modes differ"). These have
a separate root cause, `_join_with_empty()` keeping the base host for a
reference with an empty authority, and are left for a follow-up. No row
moved the other way and "yarl differs, RFC 3986+3987 and WHATWG agree"
stays empty.
Existing tests updated because they pinned the old behaviour:
`tests/test_url_special_authority.py::test_rfc_mode_keeps_path[http:///example.com/]`
(now rejected in RFC mode) and
`tests/test_url_empty_components.py::test_equality_follows_str` (the
`http:/x` against `http:///x` RFC case). No strict xfail covered these
cases; `tests/test_rfc_schemes.py` gets an RFC mode leg for its RFC 9110
test.
## Are there changes in behavior for the user?
Yes, in RFC 3986 mode only: `URL("http://", mode="rfc")`,
`URL("http:///path", mode="rfc")`, `URL("http://?q", mode="rfc")` and
the same for `https`, `ws`, `wss` and `ftp` raise `ValueError`.
## Is it a substantial burden for the maintainers to support this?
No. The check from #1970 loses its mode condition, and the oracle gets
one RFC 9110 rule.
## Related issue number
N/A
## Checklist
- [x] I think the code is well written
- [x] Unit tests for the changes exist
- [x] Documentation reflects the changes
- [ ] If you provide code modification, please add yourself to
`CONTRIBUTORS.txt`
- [x] Add a new news fragment into the `CHANGES/` folder
<details>
<summary>Agent run details (optional, for reviewers)</summary>
Tests: `PYTHONPATH=$PWD python -m pytest ./tests ./yarl`: 3462 passed,
55 xfailed. With `YARL_NO_EXTENSIONS=1`: 3079 passed, 7 skipped, 53
xfailed. `tests/test_url_special_empty_host.py` and
`tests/test_rfc_schemes.py` fail 18 cases against master with #1970.
Coverage: `PYTHONPATH=$PWD python -m coverage run --source=yarl,tests -m
pytest ./tests ./yarl -n0 --no-cov`: every changed line and every line
of the edited test modules covered; the 5 remaining misses in
`yarl/_url.py` are outside the diff.
Conformance: `PYTHONPATH=$PWD python tools/conformance/compare.py
--check` passes, including the new oracle self-check cases. Section diff
against master with #1970, case by case: the six rows listed above moved
to "agree", six join rows changed only the RFC 3986+3987 value; nothing
else changed. Against the oracle with the full RFC 9110 grammar
(authority required), 12 rows would land in the forbidden section, which
is why only the empty host rule is applied.
WHATWG mode is unchanged against #1970; the only code change is the mode
condition on the RFC side and the removed `_moved_parts()` guard, which
only RFC mode URLs reached.
Docs: `make doc-spelling` and `make -C docs doctest` (238 tests, 0
failures) pass.
Lint: `pre-commit run` on the changed files passes, including mypy.
Benchmarks: callgrind instruction counts, Python 3.13.2,
`PYTHONHASHSEED=0 YARL_NO_EXTENSIONS=1`, every body in
`tests/test_url_benchmarks.py` (91 bodies), per call as the difference
between 51 and 11 iterations, against `git archive upstream/master yarl`
before #1970 landed (so including #1970). Only `test_human_repr` moved
by more than 1%: -1.5%, from #1970, confirmed with 21 and 61 iterations.
Everything else within +-0.7% on the first run and +-0.3% on the
recheck.
</details>
Drafted with Claude Code (Claude Opus 5.5); reviewed by @asvetlov. Latest Branches
0%
dependabot/pip/requirements/tomli-2.5.0 0%
dependabot/pip/requirements/pydantic-gte-2.14.0 0%
dependabot/pip/requirements/multidict-7.1.0 © 2026 CodSpeed Technology