From c101e4e2cc5bd5d9c564736e3f992f6d09abcd24 Mon Sep 17 00:00:00 2001 From: Felix Ezama-Vaughan Date: Sun, 31 Aug 2025 11:55:09 -0600 Subject: [PATCH 01/13] init --- lib/interceptor/dns.js | 42 ++++++++++++++++++-- test/interceptors/dns.js | 86 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 4 deletions(-) diff --git a/lib/interceptor/dns.js b/lib/interceptor/dns.js index 38287607143..c183280c157 100644 --- a/lib/interceptor/dns.js +++ b/lib/interceptor/dns.js @@ -5,6 +5,43 @@ const DecoratorHandler = require('../handler/decorator-handler') const { InvalidArgumentError, InformationalError } = require('../core/errors') const maxInt = Math.pow(2, 31) - 1 +function addHostHeader (headers, host) { + if (headers == null) return { host } + + if (Array.isArray(headers)) { + // Distinguish flat internal array vs array-of-pairs + if (headers.length > 0 && Array.isArray(headers[0])) { + let hasHost = false + for (const [k] of headers) { + if (k != null && String(k).toLowerCase() === 'host') { hasHost = true; break } + } + return hasHost ? headers : [['host', host], ...headers] + } + + for (let i = 0; i < headers.length; i += 2) { + if (headers[i] != null && String(headers[i]).toLowerCase() === 'host') return headers + } + return ['host', host, ...headers] + } + + if (headers && typeof headers[Symbol.iterator] === 'function') { + const pairs = [] + let hasHost = false + for (const [k, v] of headers) { + if (k != null) { + if (String(k).toLowerCase() === 'host') hasHost = true + pairs.push([k, v]) + } + } + return hasHost ? pairs : [['host', host], ...pairs] + } + + for (const k of Object.keys(headers)) { + if (String(k).toLowerCase() === 'host') return headers + } + return { host, ...headers } +} + class DNSInstance { #maxTTL = 0 #maxItems = 0 @@ -411,10 +448,7 @@ module.exports = interceptorOpts => { ...origDispatchOpts, servername: origin.hostname, // For SNI on TLS origin: newOrigin.origin, - headers: { - host: origin.host, - ...origDispatchOpts.headers - } + headers: addHostHeader(origDispatchOpts.headers, origin.host) } dispatch( diff --git a/test/interceptors/dns.js b/test/interceptors/dns.js index 3ee48c20973..5baa759d4cf 100644 --- a/test/interceptors/dns.js +++ b/test/interceptors/dns.js @@ -1936,3 +1936,89 @@ test('#3951 - Should handle lookup errors correctly', async t => { origin: 'http://localhost' }), new Error('lookup error')) }) + +test('Headers iterable-of-pairs should work with DNS interceptor', async t => { + t = tspl(t, { plan: 3 }) + + const server = createServer({ joinDuplicateHeaders: true }) + + server.on('request', (req, res) => { + t.equal(req.headers.foo, 'bar') + t.match(req.headers.host, /^localhost:\d+$/) + res.end('ok') + }) + + server.listen(0) + await once(server, 'listening') + + const { cache: cacheInterceptor, dns: dnsInterceptor } = interceptors + + const agent = new Agent().compose([ + cacheInterceptor(), + dnsInterceptor({ + lookup: (_origin, _opts, cb) => { + cb(null, [ + { address: '127.0.0.1', family: 4 } + ]) + } + }) + ]) + + const origin = `http://localhost:${server.address().port}` + const headersIterable = [['foo', 'bar']] + + const r = await agent.request({ + origin, + path: '/', + method: 'GET', + headers: headersIterable + }) + t.equal(r.statusCode, 200) + await r.body.text() + + server.close() + await once(server, 'close') + await agent.close() +}) + +test('Headers object should work with DNS interceptor', async t => { + t = tspl(t, { plan: 3 }) + + const server = createServer({ joinDuplicateHeaders: true }) + + server.on('request', (req, res) => { + t.equal(req.headers.foo, 'bar') + t.match(req.headers.host, /^localhost:\d+$/) + res.end('ok') + }) + + server.listen(0) + await once(server, 'listening') + + const { dns: dnsInterceptor } = interceptors + const client = new Agent().compose([ + dnsInterceptor({ + lookup: (_origin, _opts, cb) => { + cb(null, [ + { address: '127.0.0.1', family: 4 } + ]) + } + }) + ]) + + const origin = `http://localhost:${server.address().port}` + const headersRecord = { foo: 'bar' } + + const r = await client.request({ + origin, + path: '/', + method: 'GET', + headers: headersRecord + }) + t.equal(r.statusCode, 200) + await r.body.text() + + server.close() + await once(server, 'close') + await client.close() +}) From 2b494d3b8bde42e75d7721e37cc557cc3d216909 Mon Sep 17 00:00:00 2001 From: Felix Ezama-Vaughan Date: Sun, 31 Aug 2025 14:51:01 -0600 Subject: [PATCH 02/13] minor --- lib/interceptor/dns.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/interceptor/dns.js b/lib/interceptor/dns.js index c183280c157..e134b891780 100644 --- a/lib/interceptor/dns.js +++ b/lib/interceptor/dns.js @@ -33,7 +33,8 @@ function addHostHeader (headers, host) { pairs.push([k, v]) } } - return hasHost ? pairs : [['host', host], ...pairs] + const withHost = hasHost ? pairs : [['host', host], ...pairs] + return new Map(withHost) } for (const k of Object.keys(headers)) { From caba0e1f81c12a853cd4652cced2d0b80ba99ad9 Mon Sep 17 00:00:00 2001 From: Felix Ezama-Vaughan Date: Sun, 31 Aug 2025 16:40:06 -0600 Subject: [PATCH 03/13] testing --- lib/interceptor/dns.js | 3 +- test/interceptors/dns.js | 115 +++++++++++++++++---------------------- 2 files changed, 51 insertions(+), 67 deletions(-) diff --git a/lib/interceptor/dns.js b/lib/interceptor/dns.js index e134b891780..e0d9f5f6ed7 100644 --- a/lib/interceptor/dns.js +++ b/lib/interceptor/dns.js @@ -33,8 +33,7 @@ function addHostHeader (headers, host) { pairs.push([k, v]) } } - const withHost = hasHost ? pairs : [['host', host], ...pairs] - return new Map(withHost) + return (hasHost ? pairs : [['host', host], ...pairs]).values() } for (const k of Object.keys(headers)) { diff --git a/test/interceptors/dns.js b/test/interceptors/dns.js index 5baa759d4cf..d1bf55197bd 100644 --- a/test/interceptors/dns.js +++ b/test/interceptors/dns.js @@ -1937,13 +1937,11 @@ test('#3951 - Should handle lookup errors correctly', async t => { }), new Error('lookup error')) }) -test('Headers iterable-of-pairs should work with DNS interceptor', async t => { - t = tspl(t, { plan: 3 }) - +test('Various (parameterized) header shapes should work with DNS interceptor', async t => { const server = createServer({ joinDuplicateHeaders: true }) - server.on('request', (req, res) => { t.equal(req.headers.foo, 'bar') + t.equal(typeof req.headers['0'], 'undefined') t.match(req.headers.host, /^localhost:\d+$/) res.end('ok') }) @@ -1951,74 +1949,61 @@ test('Headers iterable-of-pairs should work with DNS interceptor', async t => { server.listen(0) await once(server, 'listening') - const { cache: cacheInterceptor, dns: dnsInterceptor } = interceptors - - const agent = new Agent().compose([ - cacheInterceptor(), - dnsInterceptor({ - lookup: (_origin, _opts, cb) => { - cb(null, [ - { address: '127.0.0.1', family: 4 } - ]) - } - }) - ]) - const origin = `http://localhost:${server.address().port}` - const headersIterable = [['foo', 'bar']] - const r = await agent.request({ - origin, - path: '/', - method: 'GET', - headers: headersIterable - }) - t.equal(r.statusCode, 200) - await r.body.text() - - server.close() - await once(server, 'close') - await agent.close() -}) - -test('Headers object should work with DNS interceptor', async t => { - t = tspl(t, { plan: 3 }) - - const server = createServer({ joinDuplicateHeaders: true }) - - server.on('request', (req, res) => { - t.equal(req.headers.foo, 'bar') - t.match(req.headers.host, /^localhost:\d+$/) - res.end('ok') - }) + const { cache: cacheInterceptor, dns: dnsInterceptor } = interceptors - server.listen(0) - await once(server, 'listening') + function * genPairs () { yield ['foo', 'bar'] } - const { dns: dnsInterceptor } = interceptors - const client = new Agent().compose([ - dnsInterceptor({ - lookup: (_origin, _opts, cb) => { - cb(null, [ - { address: '127.0.0.1', family: 4 } - ]) - } - }) - ]) + const cases = [ + { + name: 'record', + headers: { foo: 'bar' }, + interceptors: [dnsInterceptor()] + }, + { + name: 'flat array', + headers: ['foo', 'bar'], + interceptors: [dnsInterceptor()] + }, + { + name: 'record with multi-value', + headers: { foo: ['bar'] }, + interceptors: [dnsInterceptor()] + }, + { + name: 'iterable generator of pairs', + headers: genPairs(), + interceptors: [cacheInterceptor(), dnsInterceptor({ + lookup: (_origin, _opts, cb) => cb(null, [{ address: '127.0.0.1', family: 4 }]) + })] + }, + { + name: 'set of pairs', + headers: new Set([['foo', 'bar']]), + interceptors: [cacheInterceptor(), dnsInterceptor({ + lookup: (_origin, _opts, cb) => cb(null, [{ address: '127.0.0.1', family: 4 }]) + })] + }, + { + name: 'map of pairs (single)', + headers: new Map([['foo', 'bar']]), + interceptors: [cacheInterceptor(), dnsInterceptor({ + lookup: (_origin, _opts, cb) => cb(null, [{ address: '127.0.0.1', family: 4 }]) + })] + } + ] - const origin = `http://localhost:${server.address().port}` - const headersRecord = { foo: 'bar' } + t = tspl(t, { plan: cases.length * 4 }) - const r = await client.request({ - origin, - path: '/', - method: 'GET', - headers: headersRecord - }) - t.equal(r.statusCode, 200) - await r.body.text() + for (const c of cases) { + const agent = new Agent().compose(c.interceptors) + const r = await agent.request({ origin, path: '/', method: 'GET', headers: c.headers }) + t.equal(r.statusCode, 200, c.name) + await r.body.text() + await agent.close() + } server.close() await once(server, 'close') - await client.close() }) From fbec0a01c27266da1214ca329335817f80168952 Mon Sep 17 00:00:00 2001 From: Felix Ezama-Vaughan Date: Mon, 1 Sep 2025 20:39:23 -0600 Subject: [PATCH 04/13] save prog --- lib/interceptor/dns.js | 36 +++++------------------------------- test/interceptors/dns.js | 26 +++++--------------------- 2 files changed, 10 insertions(+), 52 deletions(-) diff --git a/lib/interceptor/dns.js b/lib/interceptor/dns.js index e0d9f5f6ed7..c724fc402df 100644 --- a/lib/interceptor/dns.js +++ b/lib/interceptor/dns.js @@ -5,39 +5,13 @@ const DecoratorHandler = require('../handler/decorator-handler') const { InvalidArgumentError, InformationalError } = require('../core/errors') const maxInt = Math.pow(2, 31) - 1 -function addHostHeader (headers, host) { - if (headers == null) return { host } - +function addHostHeader (headers = {}, host) { if (Array.isArray(headers)) { - // Distinguish flat internal array vs array-of-pairs - if (headers.length > 0 && Array.isArray(headers[0])) { - let hasHost = false - for (const [k] of headers) { - if (k != null && String(k).toLowerCase() === 'host') { hasHost = true; break } - } - return hasHost ? headers : [['host', host], ...headers] - } - - for (let i = 0; i < headers.length; i += 2) { - if (headers[i] != null && String(headers[i]).toLowerCase() === 'host') return headers - } - return ['host', host, ...headers] - } - - if (headers && typeof headers[Symbol.iterator] === 'function') { - const pairs = [] - let hasHost = false - for (const [k, v] of headers) { - if (k != null) { - if (String(k).toLowerCase() === 'host') hasHost = true - pairs.push([k, v]) - } + const header = ['host', host] + if (Array.isArray(headers[0])) { + return [header, ...headers] } - return (hasHost ? pairs : [['host', host], ...pairs]).values() - } - - for (const k of Object.keys(headers)) { - if (String(k).toLowerCase() === 'host') return headers + return [...header, ...headers] } return { host, ...headers } } diff --git a/test/interceptors/dns.js b/test/interceptors/dns.js index d1bf55197bd..88a070e7c27 100644 --- a/test/interceptors/dns.js +++ b/test/interceptors/dns.js @@ -1956,6 +1956,11 @@ test('Various (parameterized) header shapes should work with DNS interceptor', a function * genPairs () { yield ['foo', 'bar'] } const cases = [ + { + name: 'array of pairs', + headers: [['foo', 'bar']], + interceptors: [dnsInterceptor()] + }, { name: 'record', headers: { foo: 'bar' }, @@ -1970,27 +1975,6 @@ test('Various (parameterized) header shapes should work with DNS interceptor', a name: 'record with multi-value', headers: { foo: ['bar'] }, interceptors: [dnsInterceptor()] - }, - { - name: 'iterable generator of pairs', - headers: genPairs(), - interceptors: [cacheInterceptor(), dnsInterceptor({ - lookup: (_origin, _opts, cb) => cb(null, [{ address: '127.0.0.1', family: 4 }]) - })] - }, - { - name: 'set of pairs', - headers: new Set([['foo', 'bar']]), - interceptors: [cacheInterceptor(), dnsInterceptor({ - lookup: (_origin, _opts, cb) => cb(null, [{ address: '127.0.0.1', family: 4 }]) - })] - }, - { - name: 'map of pairs (single)', - headers: new Map([['foo', 'bar']]), - interceptors: [cacheInterceptor(), dnsInterceptor({ - lookup: (_origin, _opts, cb) => cb(null, [{ address: '127.0.0.1', family: 4 }]) - })] } ] From d487ad0086dc11e30e1b88754ecc03533fcaae82 Mon Sep 17 00:00:00 2001 From: Felix Ezama-Vaughan Date: Mon, 1 Sep 2025 21:33:23 -0600 Subject: [PATCH 05/13] save prog --- test/interceptors/decompress.js | 1 + test/interceptors/dns.js | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/test/interceptors/decompress.js b/test/interceptors/decompress.js index e0737e400c3..05f16b59e86 100644 --- a/test/interceptors/decompress.js +++ b/test/interceptors/decompress.js @@ -38,6 +38,7 @@ test('should decompress gzip response', async t => { }) const response = await client.request({ + headers: [['foo', 'bar']], method: 'GET', path: '/' }) diff --git a/test/interceptors/dns.js b/test/interceptors/dns.js index 88a070e7c27..d28fb3bbe2a 100644 --- a/test/interceptors/dns.js +++ b/test/interceptors/dns.js @@ -1953,28 +1953,29 @@ test('Various (parameterized) header shapes should work with DNS interceptor', a const { cache: cacheInterceptor, dns: dnsInterceptor } = interceptors - function * genPairs () { yield ['foo', 'bar'] } - + // will expand const cases = [ { name: 'array of pairs', headers: [['foo', 'bar']], - interceptors: [dnsInterceptor()] + interceptors: [cacheInterceptor(), dnsInterceptor()] }, { name: 'record', headers: { foo: 'bar' }, - interceptors: [dnsInterceptor()] + interceptors: [cacheInterceptor(), dnsInterceptor()] }, { name: 'flat array', headers: ['foo', 'bar'], + // note: cacheInterceptor cannot accept flat arrays as it calls normalizeHeaders + // possibly need to fix interceptors: [dnsInterceptor()] }, { - name: 'record with multi-value', + // name: 'record with multi-value', headers: { foo: ['bar'] }, - interceptors: [dnsInterceptor()] + interceptors: [cacheInterceptor(), dnsInterceptor()] } ] From 781c8c6d6574dccbffcded37aae9f91d22408e56 Mon Sep 17 00:00:00 2001 From: Felix Ezama-Vaughan Date: Mon, 1 Sep 2025 22:45:38 -0600 Subject: [PATCH 06/13] revert unneeded change --- test/interceptors/decompress.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/interceptors/decompress.js b/test/interceptors/decompress.js index 05f16b59e86..e0737e400c3 100644 --- a/test/interceptors/decompress.js +++ b/test/interceptors/decompress.js @@ -38,7 +38,6 @@ test('should decompress gzip response', async t => { }) const response = await client.request({ - headers: [['foo', 'bar']], method: 'GET', path: '/' }) From 84c723d18d5153f40f99b3ca61269a29e6b658de Mon Sep 17 00:00:00 2001 From: Felix Ezama-Vaughan Date: Wed, 3 Sep 2025 23:01:46 -0600 Subject: [PATCH 07/13] moved header normalization logic, used in dns.js --- lib/core/util.js | 34 ++++++++++++++++++++++++++++++++++ lib/interceptor/cache.js | 3 ++- lib/interceptor/dns.js | 14 ++++++-------- lib/util/cache.js | 34 ---------------------------------- test/interceptors/dns.js | 29 ++++++++++++----------------- 5 files changed, 54 insertions(+), 60 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index c10a382101f..7fc95e82e75 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -904,6 +904,39 @@ const normalizedMethodRecords = { Object.setPrototypeOf(normalizedMethodRecordsBase, null) Object.setPrototypeOf(normalizedMethodRecords, null) +/** + * @param {Record} + * @returns {Record} + */ +function normalizeHeaders (opts) { + let headers + if (opts.headers == null) { + headers = {} + } else if (typeof opts.headers[Symbol.iterator] === 'function') { + headers = {} + for (const x of opts.headers) { + if (!Array.isArray(x)) { + throw new Error('opts.headers is not a valid header map') + } + const [key, val] = x + if (typeof key !== 'string' || typeof val !== 'string') { + throw new Error('opts.headers is not a valid header map') + } + headers[key.toLowerCase()] = val + } + } else if (typeof opts.headers === 'object') { + headers = {} + + for (const key of Object.keys(opts.headers)) { + headers[key.toLowerCase()] = opts.headers[key] + } + } else { + throw new Error('opts.headers is not an object') + } + + return headers +} + module.exports = { kEnumerableProperty, isDisturbed, @@ -939,6 +972,7 @@ module.exports = { isValidHeaderValue, isTokenCharCode, parseRangeHeader, + normalizeHeaders, normalizedMethodRecordsBase, normalizedMethodRecords, isValidPort, diff --git a/lib/interceptor/cache.js b/lib/interceptor/cache.js index 6565baf0a51..1d385156f4a 100644 --- a/lib/interceptor/cache.js +++ b/lib/interceptor/cache.js @@ -6,7 +6,8 @@ const util = require('../core/util') const CacheHandler = require('../handler/cache-handler') const MemoryCacheStore = require('../cache/memory-cache-store') const CacheRevalidationHandler = require('../handler/cache-revalidation-handler') -const { assertCacheStore, assertCacheMethods, makeCacheKey, normalizeHeaders, parseCacheControlHeader } = require('../util/cache.js') +const { assertCacheStore, assertCacheMethods, makeCacheKey, parseCacheControlHeader } = require('../util/cache.js') +const { normalizeHeaders } = require('../core/util.js') const { AbortError } = require('../core/errors.js') /** diff --git a/lib/interceptor/dns.js b/lib/interceptor/dns.js index c724fc402df..5878780c9b0 100644 --- a/lib/interceptor/dns.js +++ b/lib/interceptor/dns.js @@ -4,15 +4,13 @@ const { lookup } = require('node:dns') const DecoratorHandler = require('../handler/decorator-handler') const { InvalidArgumentError, InformationalError } = require('../core/errors') const maxInt = Math.pow(2, 31) - 1 +const { normalizeHeaders } = require('../core/util.js') -function addHostHeader (headers = {}, host) { - if (Array.isArray(headers)) { - const header = ['host', host] - if (Array.isArray(headers[0])) { - return [header, ...headers] - } - return [...header, ...headers] +function addHostHeader (opts, host) { + if (Array.isArray(opts.headers)) { + return ['host', host, ...opts.headers] } + const headers = normalizeHeaders(opts) return { host, ...headers } } @@ -422,7 +420,7 @@ module.exports = interceptorOpts => { ...origDispatchOpts, servername: origin.hostname, // For SNI on TLS origin: newOrigin.origin, - headers: addHostHeader(origDispatchOpts.headers, origin.host) + headers: addHostHeader(origDispatchOpts, origin.host) } dispatch( diff --git a/lib/util/cache.js b/lib/util/cache.js index a05530f783b..95f701c81a7 100644 --- a/lib/util/cache.js +++ b/lib/util/cache.js @@ -29,39 +29,6 @@ function makeCacheKey (opts) { } } -/** - * @param {Record} - * @returns {Record} - */ -function normalizeHeaders (opts) { - let headers - if (opts.headers == null) { - headers = {} - } else if (typeof opts.headers[Symbol.iterator] === 'function') { - headers = {} - for (const x of opts.headers) { - if (!Array.isArray(x)) { - throw new Error('opts.headers is not a valid header map') - } - const [key, val] = x - if (typeof key !== 'string' || typeof val !== 'string') { - throw new Error('opts.headers is not a valid header map') - } - headers[key.toLowerCase()] = val - } - } else if (typeof opts.headers === 'object') { - headers = {} - - for (const key of Object.keys(opts.headers)) { - headers[key.toLowerCase()] = opts.headers[key] - } - } else { - throw new Error('opts.headers is not an object') - } - - return headers -} - /** * @param {any} key */ @@ -366,7 +333,6 @@ function assertCacheMethods (methods, name = 'CacheMethods') { module.exports = { makeCacheKey, - normalizeHeaders, assertCacheKey, assertCacheValue, parseCacheControlHeader, diff --git a/test/interceptors/dns.js b/test/interceptors/dns.js index d28fb3bbe2a..950417e3bc1 100644 --- a/test/interceptors/dns.js +++ b/test/interceptors/dns.js @@ -1951,38 +1951,33 @@ test('Various (parameterized) header shapes should work with DNS interceptor', a const origin = `http://localhost:${server.address().port}` - const { cache: cacheInterceptor, dns: dnsInterceptor } = interceptors - - // will expand const cases = [ { - name: 'array of pairs', - headers: [['foo', 'bar']], - interceptors: [cacheInterceptor(), dnsInterceptor()] + name: 'flat array', + headers: ['foo', 'bar'] }, { name: 'record', - headers: { foo: 'bar' }, - interceptors: [cacheInterceptor(), dnsInterceptor()] + headers: { foo: 'bar' } }, { - name: 'flat array', - headers: ['foo', 'bar'], - // note: cacheInterceptor cannot accept flat arrays as it calls normalizeHeaders - // possibly need to fix - interceptors: [dnsInterceptor()] + name: 'record with multi-value', + headers: { foo: ['bar'] } + }, + { + name: 'iterable (map) object', + headers: new Map([['foo', 'bar']]) }, { - // name: 'record with multi-value', - headers: { foo: ['bar'] }, - interceptors: [cacheInterceptor(), dnsInterceptor()] + name: 'iterable (set) object', + headers: new Set([['foo', 'bar']]) } ] t = tspl(t, { plan: cases.length * 4 }) for (const c of cases) { - const agent = new Agent().compose(c.interceptors) + const agent = new Agent().compose(dns()) const r = await agent.request({ origin, path: '/', method: 'GET', headers: c.headers }) t.equal(r.statusCode, 200, c.name) await r.body.text() From d8584bad2329922c2a01b36301eb18bfb5f6c0ec Mon Sep 17 00:00:00 2001 From: FelixVaughan Date: Thu, 4 Sep 2025 16:35:49 -0600 Subject: [PATCH 08/13] modified normalizeHeaders to account for arrays --- lib/core/util.js | 40 ++++++++++++++++++++++------------------ lib/interceptor/dns.js | 10 +--------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index 7fc95e82e75..66a5a88e4f0 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -909,32 +909,36 @@ Object.setPrototypeOf(normalizedMethodRecords, null) * @returns {Record} */ function normalizeHeaders (opts) { - let headers - if (opts.headers == null) { - headers = {} - } else if (typeof opts.headers[Symbol.iterator] === 'function') { - headers = {} - for (const x of opts.headers) { - if (!Array.isArray(x)) { - throw new Error('opts.headers is not a valid header map') - } - const [key, val] = x + const headers = {} + const src = opts.headers + + if (src == null) return headers + + if (Array.isArray(src)) { + for (let i = 0; i < src.length; i += 2) { + headers[src[i]] = src[i + 1] + } + return headers + } + + if (typeof src[Symbol.iterator] === 'function') { + for (const s of src) { + if (!Array.isArray(s)) throw new Error('opts.headers is not a valid header map') + const [key, val] = s if (typeof key !== 'string' || typeof val !== 'string') { throw new Error('opts.headers is not a valid header map') } headers[key.toLowerCase()] = val } - } else if (typeof opts.headers === 'object') { - headers = {} + return headers + } - for (const key of Object.keys(opts.headers)) { - headers[key.toLowerCase()] = opts.headers[key] - } - } else { - throw new Error('opts.headers is not an object') + if (typeof src === 'object') { + for (const key of Object.keys(src)) headers[key.toLowerCase()] = src[key] + return headers } - return headers + throw new Error('opts.headers is not an object') } module.exports = { diff --git a/lib/interceptor/dns.js b/lib/interceptor/dns.js index 5878780c9b0..0d15a6bba8b 100644 --- a/lib/interceptor/dns.js +++ b/lib/interceptor/dns.js @@ -6,14 +6,6 @@ const { InvalidArgumentError, InformationalError } = require('../core/errors') const maxInt = Math.pow(2, 31) - 1 const { normalizeHeaders } = require('../core/util.js') -function addHostHeader (opts, host) { - if (Array.isArray(opts.headers)) { - return ['host', host, ...opts.headers] - } - const headers = normalizeHeaders(opts) - return { host, ...headers } -} - class DNSInstance { #maxTTL = 0 #maxItems = 0 @@ -420,7 +412,7 @@ module.exports = interceptorOpts => { ...origDispatchOpts, servername: origin.hostname, // For SNI on TLS origin: newOrigin.origin, - headers: addHostHeader(origDispatchOpts, origin.host) + headers: { host: origin.host, ...normalizeHeaders(origDispatchOpts) } } dispatch( From daf988cc998789d2e595ce55a6171f77205c9c3f Mon Sep 17 00:00:00 2001 From: FelixVaughan Date: Thu, 4 Sep 2025 16:45:51 -0600 Subject: [PATCH 09/13] jsdoc and str redundancy --- lib/core/util.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index 66a5a88e4f0..6d9354112ca 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -905,7 +905,7 @@ Object.setPrototypeOf(normalizedMethodRecordsBase, null) Object.setPrototypeOf(normalizedMethodRecords, null) /** - * @param {Record} + * @param {Record | Iterable<[string, string]> | string[]} * @returns {Record} */ function normalizeHeaders (opts) { @@ -922,11 +922,12 @@ function normalizeHeaders (opts) { } if (typeof src[Symbol.iterator] === 'function') { + const msg = 'opts.headers is not a valid header map' for (const s of src) { - if (!Array.isArray(s)) throw new Error('opts.headers is not a valid header map') + if (!Array.isArray(s)) throw new Error(msg) const [key, val] = s if (typeof key !== 'string' || typeof val !== 'string') { - throw new Error('opts.headers is not a valid header map') + throw new Error(msg) } headers[key.toLowerCase()] = val } From 0ad29bc8d12b6fc993d6f63d40f74985b9e39a0f Mon Sep 17 00:00:00 2001 From: FelixVaughan Date: Fri, 5 Sep 2025 12:16:52 -0600 Subject: [PATCH 10/13] undici error usage --- lib/core/util.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index 6d9354112ca..aa144debefb 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -924,10 +924,10 @@ function normalizeHeaders (opts) { if (typeof src[Symbol.iterator] === 'function') { const msg = 'opts.headers is not a valid header map' for (const s of src) { - if (!Array.isArray(s)) throw new Error(msg) + if (!Array.isArray(s)) throw new InvalidArgumentError(msg) const [key, val] = s if (typeof key !== 'string' || typeof val !== 'string') { - throw new Error(msg) + throw new InvalidArgumentError(msg) } headers[key.toLowerCase()] = val } @@ -939,7 +939,7 @@ function normalizeHeaders (opts) { return headers } - throw new Error('opts.headers is not an object') + throw new InvalidArgumentError('opts.headers is not an object') } module.exports = { From bea9f5d60f1193562a876327cce74a53a991085d Mon Sep 17 00:00:00 2001 From: FelixVaughan Date: Fri, 5 Sep 2025 12:26:53 -0600 Subject: [PATCH 11/13] nit --- lib/core/util.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index aa144debefb..27625f8c84c 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -922,12 +922,12 @@ function normalizeHeaders (opts) { } if (typeof src[Symbol.iterator] === 'function') { - const msg = 'opts.headers is not a valid header map' + const errMsg = 'opts.headers is not a valid header map' for (const s of src) { - if (!Array.isArray(s)) throw new InvalidArgumentError(msg) + if (!Array.isArray(s)) throw new InvalidArgumentError(errMsg) const [key, val] = s if (typeof key !== 'string' || typeof val !== 'string') { - throw new InvalidArgumentError(msg) + throw new InvalidArgumentError(errMsg) } headers[key.toLowerCase()] = val } From 529fabdf8717408ab41c17d08fc3d2a09c89cc47 Mon Sep 17 00:00:00 2001 From: FelixVaughan Date: Wed, 10 Sep 2025 12:35:12 -0600 Subject: [PATCH 12/13] minor pr changes --- lib/core/util.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index 27625f8c84c..a0dce6f96e9 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -905,14 +905,14 @@ Object.setPrototypeOf(normalizedMethodRecordsBase, null) Object.setPrototypeOf(normalizedMethodRecords, null) /** - * @param {Record | Iterable<[string, string]> | string[]} + * @param {Record | Iterable<[string, string]> | string[]} opts * @returns {Record} */ function normalizeHeaders (opts) { const headers = {} const src = opts.headers - if (src == null) return headers + if (typeof src !== 'object' || src === null) return headers if (Array.isArray(src)) { for (let i = 0; i < src.length; i += 2) { @@ -934,12 +934,10 @@ function normalizeHeaders (opts) { return headers } - if (typeof src === 'object') { - for (const key of Object.keys(src)) headers[key.toLowerCase()] = src[key] - return headers + for (const key of Object.keys(src)) { + headers[key.toLowerCase()] = src[key] } - - throw new InvalidArgumentError('opts.headers is not an object') + return headers } module.exports = { From dec08f9bc433a3e7d1567fb2e9c7013432c43caa Mon Sep 17 00:00:00 2001 From: Felix Ezama-Vaughan Date: Wed, 17 Sep 2025 22:35:56 -0600 Subject: [PATCH 13/13] pr suggestions --- lib/core/util.js | 26 ++++++++++++++------------ lib/interceptor/cache.js | 2 +- lib/interceptor/dns.js | 2 +- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index a0dce6f96e9..3d1dcbd3540 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -905,39 +905,41 @@ Object.setPrototypeOf(normalizedMethodRecordsBase, null) Object.setPrototypeOf(normalizedMethodRecords, null) /** - * @param {Record | Iterable<[string, string]> | string[]} opts + * @param {Record | Iterable<[string, string]> | string[] | null | undefined} headers * @returns {Record} */ -function normalizeHeaders (opts) { - const headers = {} - const src = opts.headers +function normalizeHeaders (headers) { + const normalized = {} + const src = headers - if (typeof src !== 'object' || src === null) return headers + if (typeof src !== 'object' || src === null) return normalized if (Array.isArray(src)) { for (let i = 0; i < src.length; i += 2) { - headers[src[i]] = src[i + 1] + normalized[src[i]] = src[i + 1] } - return headers + return normalized } if (typeof src[Symbol.iterator] === 'function') { - const errMsg = 'opts.headers is not a valid header map' + const errMsg = 'headers is not a valid header map' for (const s of src) { if (!Array.isArray(s)) throw new InvalidArgumentError(errMsg) const [key, val] = s if (typeof key !== 'string' || typeof val !== 'string') { throw new InvalidArgumentError(errMsg) } - headers[key.toLowerCase()] = val + const lowerKey = headerNameLowerCasedRecord[key] ?? key.toLowerCase() + normalized[lowerKey] = val } - return headers + return normalized } for (const key of Object.keys(src)) { - headers[key.toLowerCase()] = src[key] + const lowerKey = headerNameLowerCasedRecord[key] ?? key.toLowerCase() + normalized[lowerKey] = src[key] } - return headers + return normalized } module.exports = { diff --git a/lib/interceptor/cache.js b/lib/interceptor/cache.js index 1d385156f4a..b075a9db321 100644 --- a/lib/interceptor/cache.js +++ b/lib/interceptor/cache.js @@ -327,7 +327,7 @@ module.exports = (opts = {}) => { opts = { ...opts, - headers: normalizeHeaders(opts) + headers: normalizeHeaders(opts.headers) } const reqCacheControl = opts.headers?.['cache-control'] diff --git a/lib/interceptor/dns.js b/lib/interceptor/dns.js index 0d15a6bba8b..d97484d7efc 100644 --- a/lib/interceptor/dns.js +++ b/lib/interceptor/dns.js @@ -412,7 +412,7 @@ module.exports = interceptorOpts => { ...origDispatchOpts, servername: origin.hostname, // For SNI on TLS origin: newOrigin.origin, - headers: { host: origin.host, ...normalizeHeaders(origDispatchOpts) } + headers: { host: origin.host, ...normalizeHeaders(origDispatchOpts.headers) } } dispatch(