From 54935d4c9c96302060f64df109d24e058bc5632a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:10:55 +0000 Subject: [PATCH 1/8] url: reject special-scheme hosts made only of IDNA-ignored code points A host consisting solely of IDNA-ignored code points (U+180E, U+206A..U+206F) maps to the empty string under the Unicode 16 IDNA delta. Splicing that empty host back into the input before parsing let the special-authority-ignore-slashes state take the first path segment as the host: new URL("http://\u180E/evil.example/x") parsed with host "evil.example" where node and previous bun throw ERR_INVALID_URL. An empty domain-to-ASCII result is parse failure, so leave the input untouched and let the parser reject the original code points. The host/hostname setters had the same conflation for file: URLs: assigning a host that maps to empty cleared the host instead of no-opping like a failed host parse (a literal empty string still clears it, matching node). --- src/jsc/bindings/DOMURL.cpp | 7 +++++ src/jsc/bindings/URLDecomposition.cpp | 7 +++++ test/js/web/url/url.test.ts | 39 +++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/src/jsc/bindings/DOMURL.cpp b/src/jsc/bindings/DOMURL.cpp index 64718d73cf3f..69bb80558f9a 100644 --- a/src/jsc/bindings/DOMURL.cpp +++ b/src/jsc/bindings/DOMURL.cpp @@ -150,6 +150,13 @@ static String applyIDNADeltaToURLAuthority(const String& urlString, StringView s return {}; auto mappedHost = Bun::applyUnicode16IDNADelta(hostView.toString()); + // A host of only ignored-class code points maps to the empty string, which is + // domain-to-ASCII failure (node throws ERR_INVALID_URL). Splicing an empty host + // back in would instead let the special-authority-ignore-slashes state promote + // the first path segment to the host (http://\u180E/a -> http:///a -> host "a"). + // Leave the input untouched so the parser rejects the original code points. + if (mappedHost.isEmpty()) + return {}; StringBuilder builder; builder.append(view.left(hostStart)); builder.append(mappedHost); diff --git a/src/jsc/bindings/URLDecomposition.cpp b/src/jsc/bindings/URLDecomposition.cpp index a0806ad44acf..d99c61864156 100644 --- a/src/jsc/bindings/URLDecomposition.cpp +++ b/src/jsc/bindings/URLDecomposition.cpp @@ -125,6 +125,10 @@ void URLDecomposition::setHost(StringView value) auto hostSpan = hostEnd == notFound ? value : value.left(hostEnd); if (Bun::containsUnicode16IDNADeltaSource(hostSpan)) { auto mappedHost = Bun::applyUnicode16IDNADelta(hostSpan.toString()); + // A non-empty host span mapping to empty is domain-to-ASCII failure + // (setter no-ops, even for file: where a literal "" is assignable). + if (mappedHost.isEmpty()) + return; mappedValue = hostEnd == notFound ? mappedHost : makeString(mappedHost, value.substring(hostEnd)); value = mappedValue; } @@ -175,6 +179,9 @@ void URLDecomposition::setHostname(StringView host) String mappedHost; if (fullURL.hasSpecialScheme() && !host.startsWith('[') && Bun::containsUnicode16IDNADeltaSource(host)) { mappedHost = Bun::applyUnicode16IDNADelta(host.toString()); + // See setHost: mapping a non-empty hostname to empty is failure, not "". + if (mappedHost.isEmpty()) + return; host = mappedHost; } if (host.isEmpty() && !fullURL.protocolIsFile() && fullURL.hasSpecialScheme()) diff --git a/test/js/web/url/url.test.ts b/test/js/web/url/url.test.ts index a148dd49d299..087427eb411c 100755 --- a/test/js/web/url/url.test.ts +++ b/test/js/web/url/url.test.ts @@ -161,6 +161,45 @@ describe("url", () => { expect(h2.host).toBe("xn--foo-7ka:81"); }); + it("rejects special-scheme hosts made only of IDNA-ignored code points", () => { + // U+180E and U+206A..U+206F map to nothing under UTS #46, so these hosts + // map to the empty string: domain-to-ASCII failure (node throws + // ERR_INVALID_URL), never "promote the first path segment to the host". + const inputs = [ + "http://\u180E/evil.example/x", + "https://\u206A\u206F/other.example/p", + "ws://\u180E/h/p", + "file://\u180E/some/dir/f", + "http://\u180E\u206B:8080/x", + "http://user@\u180E/x", + ]; + for (const input of inputs) { + expect(() => new URL(input)).toThrow(); + expect(URL.canParse(input)).toBe(false); + expect(URL.parse(input)).toBeNull(); + const u = new URL("http://ok.example/"); + expect(() => (u.href = input)).toThrow(); + } + // Scheme-relative input and an all-ignored base reach the same host span. + expect(() => new URL("//\u180E/evil.example/", "http://good.example/")).toThrow(); + expect(URL.canParse("//\u180E/evil.example/", "http://good.example/")).toBe(false); + expect(() => new URL("/x", "http://\u206A/base.example/")).toThrow(); + // Mixed hosts still strip the ignored code point rather than failing. + expect(new URL("http://a\u180Eb/").href).toBe("http://ab/"); + expect(new URL("file://a\u180Eb/x").host).toBe("ab"); + // Setters: a non-empty host that maps to empty is a failed host parse and + // no-ops, even for file: where assigning a literal "" clears the host. + const f1 = new URL("file://server/share"); + f1.host = "\u180E"; + expect(f1.href).toBe("file://server/share"); + const f2 = new URL("file://server/share"); + f2.hostname = "\u180E"; + expect(f2.href).toBe("file://server/share"); + const f3 = new URL("file://server/share"); + f3.host = ""; + expect(f3.href).toBe("file:///share"); + }); + it("prints", () => { // URL.prototype carries [Symbol.for("nodejs.util.inspect.custom")], so // Bun.inspect matches node's util.inspect output. From 815431425c9b8274957dc50ff75ab7d61f70e37e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:20:57 +0000 Subject: [PATCH 2/8] test: assert ERR_INVALID_URL code on all-ignored-host rejections --- test/js/web/url/url.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/js/web/url/url.test.ts b/test/js/web/url/url.test.ts index 087427eb411c..f7e36736841c 100755 --- a/test/js/web/url/url.test.ts +++ b/test/js/web/url/url.test.ts @@ -173,17 +173,18 @@ describe("url", () => { "http://\u180E\u206B:8080/x", "http://user@\u180E/x", ]; + const errInvalidURL = expect.objectContaining({ code: "ERR_INVALID_URL" }); for (const input of inputs) { - expect(() => new URL(input)).toThrow(); + expect(() => new URL(input)).toThrow(errInvalidURL); expect(URL.canParse(input)).toBe(false); expect(URL.parse(input)).toBeNull(); const u = new URL("http://ok.example/"); - expect(() => (u.href = input)).toThrow(); + expect(() => (u.href = input)).toThrow(errInvalidURL); } // Scheme-relative input and an all-ignored base reach the same host span. - expect(() => new URL("//\u180E/evil.example/", "http://good.example/")).toThrow(); + expect(() => new URL("//\u180E/evil.example/", "http://good.example/")).toThrow(errInvalidURL); expect(URL.canParse("//\u180E/evil.example/", "http://good.example/")).toBe(false); - expect(() => new URL("/x", "http://\u206A/base.example/")).toThrow(); + expect(() => new URL("/x", "http://\u206A/base.example/")).toThrow(errInvalidURL); // Mixed hosts still strip the ignored code point rather than failing. expect(new URL("http://a\u180Eb/").href).toBe("http://ab/"); expect(new URL("file://a\u180Eb/x").host).toBe("ab"); From 22044db557ee2b2f925256dd8fddbc087a8856d7 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:22:27 +0000 Subject: [PATCH 3/8] Tighten guard comments --- src/jsc/bindings/DOMURL.cpp | 8 +++----- src/jsc/bindings/URLDecomposition.cpp | 3 +-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/jsc/bindings/DOMURL.cpp b/src/jsc/bindings/DOMURL.cpp index 69bb80558f9a..85adbbdde436 100644 --- a/src/jsc/bindings/DOMURL.cpp +++ b/src/jsc/bindings/DOMURL.cpp @@ -150,11 +150,9 @@ static String applyIDNADeltaToURLAuthority(const String& urlString, StringView s return {}; auto mappedHost = Bun::applyUnicode16IDNADelta(hostView.toString()); - // A host of only ignored-class code points maps to the empty string, which is - // domain-to-ASCII failure (node throws ERR_INVALID_URL). Splicing an empty host - // back in would instead let the special-authority-ignore-slashes state promote - // the first path segment to the host (http://\u180E/a -> http:///a -> host "a"). - // Leave the input untouched so the parser rejects the original code points. + // An all-ignored host maps to empty, which is domain-to-ASCII failure; splicing + // "" in would instead reparse http://\u180E/a as http:///a (host "a"). Skip the + // rewrite so the parser rejects the original code points. if (mappedHost.isEmpty()) return {}; StringBuilder builder; diff --git a/src/jsc/bindings/URLDecomposition.cpp b/src/jsc/bindings/URLDecomposition.cpp index d99c61864156..0ce5cc505a8c 100644 --- a/src/jsc/bindings/URLDecomposition.cpp +++ b/src/jsc/bindings/URLDecomposition.cpp @@ -125,8 +125,7 @@ void URLDecomposition::setHost(StringView value) auto hostSpan = hostEnd == notFound ? value : value.left(hostEnd); if (Bun::containsUnicode16IDNADeltaSource(hostSpan)) { auto mappedHost = Bun::applyUnicode16IDNADelta(hostSpan.toString()); - // A non-empty host span mapping to empty is domain-to-ASCII failure - // (setter no-ops, even for file: where a literal "" is assignable). + // A host mapping to empty is a failed host parse, not an assignable literal "". if (mappedHost.isEmpty()) return; mappedValue = hostEnd == notFound ? mappedHost : makeString(mappedHost, value.substring(hostEnd)); From 14ed7efc5a18edad43706f30125b6b0462247176 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:23:37 +0000 Subject: [PATCH 4/8] Shorten guard comment --- src/jsc/bindings/DOMURL.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/jsc/bindings/DOMURL.cpp b/src/jsc/bindings/DOMURL.cpp index 85adbbdde436..e93c12043a2d 100644 --- a/src/jsc/bindings/DOMURL.cpp +++ b/src/jsc/bindings/DOMURL.cpp @@ -150,9 +150,7 @@ static String applyIDNADeltaToURLAuthority(const String& urlString, StringView s return {}; auto mappedHost = Bun::applyUnicode16IDNADelta(hostView.toString()); - // An all-ignored host maps to empty, which is domain-to-ASCII failure; splicing - // "" in would instead reparse http://\u180E/a as http:///a (host "a"). Skip the - // rewrite so the parser rejects the original code points. + // All-ignored host: splicing "" in would reparse http://\u180E/a as http:///a (host "a"); skip so the parser rejects it. if (mappedHost.isEmpty()) return {}; StringBuilder builder; From ec82ee6bb7889dbbb12a6c0f82b8716526e6510a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:41:35 +0000 Subject: [PATCH 5/8] url: bound the setter IDNA delta span at the first host terminator The host/hostname setters applied the Unicode 16 IDNA delta to the whole input, so an all-ignored host followed by a terminator ("\u180E/x") mapped to a non-empty string, bypassed the empty-host guard, and reached the parser: on file: URLs it cleared the host and on http it promoted the first path segment, where node treats the pre-terminator span as a failed host parse and no-ops. Bound the delta (and the empty-host check) to the span before the first / \ ? #, matching the WHATWG host state and the constructor path in DOMURL.cpp. --- src/jsc/bindings/URLDecomposition.cpp | 37 ++++++++++++++++++++------- test/js/web/url/url.test.ts | 12 +++++++++ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/jsc/bindings/URLDecomposition.cpp b/src/jsc/bindings/URLDecomposition.cpp index 0ce5cc505a8c..b577511484e2 100644 --- a/src/jsc/bindings/URLDecomposition.cpp +++ b/src/jsc/bindings/URLDecomposition.cpp @@ -113,6 +113,18 @@ static unsigned countASCIIDigits(StringView string) return length; } +// The WHATWG host/hostname states stop consuming at the first of these; the +// IDNA delta must not touch anything at or past it (see DOMURL.cpp). +static size_t findHostTerminator(StringView value) +{ + for (size_t i = 0; i < value.length(); i++) { + char16_t c = value[i]; + if (c == '/' || c == '\\' || c == '?' || c == '#') + return i; + } + return value.length(); +} + void URLDecomposition::setHost(StringView value) { auto fullURL = this->fullURL(); @@ -121,14 +133,16 @@ void URLDecomposition::setHost(StringView value) // Non-special schemes and '['-prefixed (IPv6) hosts never run IDNA. String mappedValue; if (fullURL.hasSpecialScheme() && !value.startsWith('[')) { - size_t hostEnd = value.reverseFind(':'); - auto hostSpan = hostEnd == notFound ? value : value.left(hostEnd); + size_t terminator = findHostTerminator(value); + size_t hostEnd = value.left(terminator).reverseFind(':'); + size_t hostSpanEnd = hostEnd == notFound ? terminator : hostEnd; + auto hostSpan = value.left(hostSpanEnd); if (Bun::containsUnicode16IDNADeltaSource(hostSpan)) { auto mappedHost = Bun::applyUnicode16IDNADelta(hostSpan.toString()); // A host mapping to empty is a failed host parse, not an assignable literal "". if (mappedHost.isEmpty()) return; - mappedValue = hostEnd == notFound ? mappedHost : makeString(mappedHost, value.substring(hostEnd)); + mappedValue = makeString(mappedHost, value.substring(hostSpanEnd)); value = mappedValue; } } @@ -176,12 +190,17 @@ void URLDecomposition::setHostname(StringView host) // See setHost: the input is a hostname by definition, and only special // schemes run IDNA on it. String mappedHost; - if (fullURL.hasSpecialScheme() && !host.startsWith('[') && Bun::containsUnicode16IDNADeltaSource(host)) { - mappedHost = Bun::applyUnicode16IDNADelta(host.toString()); - // See setHost: mapping a non-empty hostname to empty is failure, not "". - if (mappedHost.isEmpty()) - return; - host = mappedHost; + if (fullURL.hasSpecialScheme() && !host.startsWith('[')) { + size_t terminator = findHostTerminator(host); + auto hostSpan = host.left(terminator); + if (Bun::containsUnicode16IDNADeltaSource(hostSpan)) { + auto mappedSpan = Bun::applyUnicode16IDNADelta(hostSpan.toString()); + // See setHost: mapping a non-empty hostname to empty is failure, not "". + if (mappedSpan.isEmpty()) + return; + mappedHost = makeString(mappedSpan, host.substring(terminator)); + host = mappedHost; + } } if (host.isEmpty() && !fullURL.protocolIsFile() && fullURL.hasSpecialScheme()) return; diff --git a/test/js/web/url/url.test.ts b/test/js/web/url/url.test.ts index f7e36736841c..e0faaefcb00d 100755 --- a/test/js/web/url/url.test.ts +++ b/test/js/web/url/url.test.ts @@ -199,6 +199,18 @@ describe("url", () => { const f3 = new URL("file://server/share"); f3.host = ""; expect(f3.href).toBe("file:///share"); + // A terminator after the ignored code points must not smuggle the tail + // past the empty-host guard: the host span ends at the first / \ ? #. + for (const tail of ["/x", "\\x", "?x", "#x"]) { + for (const base of ["file://server/share", "http://ok.example/p"]) { + const withHost = new URL(base); + withHost.host = "\u180E" + tail; + expect(withHost.href).toBe(base); + const withHostname = new URL(base); + withHostname.hostname = "\u180E" + tail; + expect(withHostname.href).toBe(base); + } + } }); it("prints", () => { From 10ea67a4e273f02ef8cdd4c59b6e064d6946e397 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:42:57 +0000 Subject: [PATCH 6/8] Shorten helper comment --- src/jsc/bindings/URLDecomposition.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/jsc/bindings/URLDecomposition.cpp b/src/jsc/bindings/URLDecomposition.cpp index b577511484e2..34df3d90ab4a 100644 --- a/src/jsc/bindings/URLDecomposition.cpp +++ b/src/jsc/bindings/URLDecomposition.cpp @@ -113,8 +113,7 @@ static unsigned countASCIIDigits(StringView string) return length; } -// The WHATWG host/hostname states stop consuming at the first of these; the -// IDNA delta must not touch anything at or past it (see DOMURL.cpp). +// The WHATWG host/hostname states stop at the first of these; the IDNA delta must not touch anything past it. static size_t findHostTerminator(StringView value) { for (size_t i = 0; i < value.length(); i++) { From 0b20ff578b68b8f320e82e32a4d0f71cc1dbcc9e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:45:26 +0000 Subject: [PATCH 7/8] test: cover parse/canParse with invalid base and literal empty hostname --- test/js/web/url/url.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/js/web/url/url.test.ts b/test/js/web/url/url.test.ts index e0faaefcb00d..f1570a3f4eb8 100755 --- a/test/js/web/url/url.test.ts +++ b/test/js/web/url/url.test.ts @@ -184,7 +184,10 @@ describe("url", () => { // Scheme-relative input and an all-ignored base reach the same host span. expect(() => new URL("//\u180E/evil.example/", "http://good.example/")).toThrow(errInvalidURL); expect(URL.canParse("//\u180E/evil.example/", "http://good.example/")).toBe(false); + expect(URL.parse("//\u180E/evil.example/", "http://good.example/")).toBeNull(); expect(() => new URL("/x", "http://\u206A/base.example/")).toThrow(errInvalidURL); + expect(URL.canParse("/x", "http://\u206A/base.example/")).toBe(false); + expect(URL.parse("/x", "http://\u206A/base.example/")).toBeNull(); // Mixed hosts still strip the ignored code point rather than failing. expect(new URL("http://a\u180Eb/").href).toBe("http://ab/"); expect(new URL("file://a\u180Eb/x").host).toBe("ab"); @@ -199,6 +202,9 @@ describe("url", () => { const f3 = new URL("file://server/share"); f3.host = ""; expect(f3.href).toBe("file:///share"); + const f4 = new URL("file://server/share"); + f4.hostname = ""; + expect(f4.href).toBe("file:///share"); // A terminator after the ignored code points must not smuggle the tail // past the empty-host guard: the host span ends at the first / \ ? #. for (const tail of ["/x", "\\x", "?x", "#x"]) { From 3c1dd0fcb6e4d8ee3bc3acdd51252e994e101a6d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 8 Aug 2026 01:05:52 +0000 Subject: [PATCH 8/8] url: share the host-terminator scan via findURLHostTerminator --- src/jsc/bindings/DOMURL.cpp | 9 +-------- src/jsc/bindings/NodeURL.cpp | 19 +++++++++++-------- src/jsc/bindings/NodeURLHelpers.h | 4 ++++ src/jsc/bindings/URLDecomposition.cpp | 15 ++------------- 4 files changed, 18 insertions(+), 29 deletions(-) diff --git a/src/jsc/bindings/DOMURL.cpp b/src/jsc/bindings/DOMURL.cpp index e93c12043a2d..924c11245f6f 100644 --- a/src/jsc/bindings/DOMURL.cpp +++ b/src/jsc/bindings/DOMURL.cpp @@ -117,14 +117,7 @@ static String applyIDNADeltaToURLAuthority(const String& urlString, StringView s // The authority ends at the first path/query/fragment terminator; // backslash terminates it for special schemes and never appears in a // valid host, so treating it as a terminator is safe for both kinds. - size_t authorityEnd = view.length(); - for (size_t i = authorityStart; i < view.length(); i++) { - char16_t ch = view[i]; - if (ch == '/' || ch == '?' || ch == '#' || ch == '\\') { - authorityEnd = i; - break; - } - } + size_t authorityEnd = Bun::findURLHostTerminator(view, authorityStart); // Userinfo is percent-encoded, not IDNA-mapped, in node too: only the // host span after the last '@' gets the delta. diff --git a/src/jsc/bindings/NodeURL.cpp b/src/jsc/bindings/NodeURL.cpp index 394c92f88414..d50d5bde64f2 100644 --- a/src/jsc/bindings/NodeURL.cpp +++ b/src/jsc/bindings/NodeURL.cpp @@ -124,6 +124,16 @@ String applyUnicode16IDNADelta(const String& input) return builder.toString(); } +size_t findURLHostTerminator(StringView view, size_t start) +{ + for (size_t i = start; i < view.length(); i++) { + char16_t c = view[i]; + if (c == '/' || c == '\\' || c == '?' || c == '#') + return i; + } + return view.length(); +} + // Port of Node's icu-based ToASCII (removed in nodejs/node#55156): // https://github.com/nodejs/node/blob/9f5000e0f2a2^/src/node_i18n.cc — filter // the CheckHyphens/VerifyDnsLength error classes, fail otherwise unless lenient. @@ -187,14 +197,7 @@ static String parseDomainAsHost(const String& rawDomain) // The hostname setter's basic-URL parse stops at the first path, query, // fragment, or backslash (special scheme) terminator. StringView view { domain }; - size_t end = view.length(); - for (size_t i = 0; i < view.length(); i++) { - char16_t c = view[i]; - if (c == '/' || c == '?' || c == '#' || c == '\\') { - end = i; - break; - } - } + size_t end = findURLHostTerminator(view); String host = domain.left(end); if (host.isEmpty()) return {}; diff --git a/src/jsc/bindings/NodeURLHelpers.h b/src/jsc/bindings/NodeURLHelpers.h index b05875c9449b..c3301407fde9 100644 --- a/src/jsc/bindings/NodeURLHelpers.h +++ b/src/jsc/bindings/NodeURLHelpers.h @@ -20,4 +20,8 @@ bool containsUnicode16IDNADeltaSource(WTF::StringView view); // the input unchanged when no delta source is present. WTF::String applyUnicode16IDNADelta(const WTF::String& input); +// Index of the first WHATWG host-state terminator (/ \ ? #) at or after +// `start`, or view.length() when none. +size_t findURLHostTerminator(WTF::StringView view, size_t start = 0); + } // namespace Bun diff --git a/src/jsc/bindings/URLDecomposition.cpp b/src/jsc/bindings/URLDecomposition.cpp index 34df3d90ab4a..718c12087df5 100644 --- a/src/jsc/bindings/URLDecomposition.cpp +++ b/src/jsc/bindings/URLDecomposition.cpp @@ -113,17 +113,6 @@ static unsigned countASCIIDigits(StringView string) return length; } -// The WHATWG host/hostname states stop at the first of these; the IDNA delta must not touch anything past it. -static size_t findHostTerminator(StringView value) -{ - for (size_t i = 0; i < value.length(); i++) { - char16_t c = value[i]; - if (c == '/' || c == '\\' || c == '?' || c == '#') - return i; - } - return value.length(); -} - void URLDecomposition::setHost(StringView value) { auto fullURL = this->fullURL(); @@ -132,7 +121,7 @@ void URLDecomposition::setHost(StringView value) // Non-special schemes and '['-prefixed (IPv6) hosts never run IDNA. String mappedValue; if (fullURL.hasSpecialScheme() && !value.startsWith('[')) { - size_t terminator = findHostTerminator(value); + size_t terminator = Bun::findURLHostTerminator(value); size_t hostEnd = value.left(terminator).reverseFind(':'); size_t hostSpanEnd = hostEnd == notFound ? terminator : hostEnd; auto hostSpan = value.left(hostSpanEnd); @@ -190,7 +179,7 @@ void URLDecomposition::setHostname(StringView host) // schemes run IDNA on it. String mappedHost; if (fullURL.hasSpecialScheme() && !host.startsWith('[')) { - size_t terminator = findHostTerminator(host); + size_t terminator = Bun::findURLHostTerminator(host); auto hostSpan = host.left(terminator); if (Bun::containsUnicode16IDNADeltaSource(hostSpan)) { auto mappedSpan = Bun::applyUnicode16IDNADelta(hostSpan.toString());