From fb16f5f9b7e2ab4ee9970aa2098f27870e7e31ef Mon Sep 17 00:00:00 2001 From: shubham Date: Wed, 7 Jan 2026 20:35:11 +0530 Subject: [PATCH 1/8] Implement origin normalization in MockAgent for case-insensitivity and URL handling --- lib/mock/mock-agent.js | 38 +++++---- lib/mock/mock-utils.js | 25 +++++- test/mock-agent.js | 182 +++++++++++++++++++++++++++++++++++++++++ test/mock-utils.js | 136 +++++++++++++++++++++++++++++- 4 files changed, 364 insertions(+), 17 deletions(-) diff --git a/lib/mock/mock-agent.js b/lib/mock/mock-agent.js index 3ab14949f35..ddf977883de 100644 --- a/lib/mock/mock-agent.js +++ b/lib/mock/mock-agent.js @@ -22,7 +22,7 @@ const { } = require('./mock-symbols') const MockClient = require('./mock-client') const MockPool = require('./mock-pool') -const { matchValue, normalizeSearchParams, buildAndValidateMockOptions } = require('./mock-utils') +const { matchValue, normalizeSearchParams, buildAndValidateMockOptions, normalizeOrigin } = require('./mock-utils') const { InvalidArgumentError, UndiciError } = require('../core/errors') const Dispatcher = require('../dispatcher/dispatcher') const PendingInterceptorsFormatter = require('./pending-interceptors-formatter') @@ -56,9 +56,8 @@ class MockAgent extends Dispatcher { } get (origin) { - const originKey = this[kIgnoreTrailingSlash] - ? origin.replace(/\/$/, '') - : origin + // Normalize origin to handle URL objects and case-insensitive hostnames + const originKey = normalizeOrigin(origin, this[kIgnoreTrailingSlash]) let dispatcher = this[kMockAgentGet](originKey) @@ -70,14 +69,17 @@ class MockAgent extends Dispatcher { } dispatch (opts, handler) { + const normalizedOrigin = normalizeOrigin(opts.origin, this[kIgnoreTrailingSlash]) + // Call MockAgent.get to perform additional setup before dispatching as normal - this.get(opts.origin) + this.get(normalizedOrigin) this[kMockAgentAddCallHistoryLog](opts) const acceptNonStandardSearchParameters = this[kMockAgentAcceptsNonStandardSearchParameters] const dispatchOpts = { ...opts } + dispatchOpts.origin = normalizedOrigin if (acceptNonStandardSearchParameters && dispatchOpts.path) { const [path, searchParams] = dispatchOpts.path.split('?') @@ -165,7 +167,8 @@ class MockAgent extends Dispatcher { } [kMockAgentSet] (origin, dispatcher) { - this[kClients].set(origin, { count: 0, dispatcher }) + const normalizedOrigin = normalizeOrigin(origin, this[kIgnoreTrailingSlash]) + this[kClients].set(normalizedOrigin, { count: 0, dispatcher }) } [kFactory] (origin) { @@ -176,26 +179,31 @@ class MockAgent extends Dispatcher { } [kMockAgentGet] (origin) { - // First check if we can immediately find it - const result = this[kClients].get(origin) + const normalizedOrigin = normalizeOrigin(origin, this[kIgnoreTrailingSlash]) + + // First check if we can immediately find it with normalized origin + const result = this[kClients].get(normalizedOrigin) if (result?.dispatcher) { return result.dispatcher } // If the origin is not a string create a dummy parent pool and return to user - if (typeof origin !== 'string') { + if (typeof normalizedOrigin !== 'string') { const dispatcher = this[kFactory]('http://localhost:9999') - this[kMockAgentSet](origin, dispatcher) + this[kMockAgentSet](normalizedOrigin, dispatcher) return dispatcher } // If we match, create a pool and assign the same dispatches for (const [keyMatcher, result] of Array.from(this[kClients])) { - if (result && typeof keyMatcher !== 'string' && matchValue(keyMatcher, origin)) { - const dispatcher = this[kFactory](origin) - this[kMockAgentSet](origin, dispatcher) - dispatcher[kDispatches] = result.dispatcher[kDispatches] - return dispatcher + if (result && typeof keyMatcher !== 'string') { + const normalizedKeyMatcher = normalizeOrigin(keyMatcher, this[kIgnoreTrailingSlash]) + if (matchValue(normalizedKeyMatcher, normalizedOrigin)) { + const dispatcher = this[kFactory](normalizedOrigin) + this[kMockAgentSet](normalizedOrigin, dispatcher) + dispatcher[kDispatches] = result.dispatcher[kDispatches] + return dispatcher + } } } } diff --git a/lib/mock/mock-utils.js b/lib/mock/mock-utils.js index 3b6d5b741bc..6f477b46514 100644 --- a/lib/mock/mock-utils.js +++ b/lib/mock/mock-utils.js @@ -396,6 +396,28 @@ function checkNetConnect (netConnect, origin) { return false } +function normalizeOrigin (origin, ignoreTrailingSlash = false) { + if (typeof origin !== 'string' && !(origin instanceof URL)) { + return origin + } + + let originString = typeof origin === 'string' ? origin : origin.toString() + + try { + const url = new URL(originString) + url.hostname = url.hostname.toLowerCase() + originString = url.origin + } catch (err) { + return originString + } + + if (ignoreTrailingSlash && originString.endsWith('/')) { + originString = originString.slice(0, -1) + } + + return originString +} + function buildAndValidateMockOptions (opts) { const { agent, ...mockOptions } = opts @@ -430,5 +452,6 @@ module.exports = { buildAndValidateMockOptions, getHeaderByName, buildHeadersFromArray, - normalizeSearchParams + normalizeSearchParams, + normalizeOrigin } diff --git a/test/mock-agent.js b/test/mock-agent.js index f0b20e909e6..a0df6413a4e 100644 --- a/test/mock-agent.js +++ b/test/mock-agent.js @@ -2934,3 +2934,185 @@ test('MockAgent - should not accept non-standard search parameters when acceptNo const textResponse = await getResponse(body) t.assert.strictEqual(textResponse, '(non-intercepted) response from server') }) + +// https://github.com/nodejs/undici/issues/4703 +describe('MockAgent - case-insensitive origin matching', () => { + test('should match origins with different hostname case', async (t) => { + t.plan(2) + + const mockAgent = new MockAgent() + after(() => mockAgent.close()) + + const url1 = 'http://myEndpoint' + const url2 = 'http://myendpoint' // Different case + + const mockPool = mockAgent.get(url1) + mockPool + .intercept({ + path: '/test', + method: 'GET' + }) + .reply(200, { success: true }, { + headers: { 'content-type': 'application/json' } + }) + + const { statusCode, body } = await mockAgent.request({ + origin: url2, // Different case should still match + method: 'GET', + path: '/test' + }) + + t.assert.strictEqual(statusCode, 200) + const jsonResponse = JSON.parse(await getResponse(body)) + t.assert.deepStrictEqual(jsonResponse, { success: true }) + }) + + test('should match URL object origin with string origin', async (t) => { + t.plan(2) + + const mockAgent = new MockAgent() + after(() => mockAgent.close()) + + const url = 'http://myEndpoint' + + const mockPool = mockAgent.get(url) + mockPool + .intercept({ + path: '/path', + method: 'GET' + }) + .reply(200, { key: 'value' }, { + headers: { 'content-type': 'application/json' } + }) + + const { statusCode, body } = await mockAgent.request({ + origin: new URL(url), // URL object should match string origin + method: 'GET', + path: '/path' + }) + + t.assert.strictEqual(statusCode, 200) + const jsonResponse = JSON.parse(await getResponse(body)) + t.assert.deepStrictEqual(jsonResponse, { key: 'value' }) + }) + + test('should match URL object with different hostname case', async (t) => { + t.plan(2) + + const mockAgent = new MockAgent() + after(() => mockAgent.close()) + + const url1 = 'http://Example.com' + const url2 = new URL('http://example.com') // Different case + + const mockPool = mockAgent.get(url1) + mockPool + .intercept({ + path: '/test', + method: 'GET' + }) + .reply(200, { success: true }, { + headers: { 'content-type': 'application/json' } + }) + + const { statusCode, body } = await mockAgent.request({ + origin: url2, // URL object with different case should match + method: 'GET', + path: '/test' + }) + + t.assert.strictEqual(statusCode, 200) + const jsonResponse = JSON.parse(await getResponse(body)) + t.assert.deepStrictEqual(jsonResponse, { success: true }) + }) + + test('should handle mixed case scenarios correctly', async (t) => { + t.plan(2) + + const mockAgent = new MockAgent() + after(() => mockAgent.close()) + + const url1 = 'http://MyEndpoint.com' + const url2 = 'http://myendpoint.com' // All lowercase + + const mockPool = mockAgent.get(url1) + mockPool + .intercept({ + path: '/api', + method: 'GET' + }) + .reply(200, { data: 'test' }, { + headers: { 'content-type': 'application/json' } + }) + + const { statusCode, body } = await mockAgent.request({ + origin: url2, + method: 'GET', + path: '/api' + }) + + t.assert.strictEqual(statusCode, 200) + const jsonResponse = JSON.parse(await getResponse(body)) + t.assert.deepStrictEqual(jsonResponse, { data: 'test' }) + }) + + test('should preserve port numbers when normalizing', async (t) => { + t.plan(2) + + const mockAgent = new MockAgent() + after(() => mockAgent.close()) + + const url1 = 'http://Example.com:8080' + const url2 = 'http://example.com:8080' // Different case, same port + + const mockPool = mockAgent.get(url1) + mockPool + .intercept({ + path: '/test', + method: 'GET' + }) + .reply(200, { port: 8080 }, { + headers: { 'content-type': 'application/json' } + }) + + const { statusCode, body } = await mockAgent.request({ + origin: url2, + method: 'GET', + path: '/test' + }) + + t.assert.strictEqual(statusCode, 200) + const jsonResponse = JSON.parse(await getResponse(body)) + t.assert.deepStrictEqual(jsonResponse, { port: 8080 }) + }) + + test('should handle https origins with case differences', async (t) => { + t.plan(2) + + const mockAgent = new MockAgent() + after(() => mockAgent.close()) + + const url1 = 'https://Api.Example.com' + const url2 = new URL('https://api.example.com') // Different case + + const mockPool = mockAgent.get(url1) + mockPool + .intercept({ + path: '/data', + method: 'GET' + }) + .reply(200, { secure: true }, { + headers: { 'content-type': 'application/json' } + }) + + const { statusCode, body } = await mockAgent.request({ + origin: url2, + method: 'GET', + path: '/data' + }) + + t.assert.strictEqual(statusCode, 200) + const jsonResponse = JSON.parse(await getResponse(body)) + t.assert.deepStrictEqual(jsonResponse, { secure: true }) + }) +}) diff --git a/test/mock-utils.js b/test/mock-utils.js index 3b00242d8b7..256dffd3d7f 100644 --- a/test/mock-utils.js +++ b/test/mock-utils.js @@ -9,7 +9,8 @@ const { getStatusText, getHeaderByName, buildHeadersFromArray, - normalizeSearchParams + normalizeSearchParams, + normalizeOrigin } = require('../lib/mock/mock-utils') test('deleteMockDispatch - should do nothing if not able to find mock dispatch', (t) => { @@ -268,3 +269,136 @@ describe('normalizeQueryParams', () => { t.assert.deepStrictEqual(normalizeSearchParams("a='1,2,3'").toString(), `a=${encodedSingleQuote}${encodeURIComponent('1,2,3')}${encodedSingleQuote}`) }) }) + +describe('normalizeOrigin', () => { + test('should normalize hostname to lowercase for string origins', (t) => { + t.plan(4) + + t.assert.strictEqual(normalizeOrigin('http://Example.com'), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('http://EXAMPLE.COM'), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('https://Api.Example.com'), 'https://api.example.com') + t.assert.strictEqual(normalizeOrigin('http://MyEndpoint'), 'http://myendpoint') + }) + + test('should normalize hostname to lowercase for URL objects', (t) => { + t.plan(4) + + t.assert.strictEqual(normalizeOrigin(new URL('http://Example.com')), 'http://example.com') + t.assert.strictEqual(normalizeOrigin(new URL('http://EXAMPLE.COM')), 'http://example.com') + t.assert.strictEqual(normalizeOrigin(new URL('https://Api.Example.com')), 'https://api.example.com') + t.assert.strictEqual(normalizeOrigin(new URL('http://MyEndpoint')), 'http://myendpoint') + }) + + test('should preserve port numbers', (t) => { + t.plan(4) + + t.assert.strictEqual(normalizeOrigin('http://Example.com:8080'), 'http://example.com:8080') + // Note: url.origin omits default ports (443 for HTTPS, 80 for HTTP) + t.assert.strictEqual(normalizeOrigin('https://Api.Example.com:443'), 'https://api.example.com') + t.assert.strictEqual(normalizeOrigin(new URL('http://Example.com:3000')), 'http://example.com:3000') + t.assert.strictEqual(normalizeOrigin(new URL('https://Test.com:8443')), 'https://test.com:8443') + }) + + test('should handle default ports correctly', (t) => { + t.plan(2) + + // Default ports should be omitted from origin + t.assert.strictEqual(normalizeOrigin('http://Example.com:80'), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('https://Example.com:443'), 'https://example.com') + }) + + test('should remove trailing slash when ignoreTrailingSlash is true', (t) => { + t.plan(4) + + t.assert.strictEqual(normalizeOrigin('http://example.com/', true), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('https://example.com/', true), 'https://example.com') + t.assert.strictEqual(normalizeOrigin(new URL('http://example.com/'), true), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('http://example.com', true), 'http://example.com') + }) + + test('should not remove trailing slash when ignoreTrailingSlash is false', (t) => { + t.plan(2) + + t.assert.strictEqual(normalizeOrigin('http://example.com/', false), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('http://example.com', false), 'http://example.com') + }) + + test('should return RegExp matchers as-is', (t) => { + t.plan(2) + + const regex = /http:\/\/example\.com/ + t.assert.strictEqual(normalizeOrigin(regex), regex) + t.assert.strictEqual(normalizeOrigin(regex, true), regex) + }) + + test('should return function matchers as-is', (t) => { + t.plan(2) + + const fn = (origin) => origin === 'http://example.com' + t.assert.strictEqual(normalizeOrigin(fn), fn) + t.assert.strictEqual(normalizeOrigin(fn, true), fn) + }) + + test('should return other non-string, non-URL types as-is', (t) => { + t.plan(4) + + const obj = { origin: 'http://example.com' } + const num = 123 + const bool = true + const nullValue = null + + t.assert.strictEqual(normalizeOrigin(obj), obj) + t.assert.strictEqual(normalizeOrigin(num), num) + t.assert.strictEqual(normalizeOrigin(bool), bool) + t.assert.strictEqual(normalizeOrigin(nullValue), nullValue) + }) + + test('should handle invalid URLs gracefully', (t) => { + t.plan(2) + + // Invalid URL strings should be returned as-is + t.assert.strictEqual(normalizeOrigin('not-a-url'), 'not-a-url') + t.assert.strictEqual(normalizeOrigin('://invalid'), '://invalid') + }) + + test('should handle IPv4 addresses', (t) => { + t.plan(2) + + t.assert.strictEqual(normalizeOrigin('http://192.168.1.1'), 'http://192.168.1.1') + t.assert.strictEqual(normalizeOrigin('http://127.0.0.1:3000'), 'http://127.0.0.1:3000') + }) + + test('should handle IPv6 addresses', (t) => { + t.plan(2) + + t.assert.strictEqual(normalizeOrigin('http://[::1]'), 'http://[::1]') + t.assert.strictEqual(normalizeOrigin('http://[2001:db8::1]:8080'), 'http://[2001:db8::1]:8080') + }) + + test('should handle localhost with different cases', (t) => { + t.plan(3) + + t.assert.strictEqual(normalizeOrigin('http://LocalHost'), 'http://localhost') + t.assert.strictEqual(normalizeOrigin('http://LOCALHOST:3000'), 'http://localhost:3000') + t.assert.strictEqual(normalizeOrigin(new URL('http://LocalHost')), 'http://localhost') + }) + + test('should handle subdomains with mixed case', (t) => { + t.plan(3) + + t.assert.strictEqual(normalizeOrigin('http://Api.Example.Com'), 'http://api.example.com') + t.assert.strictEqual(normalizeOrigin('https://WWW.Example.COM'), 'https://www.example.com') + t.assert.strictEqual(normalizeOrigin(new URL('http://Sub.Domain.Example.Com')), 'http://sub.domain.example.com') + }) + + test('should handle paths in URL objects (should only normalize origin part)', (t) => { + t.plan(2) + + // URL objects with paths should still only return the origin + const url1 = new URL('http://Example.com/path/to/resource') + t.assert.strictEqual(normalizeOrigin(url1), 'http://example.com') + + const url2 = new URL('https://Api.Example.com:8080/api/v1') + t.assert.strictEqual(normalizeOrigin(url2), 'https://api.example.com:8080') + }) +}) From 41d1b1a7f7d257b2df943343dfa70d6c56ddb681 Mon Sep 17 00:00:00 2001 From: shubham Date: Thu, 8 Jan 2026 15:48:29 +0530 Subject: [PATCH 2/8] Refactor normalizeOrigin function to remove ignoreTrailingSlash parameter and simplify URL handling. Update MockAgent to utilize the new implementation for origin normalization. --- lib/mock/mock-agent.js | 28 +++++++++++----------------- lib/mock/mock-utils.js | 29 +++++++++++++++-------------- test/mock-utils.js | 16 ++++++++-------- 3 files changed, 34 insertions(+), 39 deletions(-) diff --git a/lib/mock/mock-agent.js b/lib/mock/mock-agent.js index ddf977883de..f3e68c8b0d1 100644 --- a/lib/mock/mock-agent.js +++ b/lib/mock/mock-agent.js @@ -57,7 +57,7 @@ class MockAgent extends Dispatcher { get (origin) { // Normalize origin to handle URL objects and case-insensitive hostnames - const originKey = normalizeOrigin(origin, this[kIgnoreTrailingSlash]) + const originKey = normalizeOrigin(origin) let dispatcher = this[kMockAgentGet](originKey) @@ -69,7 +69,7 @@ class MockAgent extends Dispatcher { } dispatch (opts, handler) { - const normalizedOrigin = normalizeOrigin(opts.origin, this[kIgnoreTrailingSlash]) + const normalizedOrigin = normalizeOrigin(opts.origin) // Call MockAgent.get to perform additional setup before dispatching as normal this.get(normalizedOrigin) @@ -167,8 +167,7 @@ class MockAgent extends Dispatcher { } [kMockAgentSet] (origin, dispatcher) { - const normalizedOrigin = normalizeOrigin(origin, this[kIgnoreTrailingSlash]) - this[kClients].set(normalizedOrigin, { count: 0, dispatcher }) + this[kClients].set(origin, { count: 0, dispatcher }) } [kFactory] (origin) { @@ -179,31 +178,26 @@ class MockAgent extends Dispatcher { } [kMockAgentGet] (origin) { - const normalizedOrigin = normalizeOrigin(origin, this[kIgnoreTrailingSlash]) - // First check if we can immediately find it with normalized origin - const result = this[kClients].get(normalizedOrigin) + const result = this[kClients].get(origin) if (result?.dispatcher) { return result.dispatcher } // If the origin is not a string create a dummy parent pool and return to user - if (typeof normalizedOrigin !== 'string') { + if (typeof origin !== 'string') { const dispatcher = this[kFactory]('http://localhost:9999') - this[kMockAgentSet](normalizedOrigin, dispatcher) + this[kMockAgentSet](origin, dispatcher) return dispatcher } // If we match, create a pool and assign the same dispatches for (const [keyMatcher, result] of Array.from(this[kClients])) { - if (result && typeof keyMatcher !== 'string') { - const normalizedKeyMatcher = normalizeOrigin(keyMatcher, this[kIgnoreTrailingSlash]) - if (matchValue(normalizedKeyMatcher, normalizedOrigin)) { - const dispatcher = this[kFactory](normalizedOrigin) - this[kMockAgentSet](normalizedOrigin, dispatcher) - dispatcher[kDispatches] = result.dispatcher[kDispatches] - return dispatcher - } + if (result && typeof keyMatcher !== 'string' && matchValue(keyMatcher, origin)) { + const dispatcher = this[kFactory](origin) + this[kMockAgentSet](origin, dispatcher) + dispatcher[kDispatches] = result.dispatcher[kDispatches] + return dispatcher } } } diff --git a/lib/mock/mock-utils.js b/lib/mock/mock-utils.js index 6f477b46514..0cf73e15b7e 100644 --- a/lib/mock/mock-utils.js +++ b/lib/mock/mock-utils.js @@ -396,26 +396,27 @@ function checkNetConnect (netConnect, origin) { return false } -function normalizeOrigin (origin, ignoreTrailingSlash = false) { +function normalizeOrigin (origin) { if (typeof origin !== 'string' && !(origin instanceof URL)) { return origin } - let originString = typeof origin === 'string' ? origin : origin.toString() - - try { - const url = new URL(originString) - url.hostname = url.hostname.toLowerCase() - originString = url.origin - } catch (err) { - return originString - } - - if (ignoreTrailingSlash && originString.endsWith('/')) { - originString = originString.slice(0, -1) + let url + if (origin instanceof URL) { + // Use the URL directly if it's already a URL instance + url = origin + } else { + // Parse string to URL + try { + url = new URL(origin) + } catch (err) { + return origin + } } - return originString + // Lowercase hostname (URL constructor preserves case, so we need to do this) + url.hostname = url.hostname.toLowerCase() + return url.origin } function buildAndValidateMockOptions (opts) { diff --git a/test/mock-utils.js b/test/mock-utils.js index 256dffd3d7f..945aaf34008 100644 --- a/test/mock-utils.js +++ b/test/mock-utils.js @@ -310,17 +310,17 @@ describe('normalizeOrigin', () => { test('should remove trailing slash when ignoreTrailingSlash is true', (t) => { t.plan(4) - t.assert.strictEqual(normalizeOrigin('http://example.com/', true), 'http://example.com') - t.assert.strictEqual(normalizeOrigin('https://example.com/', true), 'https://example.com') - t.assert.strictEqual(normalizeOrigin(new URL('http://example.com/'), true), 'http://example.com') - t.assert.strictEqual(normalizeOrigin('http://example.com', true), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('http://example.com/'), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('https://example.com/'), 'https://example.com') + t.assert.strictEqual(normalizeOrigin(new URL('http://example.com/')), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('http://example.com'), 'http://example.com') }) test('should not remove trailing slash when ignoreTrailingSlash is false', (t) => { t.plan(2) - t.assert.strictEqual(normalizeOrigin('http://example.com/', false), 'http://example.com') - t.assert.strictEqual(normalizeOrigin('http://example.com', false), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('http://example.com/'), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('http://example.com'), 'http://example.com') }) test('should return RegExp matchers as-is', (t) => { @@ -328,7 +328,7 @@ describe('normalizeOrigin', () => { const regex = /http:\/\/example\.com/ t.assert.strictEqual(normalizeOrigin(regex), regex) - t.assert.strictEqual(normalizeOrigin(regex, true), regex) + t.assert.strictEqual(normalizeOrigin(regex), regex) }) test('should return function matchers as-is', (t) => { @@ -336,7 +336,7 @@ describe('normalizeOrigin', () => { const fn = (origin) => origin === 'http://example.com' t.assert.strictEqual(normalizeOrigin(fn), fn) - t.assert.strictEqual(normalizeOrigin(fn, true), fn) + t.assert.strictEqual(normalizeOrigin(fn), fn) }) test('should return other non-string, non-URL types as-is', (t) => { From b2f9b6140a64f8091b15ec3eb1d08f640dd81252 Mon Sep 17 00:00:00 2001 From: shubham Date: Thu, 8 Jan 2026 15:55:02 +0530 Subject: [PATCH 3/8] Refactor MockAgent to directly assign normalized origin in dispatch method, improving clarity and consistency in origin handling. --- lib/mock/mock-agent.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/mock/mock-agent.js b/lib/mock/mock-agent.js index f3e68c8b0d1..f3f7d9dff0f 100644 --- a/lib/mock/mock-agent.js +++ b/lib/mock/mock-agent.js @@ -69,17 +69,16 @@ class MockAgent extends Dispatcher { } dispatch (opts, handler) { - const normalizedOrigin = normalizeOrigin(opts.origin) + opts.origin = normalizeOrigin(opts.origin) // Call MockAgent.get to perform additional setup before dispatching as normal - this.get(normalizedOrigin) + this.get(opts.origin) this[kMockAgentAddCallHistoryLog](opts) const acceptNonStandardSearchParameters = this[kMockAgentAcceptsNonStandardSearchParameters] const dispatchOpts = { ...opts } - dispatchOpts.origin = normalizedOrigin if (acceptNonStandardSearchParameters && dispatchOpts.path) { const [path, searchParams] = dispatchOpts.path.split('?') From 452dfb4782fed7acab29c6643febf5a74a4b8ced Mon Sep 17 00:00:00 2001 From: shubham Date: Thu, 8 Jan 2026 16:01:48 +0530 Subject: [PATCH 4/8] Refactor normalizeOrigin function to improve URL handling by directly returning the origin of a URL instance, simplifying the logic and enhancing readability. --- lib/mock/mock-utils.js | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/mock/mock-utils.js b/lib/mock/mock-utils.js index 0cf73e15b7e..573c2c0053c 100644 --- a/lib/mock/mock-utils.js +++ b/lib/mock/mock-utils.js @@ -403,19 +403,14 @@ function normalizeOrigin (origin) { let url if (origin instanceof URL) { - // Use the URL directly if it's already a URL instance - url = origin - } else { - // Parse string to URL - try { - url = new URL(origin) - } catch (err) { - return origin - } + return origin.origin } - // Lowercase hostname (URL constructor preserves case, so we need to do this) - url.hostname = url.hostname.toLowerCase() + try { + url = new URL(origin) + } catch (err) { + return origin + } return url.origin } From 1ba094a6d0baacfa278f48f59c44c4a7e769b521 Mon Sep 17 00:00:00 2001 From: shubham Date: Thu, 8 Jan 2026 16:12:23 +0530 Subject: [PATCH 5/8] Enhance normalizeOrigin function to accept an ignoreTrailingSlash parameter, allowing for flexible origin normalization. Update MockAgent methods to utilize the new parameter for improved origin handling. --- lib/mock/mock-agent.js | 4 ++-- lib/mock/mock-utils.js | 11 ++++++----- test/mock-utils.js | 16 ++++++++-------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/lib/mock/mock-agent.js b/lib/mock/mock-agent.js index f3f7d9dff0f..b70e8341057 100644 --- a/lib/mock/mock-agent.js +++ b/lib/mock/mock-agent.js @@ -57,7 +57,7 @@ class MockAgent extends Dispatcher { get (origin) { // Normalize origin to handle URL objects and case-insensitive hostnames - const originKey = normalizeOrigin(origin) + const originKey = normalizeOrigin(origin, this[kIgnoreTrailingSlash]) let dispatcher = this[kMockAgentGet](originKey) @@ -69,7 +69,7 @@ class MockAgent extends Dispatcher { } dispatch (opts, handler) { - opts.origin = normalizeOrigin(opts.origin) + opts.origin = normalizeOrigin(opts.origin, this[kIgnoreTrailingSlash]) // Call MockAgent.get to perform additional setup before dispatching as normal this.get(opts.origin) diff --git a/lib/mock/mock-utils.js b/lib/mock/mock-utils.js index 573c2c0053c..9ed280d9dc8 100644 --- a/lib/mock/mock-utils.js +++ b/lib/mock/mock-utils.js @@ -396,22 +396,23 @@ function checkNetConnect (netConnect, origin) { return false } -function normalizeOrigin (origin) { +function normalizeOrigin (origin, ignoreTrailingSlash = false) { if (typeof origin !== 'string' && !(origin instanceof URL)) { return origin } - let url + let originString + if (origin instanceof URL) { - return origin.origin + originString = origin.origin } try { - url = new URL(origin) + originString = new URL(origin).origin } catch (err) { return origin } - return url.origin + return ignoreTrailingSlash ? originString.replace(/\/$/, '') : originString } function buildAndValidateMockOptions (opts) { diff --git a/test/mock-utils.js b/test/mock-utils.js index 945aaf34008..256dffd3d7f 100644 --- a/test/mock-utils.js +++ b/test/mock-utils.js @@ -310,17 +310,17 @@ describe('normalizeOrigin', () => { test('should remove trailing slash when ignoreTrailingSlash is true', (t) => { t.plan(4) - t.assert.strictEqual(normalizeOrigin('http://example.com/'), 'http://example.com') - t.assert.strictEqual(normalizeOrigin('https://example.com/'), 'https://example.com') - t.assert.strictEqual(normalizeOrigin(new URL('http://example.com/')), 'http://example.com') - t.assert.strictEqual(normalizeOrigin('http://example.com'), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('http://example.com/', true), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('https://example.com/', true), 'https://example.com') + t.assert.strictEqual(normalizeOrigin(new URL('http://example.com/'), true), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('http://example.com', true), 'http://example.com') }) test('should not remove trailing slash when ignoreTrailingSlash is false', (t) => { t.plan(2) - t.assert.strictEqual(normalizeOrigin('http://example.com/'), 'http://example.com') - t.assert.strictEqual(normalizeOrigin('http://example.com'), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('http://example.com/', false), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('http://example.com', false), 'http://example.com') }) test('should return RegExp matchers as-is', (t) => { @@ -328,7 +328,7 @@ describe('normalizeOrigin', () => { const regex = /http:\/\/example\.com/ t.assert.strictEqual(normalizeOrigin(regex), regex) - t.assert.strictEqual(normalizeOrigin(regex), regex) + t.assert.strictEqual(normalizeOrigin(regex, true), regex) }) test('should return function matchers as-is', (t) => { @@ -336,7 +336,7 @@ describe('normalizeOrigin', () => { const fn = (origin) => origin === 'http://example.com' t.assert.strictEqual(normalizeOrigin(fn), fn) - t.assert.strictEqual(normalizeOrigin(fn), fn) + t.assert.strictEqual(normalizeOrigin(fn, true), fn) }) test('should return other non-string, non-URL types as-is', (t) => { From adcf39182ef5d95f24ce4e8c5de093e1b6b9c658 Mon Sep 17 00:00:00 2001 From: shubham Date: Fri, 9 Jan 2026 09:13:00 +0530 Subject: [PATCH 6/8] remove ignoreTrailingSlash parameter from normalizeOrigin --- lib/mock/mock-agent.js | 6 +++--- lib/mock/mock-utils.js | 9 ++++----- test/mock-utils.js | 18 ++++++++---------- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/lib/mock/mock-agent.js b/lib/mock/mock-agent.js index b70e8341057..be43edefdf5 100644 --- a/lib/mock/mock-agent.js +++ b/lib/mock/mock-agent.js @@ -57,7 +57,7 @@ class MockAgent extends Dispatcher { get (origin) { // Normalize origin to handle URL objects and case-insensitive hostnames - const originKey = normalizeOrigin(origin, this[kIgnoreTrailingSlash]) + const originKey = normalizeOrigin(origin) let dispatcher = this[kMockAgentGet](originKey) @@ -69,7 +69,7 @@ class MockAgent extends Dispatcher { } dispatch (opts, handler) { - opts.origin = normalizeOrigin(opts.origin, this[kIgnoreTrailingSlash]) + opts.origin = normalizeOrigin(opts.origin) // Call MockAgent.get to perform additional setup before dispatching as normal this.get(opts.origin) @@ -177,7 +177,7 @@ class MockAgent extends Dispatcher { } [kMockAgentGet] (origin) { - // First check if we can immediately find it with normalized origin + // First check if we can immediately find it const result = this[kClients].get(origin) if (result?.dispatcher) { return result.dispatcher diff --git a/lib/mock/mock-utils.js b/lib/mock/mock-utils.js index 9ed280d9dc8..a1f10f427b9 100644 --- a/lib/mock/mock-utils.js +++ b/lib/mock/mock-utils.js @@ -396,23 +396,22 @@ function checkNetConnect (netConnect, origin) { return false } -function normalizeOrigin (origin, ignoreTrailingSlash = false) { +function normalizeOrigin (origin) { if (typeof origin !== 'string' && !(origin instanceof URL)) { return origin } - let originString - if (origin instanceof URL) { - originString = origin.origin + return origin.origin } + let originString try { originString = new URL(origin).origin } catch (err) { return origin } - return ignoreTrailingSlash ? originString.replace(/\/$/, '') : originString + return originString.replace(/\/$/, '') } function buildAndValidateMockOptions (opts) { diff --git a/test/mock-utils.js b/test/mock-utils.js index 256dffd3d7f..586c1196c58 100644 --- a/test/mock-utils.js +++ b/test/mock-utils.js @@ -310,33 +310,31 @@ describe('normalizeOrigin', () => { test('should remove trailing slash when ignoreTrailingSlash is true', (t) => { t.plan(4) - t.assert.strictEqual(normalizeOrigin('http://example.com/', true), 'http://example.com') - t.assert.strictEqual(normalizeOrigin('https://example.com/', true), 'https://example.com') - t.assert.strictEqual(normalizeOrigin(new URL('http://example.com/'), true), 'http://example.com') - t.assert.strictEqual(normalizeOrigin('http://example.com', true), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('http://example.com/'), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('https://example.com/'), 'https://example.com') + t.assert.strictEqual(normalizeOrigin(new URL('http://example.com/')), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('http://example.com'), 'http://example.com') }) test('should not remove trailing slash when ignoreTrailingSlash is false', (t) => { t.plan(2) - t.assert.strictEqual(normalizeOrigin('http://example.com/', false), 'http://example.com') - t.assert.strictEqual(normalizeOrigin('http://example.com', false), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('http://example.com/'), 'http://example.com') + t.assert.strictEqual(normalizeOrigin('http://example.com'), 'http://example.com') }) test('should return RegExp matchers as-is', (t) => { - t.plan(2) + t.plan(1) const regex = /http:\/\/example\.com/ t.assert.strictEqual(normalizeOrigin(regex), regex) - t.assert.strictEqual(normalizeOrigin(regex, true), regex) }) test('should return function matchers as-is', (t) => { - t.plan(2) + t.plan(1) const fn = (origin) => origin === 'http://example.com' t.assert.strictEqual(normalizeOrigin(fn), fn) - t.assert.strictEqual(normalizeOrigin(fn, true), fn) }) test('should return other non-string, non-URL types as-is', (t) => { From d7de6f4009194f5d66455b7b5a80ae8e1cbf1da3 Mon Sep 17 00:00:00 2001 From: shubham Date: Fri, 9 Jan 2026 22:18:35 +0530 Subject: [PATCH 7/8] simplify normalize origin to use string.toLowerCase to keep the trailing slagh behavior consistent --- lib/mock/mock-agent.js | 3 ++- lib/mock/mock-utils.js | 8 +------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/mock/mock-agent.js b/lib/mock/mock-agent.js index be43edefdf5..61449e077ea 100644 --- a/lib/mock/mock-agent.js +++ b/lib/mock/mock-agent.js @@ -57,7 +57,8 @@ class MockAgent extends Dispatcher { get (origin) { // Normalize origin to handle URL objects and case-insensitive hostnames - const originKey = normalizeOrigin(origin) + const normalizedOrigin = normalizeOrigin(origin) + const originKey = this[kIgnoreTrailingSlash] ? normalizedOrigin.replace(/\/$/, '') : normalizedOrigin let dispatcher = this[kMockAgentGet](originKey) diff --git a/lib/mock/mock-utils.js b/lib/mock/mock-utils.js index a1f10f427b9..e1e3f040643 100644 --- a/lib/mock/mock-utils.js +++ b/lib/mock/mock-utils.js @@ -405,13 +405,7 @@ function normalizeOrigin (origin) { return origin.origin } - let originString - try { - originString = new URL(origin).origin - } catch (err) { - return origin - } - return originString.replace(/\/$/, '') + return origin.toLowerCase() } function buildAndValidateMockOptions (opts) { From 8b8cfb327d5a8dfa729fdc976af3339c897c2859 Mon Sep 17 00:00:00 2001 From: shubham Date: Mon, 12 Jan 2026 17:08:51 +0530 Subject: [PATCH 8/8] update tests for mock agent utils. Cleanup the IgnoreTrailingSlash parameter in normalizeOrigin unit tests from mockutils.js --- test/mock-utils.js | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/test/mock-utils.js b/test/mock-utils.js index 586c1196c58..873ac84014a 100644 --- a/test/mock-utils.js +++ b/test/mock-utils.js @@ -290,39 +290,13 @@ describe('normalizeOrigin', () => { }) test('should preserve port numbers', (t) => { - t.plan(4) + t.plan(3) t.assert.strictEqual(normalizeOrigin('http://Example.com:8080'), 'http://example.com:8080') - // Note: url.origin omits default ports (443 for HTTPS, 80 for HTTP) - t.assert.strictEqual(normalizeOrigin('https://Api.Example.com:443'), 'https://api.example.com') t.assert.strictEqual(normalizeOrigin(new URL('http://Example.com:3000')), 'http://example.com:3000') t.assert.strictEqual(normalizeOrigin(new URL('https://Test.com:8443')), 'https://test.com:8443') }) - test('should handle default ports correctly', (t) => { - t.plan(2) - - // Default ports should be omitted from origin - t.assert.strictEqual(normalizeOrigin('http://Example.com:80'), 'http://example.com') - t.assert.strictEqual(normalizeOrigin('https://Example.com:443'), 'https://example.com') - }) - - test('should remove trailing slash when ignoreTrailingSlash is true', (t) => { - t.plan(4) - - t.assert.strictEqual(normalizeOrigin('http://example.com/'), 'http://example.com') - t.assert.strictEqual(normalizeOrigin('https://example.com/'), 'https://example.com') - t.assert.strictEqual(normalizeOrigin(new URL('http://example.com/')), 'http://example.com') - t.assert.strictEqual(normalizeOrigin('http://example.com'), 'http://example.com') - }) - - test('should not remove trailing slash when ignoreTrailingSlash is false', (t) => { - t.plan(2) - - t.assert.strictEqual(normalizeOrigin('http://example.com/'), 'http://example.com') - t.assert.strictEqual(normalizeOrigin('http://example.com'), 'http://example.com') - }) - test('should return RegExp matchers as-is', (t) => { t.plan(1)