Skip to content

[WTF] URLParser: parseIPv6Host follows the IPv6 parser of the URL Standard - #909

Open
robobun wants to merge 2 commits into
mainfrom
robobun/db9d283d/url-ipv6-trailing-colon
Open

robobun wants to merge 2 commits into
mainfrom
robobun/db9d283d/url-ipv6-trailing-colon

Conversation

@robobun

@robobun robobun commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • new URL("http://[::1:]/") gives http://[::1]/, and fetch("http://[::1:]:3000/") connects to [::1]. Node.js, Chromium and rust-url refuse the URL.
  • new URL("http://[2001:db8:1234::5efe:127.0.0.1]/") throws. Node.js parses it (WTF::URLParser should accept compressed IPv4-in-IPv6 addresses #648).
  • Both are in URLParser::parseIPv6Host (Source/WTF/wtf/URLParser.cpp:3293, :3332). Upstream added the first on purpose (r247267, bug 199627), misreading the URL Standard.

Fix

  • Commit 1 is WTF::URLParser should accept compressed IPv4-in-IPv6 addresses #648's commit by @steipete, unchanged: a compressed address takes an IPv4 tail at piece 5.
  • Commit 2: an address that ends right after a piece and a : fails. A final :: still parses.
  • Verified: WTF_URLParser and WTF_URL (30 tests), linux-amd64-debug-asan lane flags. Each changed test fails without its commit. 334,619 bracketed hosts: 488 differ from Node.js v26.3.0 before, 0 after.
  • Self-reviewed: 22 concerns raised, 18 addressed. Not taken (Notes): an upstream bug, WPT rows, .failing Bun tests now.

Background

  • A piece is up to four hex digits. :: (a compression) is a run of zero pieces.
  • syntaxViolation marks valid input that needs normalizing. std::nullopt makes the URL invalid.
  • No user reported this. A differential test against Node.js found it. zod's z.ipv6() uses this parser.
  • No other layer fits: the constructor, URL::setHost and URLPattern all end here. These are the first intended result changes since Make WTF::URLParser faster than Ada #452.

Downsides

  • An IPv6 host that ends with a single : no longer parses. new URL throws, fetch rejects and a setter keeps the old host, as in Node.js.
  • Other input pays nothing. The release URLParser.cpp.o shrinks by 99 bytes. Valid IPv6 URLs run 26 fewer to 5 more instructions (Notes).
Notes

The Bun side. oven-sh/bun#47236 (draft) pins the preview build of this branch and adds the tests. bun bd test on its three test files: 5 failures on Bun main's pin fcc1ea09, 574 passes with the preview build (debug ASAN). test/js/web/url, test/js/web/urlpattern and test/js/node/url give 765 passes and no failure, and the 42 test-url* and test-whatwg-url* files of the Node.js suite keep their exit codes. This branch is on fcc1ea09, which Bun main pinned when that pull request was opened, so the preview differs from that pin by these two commits only. Bun main's pin has moved since. Both branches are rebased when this pull request gets a review or merges.

Repro.

for (const s of ["http://[::1:]/", "http://[1::2:]:81/", "http://[1:2:3:4:5:6:7:]/", "http://[1::]/", "http://[2001:db8:1234::5efe:127.0.0.1]/"]) {
  let r; try { r = new URL(s).href; } catch { r = "throws"; }
  console.log(JSON.stringify(s), "->", r);
}
input before after, and Node.js v26.3.0
http://[::1:]/ http://[::1]/ throws
http://[1::2:]:81/ http://[1::2]:81/ throws
http://[1:2:3:4:5:6:7:]/ throws throws
http://[1::]/ http://[1::]/ http://[1::]/
http://[2001:db8:1234::5efe:127.0.0.1]/ throws http://[2001:db8:1234::5efe:7f00:1]/

A trailing colon was accepted only with a :: in the address. Without one, the later piecePointer != 8 check rejects the address.

Reach. Every URL entry point of Bun ends in this function: new URL, URL.parse, URL.canParse, the href, host and hostname setters, Request, WebSocket, fetch, URLPattern inputs, url.domainToASCII and url.domainToUnicode. Bun 1.4.3: fetch("http://[::1:]:PORT/x") gets a 200 from a server on ::1, with Host: [::1]:PORT, while net.isIPv6("::1:") is false. zod v4 checks z.ipv6() and z.cidrv6() with URL.canParse("http://[" + value + "]") (packages/zod/src/v4/core/schemas.ts, isValidIPv6), so on Bun it accepts ::1: and on Node.js it does not. No issue in oven-sh/bun or here reports either bug.

The upstream history of commit 2. r247267 (57936a0555eb, 2019, bug 199627, "URLParser should reserialize IPv6 addresses that end with a colon") added the syntaxViolation call and the WTF_URLParser.Basic row that expects a://[::2:] to parse as a://[::2]. Its reason: "the colon should be removed [...] This matches the URL specification, and I added a unit test that used to assert." Earlier, r207162 (128a4c7e0e, 2016, bug 162660) changed the line of LayoutTests/fast/url/ipv6-expected.txt for http://[0:0::0:0:8:]/ from PASS to FAIL, with the note that the address "should indeed be compressed".

The URL Standard has said otherwise at each of those dates. After a piece and a : its IPv6 parser increases the pointer, then: "If c is the EOF code point, syntax violation, return failure" (whatwg/url 85359fc, 2016-09-02), "validation error, return failure" (8809598, 2019-04-04), "IPv6-invalid-code-point validation error, return failure" (845bafc, today). Its table of validation errors gives https://[1:2:3:] as an example. Chromium url/url_canon_ip.h (DoParseIPv6) allows an empty component at the end only directly after the contraction. rust-url url/src/host.rs (parse_ipv6addr) returns InvalidIpv6Address when the input ends after the :.

The address now fails before anything is serialized, so the assertion that r247267 fixed has nothing to check. The new rows pass in a build with assertions.

Commit 1 is #648. The cherry-pick is clean and keeps the author. #648 is a pull request from a fork, so this repository's CI never built it. If you prefer to land #648 by itself, drop commit 1: commit 2 does not depend on it. Only one comment and the row in JSTests/BUN-TEST-DIFFERENCES.md name its test rows. A merge commit keeps the two commits apart. A squash keeps the author of commit 1 as a co-author.

Tests. WTF_URLParser.Basic now expects a://[::2:] to fail (changed in place, with a // Bun: comment). ParserFailures has three more rows and ParserDifferences has the four rows of #648. LayoutTests/fast/url/ipv6-expected.txt has PASS again for http://[0:0::0:0:8:]/, which ipv6.html lists under "Don't allow leading or trailing colons". That layout test is not part of this repository's CI and was not run.

The prebuilt lane ships its compile_commands.json. URLParser.cpp, Tests/WTF/URLParser.cpp, Tests/WTF/URL.cpp and gtest were compiled with the lane's own commands and linked to the libWTF.a of autobuild-680ef47c:

parser result with the test rows of this branch
stock libWTF.a Basic, ParserDifferences and ParserFailures fail
commit 1 only Basic and ParserFailures fail
commit 2 only ParserDifferences fails
this branch 30 pass

How much differs from Node.js. 334,619 strings between http://[ and ]/: every string of length 0 to 8 over 0 1 : ., 300,000 random token strings, and a structured set. Each one goes through Bun and Node.js v26.3.0. Stock: 488 differences (478 trailing colons, 10 IPv4 tails). Commit 2 only: 10. This branch: 0, also with the preview build of this pull request.

Cost. URLParser.cpp compiled with the flags of the linux-amd64 release lane (-O3). The text of the object goes from 206,518 to 206,419 bytes. Instructions are single-step counts in gdb from the entry of URL::URL(String&&) to its return, equal on each of three calls.

input before after
http://[::1]/ 873 863
http://[2001:db8::1]:8080/ 1,652 1,639
http://[1:2:3:4:5:6:7:8]/ 1,622 1,596
http://[fe80::1ff:fe23:4567:890a]/path 2,220 2,202
http://[::ffff:192.168.0.1]/ 2,264 2,243
http://[1::]/ 838 843
http://example.com/ 348 348

Since #452. #452 rewrote this function with output identical to the old parser. These two commits change results on purpose. A differential against the old parser now differs for a compressed address that ends with a piece and a : (parsed before, fails now) and for a compressed address with an IPv4 tail at piece 5 (failed before, parses now).

Upstream. WebKit main (762eb8cd71d2) has both lines, the a://[::2:] row and the FAIL line. WPT has no row for either case: its only literal that ends with a piece and a : is https://[0:0:], which has too few pieces anyway. A bugs.webkit.org bug that follows up 199627, and WPT rows such as https://[::1:] and https://[1::2:] (failure) and https://[::1:2:3:4:1.2.3.4] ([::1:2:3:4:102:304]), need accounts that I do not have. Commit 2 applies upstream with only the iterator spelling changed.

Self-review, not taken. Three concerns ask for an upstream WebKit bug, an upstream pull request and a WPT pull request before or beside this one. They need accounts that I do not have. The Upstream note has what they would contain. One concern asks to land the Bun tests now, marked .failing. The pin bump that brings the fix then goes red for whoever makes it. The tests land with the pin instead.

Related. #724 (with oven-sh/bun#43883) changes URL::setHost and does not touch this function. The Bun tests of oven-sh/bun#47236 sit at other places in the same test files.

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Preview build of c061b10: autobuild-preview-pr-909-c061b103

steipete and others added 2 commits October 28, 2026 18:27
… parse

parseIPv6Host records a syntax violation when the address ends right after a
piece and a ':'. With a "::" compression earlier in the address it then
returns the address, and the serializer drops the colon:

    http://[::1:]/       ->  http://[::1]/
    http://[1::2:]:81/   ->  http://[1::2]:81/

Without a compression the piece count check rejects the address, so only the
compressed form parses.

This reverses an upstream decision. r247267 (57936a0,
https://bugs.webkit.org/show_bug.cgi?id=199627, "URLParser should reserialize
IPv6 addresses that end with a colon") added the syntaxViolation() call and
the WTF_URLParser.Basic row that expects "a://[::2:]" to parse as "a://[::2]".
Its reason: "When an IPv6 address ends in a colon, the colon should be removed
[...] This matches the URL specification, and I added a unit test that used to
assert." Earlier, r207162 (128a4c7, bug 162660) changed the line of
fast/url/ipv6-expected.txt for that address from PASS to FAIL, with the note:
'"http://[0:0::0:0:8:]/" should indeed be compressed to "http://[::8]/"'.

The URL Standard does not say that. Its IPv6 parser, after a piece and a ':',
increases the pointer, and "If c is the EOF code point, [validation error],
return failure". The text had that step when both commits were made
(whatwg/url 85359fc of 2016-09-02 and 8809598 of 2019-04-04), and it has it
now (845bafc). Chromium, rust-url and Node.js fail on such an address.

Return std::nullopt at that step. The address fails before anything is
serialized, so the assertion that r247267 fixed has nothing to check, and the
rows below pass in a build with assertions. URL::setHost and
URL::setHostAndPort parse the new URL string with the same function, so they
refuse the value. A "::" at the end of the address is a compression and still
parses.

Tests: the "a://[::2:]" row of WTF_URLParser.Basic expects failure, and
ParserFailures has three more rows. fast/url/ipv6-expected.txt has PASS again
for "http://[0:0::0:0:8:]/", which fast/url/ipv6.html lists under "Don't allow
leading or trailing colons". JSTests/BUN-TEST-DIFFERENCES.md lists the
URLParser.cpp rows of this commit and of the previous one.
@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 4656516b-8521-4c09-8591-52993fe153dd
Commits

Reviewing files that changed from the base of the PR and between f495db1 and c061b10.

Files selected for processing: 4
  • JSTests/BUN-TEST-DIFFERENCES.md
  • LayoutTests/fast/url/ipv6-expected.txt
  • Source/WTF/wtf/URLParser.cpp
  • Tools/TestWebKitAPI/Tests/WTF/URLParser.cpp

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.

Walkthrough

The IPv6 URL parser now rejects addresses that end in a single colon and handles specified compressed addresses with IPv4 tails. Parser tests, a layout-test expected result, and test-difference documentation reflect these parsing rules.

Changes

IPv6 URL parsing

Layer / File(s) Summary
IPv6 parsing rules and regression coverage
Tools/TestWebKitAPI/Tests/WTF/URLParser.cpp, Source/WTF/wtf/URLParser.cpp, LayoutTests/fast/url/ipv6-expected.txt, JSTests/BUN-TEST-DIFFERENCES.md
The parser rejects IPv6 addresses that end in a single colon and processes compressed addresses with IPv4 tails. Tests cover the updated parsing cases, and the layout-test expectation and test-difference documentation are updated.
Merge Risk: Minimal · up to c061b

The IPv6 parsing updates and corresponding expectations are consistent and ready to merge.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: updating IPv6 host parsing to follow the URL Standard.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description is detailed and directly explains the problem, fix, affected behavior, testing, performance impact, and related references. It does not follow the exact commit-message template because…
Full details: Description check

Explanation

The description is detailed and directly explains the problem, fix, affected behavior, testing, performance impact, and related references. It does not follow the exact commit-message template because it lacks a dedicated Bugzilla link for this change, a Reviewed by line, and an explicit changed-file/function list. These omissions do not prevent review because the required technical context is present.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. Because it intentionally changes which bracketed IPv6 hosts parse (reaching new URL, fetch, Request, WebSocket and URLPattern) and the paths are under WTF code ownership, a human look would still be worthwhile.

What was reviewed:

  • Traced parseIPv6Host with the piece-5 guard removed: [2001:db8:1234::5efe:127.0.0.1] writes address[5]/address[6] (no out-of-bounds), and the compression swap yields 2001:db8:1234::5efe:7f00:1 as the new ParserDifferences rows expect; [1:2:3:4:5::5efe:127.0.0.1] still fails via the hex path.
  • Checked the trailing-colon change against [1::], [::] and [::1:]: a final :: still parses, only a piece followed by a lone : at end returns std::nullopt, matching the URL Standard's "c is EOF, return failure" step.
  • The BUN-TEST-DIFFERENCES.md row, the ipv6-expected.txt flip to PASS, and the modified Basic/ParserFailures rows are consistent with the new behavior.
Extended reasoning...

The change touches Source/WTF/wtf/URLParser.cpp (three lines in parseIPv6Host: removes the compressed-address IPv4-tail-at-piece-5 rejection and makes a trailing single ':' fail instead of being normalized away), the matching TestWebKitAPI rows, a LayoutTests expectation, and a documentation row in JSTests/BUN-TEST-DIFFERENCES.md. It alters host parsing for every URL entry point, which is a security-adjacent surface (host-based allowlists, fetch destinations), though both changes move toward the URL Standard and Node.js and neither introduces memory-unsafety. No findings were reported and no third-party objections appear in the timeline, but the paths are covered by the repository's CODEOWNERS (/Source/WTF, /Tools/TestWebKitAPI/Tests/WTF, /JSTests) and the change is a deliberate behavioral divergence from upstream WebKit, so a human should weigh the intent rather than this being auto-approved.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants