From 7717e1482558c08138b9a02ac0e8ee99fdea8b37 Mon Sep 17 00:00:00 2001 From: Taku Amano Date: Thu, 12 Mar 2026 21:57:38 +0900 Subject: [PATCH 1/2] perf: added fast path handling for port numbers in the range 1000-59999 --- src/url.ts | 31 +++++++++++++------- test/url.test.ts | 76 ++++++++++++++++++++++++++---------------------- 2 files changed, 62 insertions(+), 45 deletions(-) diff --git a/src/url.ts b/src/url.ts index 26fac87..7a3de3e 100644 --- a/src/url.ts +++ b/src/url.ts @@ -68,7 +68,6 @@ export const buildUrl = (scheme: string, host: string, incomingUrl: string) => { const url = `${scheme}://${host}${incomingUrl}` let needsHostValidationByURL = false - let portStart = -1 for (let i = 0, len = host.length; i < len; i++) { const c = host.charCodeAt(i) if (c > 0x7f || safeHostChar[c] === 0) { @@ -77,16 +76,28 @@ export const buildUrl = (scheme: string, host: string, incomingUrl: string) => { } if (c === 0x3a) { // ':' - portStart = i - } - } + i++ + const firstDigit = host.charCodeAt(i) - // Validate port range if present in safe host - if (!needsHostValidationByURL && portStart !== -1) { - const portStr = host.substring(portStart + 1) - const port = parseInt(portStr, 10) - if (portStr.length === 0 || port !== port || port < 0 || port > 65535) { - needsHostValidationByURL = true + // if the number starts with 1-9 and ranges from 1000-59999, then there is no need for normalization, so proceed + if ( + firstDigit < 0x31 || + firstDigit > 0x39 || + i + 4 > len || + i + (firstDigit < 0x36 ? 5 : 4) < len + ) { + needsHostValidationByURL = true + break + } + for (; i < len; i++) { + const c = host.charCodeAt(i) + if (c < 0x30 || c > 0x39) { + needsHostValidationByURL = true + break + } + } + + // valid port number } } diff --git a/test/url.test.ts b/test/url.test.ts index e1aeffc..9ca6ee8 100644 --- a/test/url.test.ts +++ b/test/url.test.ts @@ -18,41 +18,47 @@ describe('buildUrl', () => { describe('URL normalization', () => { test.each([ - ['[::1]', '/foo.txt'], - ['[::1]:8080', '/foo.txt'], - ['localhost', '/'], - ['localhost', '/foo/bar/baz'], - ['localhost', '/foo_bar'], - ['localhost', '/foo//bar'], - ['localhost', '/static/%2e%2e/foo.txt'], - ['localhost', '/static\\..\\foo.txt'], - ['localhost', '/..'], - ['localhost', '/foo/.'], - ['localhost', '/foo/bar/..'], - ['localhost', '/a/b/../../c'], - ['localhost', '/a/../../../b'], - ['localhost', '/a/b/c/../../../'], - ['localhost', '/./foo.txt'], - ['localhost', '/foo/../bar.txt'], - ['localhost', '/a/./b/../c?q=%2E%2E#hash'], - ['localhost', '/foo/%2E/bar/../baz'], - ['localhost', '/hello%20world'], - ['localhost', '/foo%23bar'], - ['localhost', '/foo"bar'], - ['localhost', '/%2e%2E/foo'], - ['localhost', '/caf%C3%A9'], - ['localhost', '/foo%2fbar/..//baz'], - ['localhost', '/foo?q=../bar'], - ['localhost', '/path?q=hello%20world'], - ['localhost', '/file.txt'], - ['localhost', ''], - ['LOCALHOST', '/foo.txt'], - ['LOCALHOST:80', '/foo.txt'], - ['LOCALHOST:443', '/foo.txt'], - ['LOCALHOST:8080', '/foo.txt'], - ['Localhost:3000', '/foo.txt'], - ])('Should normalize %s to %s', async (host, url) => { - expect(buildUrl('http', host, url)).toBe(new URL(url, `http://${host}`).href) + ['https', '[::1]', '/foo.txt'], + ['https', '[::1]:8080', '/foo.txt'], + ['https', 'localhost', '/'], + ['https', 'localhost', '/foo/bar/baz'], + ['https', 'localhost', '/foo_bar'], + ['https', 'localhost', '/foo//bar'], + ['https', 'localhost', '/static/%2e%2e/foo.txt'], + ['https', 'localhost', '/static\\..\\foo.txt'], + ['https', 'localhost', '/..'], + ['https', 'localhost', '/foo/.'], + ['https', 'localhost', '/foo/bar/..'], + ['https', 'localhost', '/a/b/../../c'], + ['https', 'localhost', '/a/../../../b'], + ['https', 'localhost', '/a/b/c/../../../'], + ['https', 'localhost', '/./foo.txt'], + ['https', 'localhost', '/foo/../bar.txt'], + ['https', 'localhost', '/a/./b/../c?q=%2E%2E#hash'], + ['https', 'localhost', '/foo/%2E/bar/../baz'], + ['https', 'localhost', '/hello%20world'], + ['https', 'localhost', '/foo%23bar'], + ['https', 'localhost', '/foo"bar'], + ['https', 'localhost', '/%2e%2E/foo'], + ['https', 'localhost', '/caf%C3%A9'], + ['https', 'localhost', '/foo%2fbar/..//baz'], + ['https', 'localhost', '/foo?q=../bar'], + ['https', 'localhost', '/path?q=hello%20world'], + ['https', 'localhost', '/file.txt'], + ['https', 'localhost', ''], + ['http', 'localhost:080', '/foo.txt'], + ['http', 'localhost:08080', '/foo.txt'], + ['http', 'localhost:80', '/foo.txt'], + ['https', 'localhost:80', '/foo.txt'], + ['http', 'localhost:443', '/foo.txt'], + ['https', 'localhost:443', '/foo.txt'], + ['https', 'LOCALHOST', '/foo.txt'], + ['https', 'LOCALHOST:80', '/foo.txt'], + ['https', 'LOCALHOST:443', '/foo.txt'], + ['https', 'LOCALHOST:8080', '/foo.txt'], + ['https', 'Localhost:3000', '/foo.txt'], + ])('Should normalize %s to %s', async (scheme, host, url) => { + expect(buildUrl(scheme, host, url)).toBe(new URL(url, `${scheme}://${host}`).href) }) it('Should throw a RequestError for non-origin-form request-target', async () => { From 3537c8a9b65b1ff23fa363f38ab88fad17a36790 Mon Sep 17 00:00:00 2001 From: Taku Amano Date: Thu, 12 Mar 2026 22:11:59 +0900 Subject: [PATCH 2/2] test: fix test description --- test/url.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/url.test.ts b/test/url.test.ts index 9ca6ee8..38ef76d 100644 --- a/test/url.test.ts +++ b/test/url.test.ts @@ -57,8 +57,8 @@ describe('buildUrl', () => { ['https', 'LOCALHOST:443', '/foo.txt'], ['https', 'LOCALHOST:8080', '/foo.txt'], ['https', 'Localhost:3000', '/foo.txt'], - ])('Should normalize %s to %s', async (scheme, host, url) => { - expect(buildUrl(scheme, host, url)).toBe(new URL(url, `${scheme}://${host}`).href) + ])('Should successfully normalize `%s://%s%s`', async (scheme, host, path) => { + expect(buildUrl(scheme, host, path)).toBe(new URL(path, `${scheme}://${host}`).href) }) it('Should throw a RequestError for non-origin-form request-target', async () => {