From c101e4e2cc5bd5d9c564736e3f992f6d09abcd24 Mon Sep 17 00:00:00 2001 From: Felix Ezama-Vaughan Date: Sun, 31 Aug 2025 11:55:09 -0600 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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: '/' })