diff --git a/lib/core/util.js b/lib/core/util.js index d8833c01e7c..5f5949c330e 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -928,6 +928,44 @@ const normalizedMethodRecords = { Object.setPrototypeOf(normalizedMethodRecordsBase, null) Object.setPrototypeOf(normalizedMethodRecords, null) +/** + * @param {Record | Iterable<[string, string]> | string[] | null | undefined} headers + * @returns {Record} + */ +function normalizeHeaders (headers) { + const normalized = {} + const src = headers + + if (typeof src !== 'object' || src === null) return normalized + + if (Array.isArray(src)) { + for (let i = 0; i < src.length; i += 2) { + normalized[src[i]] = src[i + 1] + } + return normalized + } + + if (typeof src[Symbol.iterator] === 'function') { + 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) + } + const lowerKey = headerNameLowerCasedRecord[key] ?? key.toLowerCase() + normalized[lowerKey] = val + } + return normalized + } + + for (const key of Object.keys(src)) { + const lowerKey = headerNameLowerCasedRecord[key] ?? key.toLowerCase() + normalized[lowerKey] = src[key] + } + return normalized +} + module.exports = { kEnumerableProperty, isDisturbed, @@ -963,6 +1001,7 @@ module.exports = { isValidHeaderValue, isTokenCharCode, parseRangeHeader, + normalizeHeaders, normalizedMethodRecordsBase, normalizedMethodRecords, isValidPort, diff --git a/lib/interceptor/cache.js b/lib/interceptor/cache.js index d561d4572cc..f1ef18e357e 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') /** @@ -387,7 +388,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 38287607143..d97484d7efc 100644 --- a/lib/interceptor/dns.js +++ b/lib/interceptor/dns.js @@ -4,6 +4,7 @@ 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') class DNSInstance { #maxTTL = 0 @@ -411,10 +412,7 @@ module.exports = interceptorOpts => { ...origDispatchOpts, servername: origin.hostname, // For SNI on TLS origin: newOrigin.origin, - headers: { - host: origin.host, - ...origDispatchOpts.headers - } + headers: { host: origin.host, ...normalizeHeaders(origDispatchOpts.headers) } } 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 3ee48c20973..950417e3bc1 100644 --- a/test/interceptors/dns.js +++ b/test/interceptors/dns.js @@ -1936,3 +1936,54 @@ test('#3951 - Should handle lookup errors correctly', async t => { origin: 'http://localhost' }), new Error('lookup error')) }) + +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') + }) + + server.listen(0) + await once(server, 'listening') + + const origin = `http://localhost:${server.address().port}` + + const cases = [ + { + name: 'flat array', + headers: ['foo', 'bar'] + }, + { + name: 'record', + headers: { foo: 'bar' } + }, + { + name: 'record with multi-value', + headers: { foo: ['bar'] } + }, + { + name: 'iterable (map) object', + headers: new Map([['foo', 'bar']]) + }, + { + 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(dns()) + 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') +})