From 8f8dcb7f2759805d38f99c49d5d6ed66c7d39a9a Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 9 Jul 2026 08:42:26 +0000 Subject: [PATCH 1/3] fix: handle flat alternating header arrays in cache normalizeHeaders The cache interceptor's normalizeHeaders function only handled two header formats: array-of-pairs ([[key, val], ...]) and plain objects ({ key: val, ...}). However, undici internally uses flat alternating arrays ([key, val, key, val, ...]). When the cache() interceptor is placed before dns() in a compose chain, the DNS interceptor converts headers to flat alternating format before passing them to the cache interceptor, causing normalizeHeaders to throw 'opts.headers is not a valid header map'. Fixes #5522 --- lib/util/cache.js | 51 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/lib/util/cache.js b/lib/util/cache.js index a8f112168e6..62f98ffff64 100644 --- a/lib/util/cache.js +++ b/lib/util/cache.js @@ -42,15 +42,52 @@ function normalizeHeaders (opts) { headers = {} if (hasSafeIterator(opts.headers)) { - for (const x of opts.headers) { - if (!Array.isArray(x)) { - throw new Error('opts.headers is not a valid header map') + if (Array.isArray(opts.headers)) { + // Array format: could be flat alternating [k, v, k, v, ...] + // or array-of-pairs [[k, v], ...] + const first = opts.headers[0] + if (Array.isArray(first)) { + 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 { + // Flat alternating array [k, v, k, v, ...] + const len = opts.headers.length + if (len % 2 !== 0) { + throw new Error('opts.headers is not a valid header map') + } + for (let i = 0; i < len; i += 2) { + const key = opts.headers[i] + const val = opts.headers[i + 1] + if (typeof key !== 'string' || (typeof val !== 'string' && !Array.isArray(val))) { + throw new Error('opts.headers is not a valid header map') + } + if (typeof val === 'string') { + headers[key.toLowerCase()] = val + } else { + headers[key.toLowerCase()] = val.map(v => typeof v === 'string' ? v : v.toString('latin1')) + } + } } - const [key, val] = x - if (typeof key !== 'string' || typeof val !== 'string') { - throw new Error('opts.headers is not a valid header map') + } else { + // Non-array iterable (e.g. Map) — use original iteration logic + 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 } - headers[key.toLowerCase()] = val } } else { for (const key of Object.keys(opts.headers)) { From db1be8f3f7f83d678cc78c43a420c913527b2515 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 9 Jul 2026 09:24:28 +0000 Subject: [PATCH 2/3] test: add normalizeHeaders tests for flat alternating header arrays --- test/cache-interceptor/cache-utils.js | 64 +++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/test/cache-interceptor/cache-utils.js b/test/cache-interceptor/cache-utils.js index 7b66e66d3aa..70f15958de4 100644 --- a/test/cache-interceptor/cache-utils.js +++ b/test/cache-interceptor/cache-utils.js @@ -42,3 +42,67 @@ test('normalizeHeaders handles headers from Map', (t) => { strictEqual(headers['x-test'], 'ok') }) + +test('normalizeHeaders handles empty array', (t) => { + const { deepEqual } = tspl(t, { plan: 1 }) + + const headers = normalizeHeaders({ headers: [] }) + + deepEqual(headers, {}) +}) + +test('normalizeHeaders handles flat alternating array (single header)', (t) => { + const { strictEqual } = tspl(t, { plan: 1 }) + + const headers = normalizeHeaders({ + headers: ['host', 'localhost'] + }) + + strictEqual(headers.host, 'localhost') +}) + +test('normalizeHeaders handles flat alternating array (multiple headers)', (t) => { + const { deepEqual } = tspl(t, { plan: 1 }) + + const headers = normalizeHeaders({ + headers: ['host', 'localhost', 'content-type', 'application/json'] + }) + + deepEqual(headers, { host: 'localhost', 'content-type': 'application/json' }) +}) + +test('normalizeHeaders handles flat alternating array with array values', (t) => { + const { deepEqual } = tspl(t, { plan: 1 }) + + const headers = normalizeHeaders({ + headers: ['accept', ['application/json', 'text/plain']] + }) + + deepEqual(headers, { accept: ['application/json', 'text/plain'] }) +}) + +test('normalizeHeaders handles array-of-pairs (existing behavior)', (t) => { + const { strictEqual } = tspl(t, { plan: 1 }) + + const headers = normalizeHeaders({ + headers: [['host', 'localhost']] + }) + + strictEqual(headers.host, 'localhost') +}) + +test('normalizeHeaders throws on odd-length flat array', (t) => { + const { throws } = require('node:assert') + + throws(() => normalizeHeaders({ headers: ['host'] }), { + message: 'opts.headers is not a valid header map' + }) +}) + +test('normalizeHeaders throws on non-string key in flat array', (t) => { + const { throws } = require('node:assert') + + throws(() => normalizeHeaders({ headers: [42, 'value'] }), { + message: 'opts.headers is not a valid header map' + }) +}) From c16c29ddf8b12235dd18d3bfa63b62f79c631081 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 9 Jul 2026 10:01:23 +0000 Subject: [PATCH 3/3] refactor: replace .map() with for loop in normalizeHeaders --- lib/util/cache.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/util/cache.js b/lib/util/cache.js index 62f98ffff64..c88db6292b3 100644 --- a/lib/util/cache.js +++ b/lib/util/cache.js @@ -72,7 +72,12 @@ function normalizeHeaders (opts) { if (typeof val === 'string') { headers[key.toLowerCase()] = val } else { - headers[key.toLowerCase()] = val.map(v => typeof v === 'string' ? v : v.toString('latin1')) + const mapped = [] + for (let j = 0; j < val.length; j++) { + const v = val[j] + mapped.push(typeof v === 'string' ? v : v.toString('latin1')) + } + headers[key.toLowerCase()] = mapped } } }