diff --git a/lib/web/fetch/headers.js b/lib/web/fetch/headers.js index 2235f125c65..a1dab1050a8 100644 --- a/lib/web/fetch/headers.js +++ b/lib/web/fetch/headers.js @@ -602,6 +602,12 @@ webidl.converters.HeadersInit = function (V) { if (webidl.util.Type(V) === 'Object') { const iterator = Reflect.get(V, Symbol.iterator) + // A work-around to ensure we send the properly-cased Headers when V is a Headers object. + // Read https://github.com/nodejs/undici/pull/3159#issuecomment-2075537226 before touching, please. + if (!util.types.isProxy(V) && kHeadersList in V && iterator === Headers.prototype.entries) { // Headers object + return V[kHeadersList].entries + } + if (typeof iterator === 'function') { return webidl.converters['sequence>'](V, iterator.bind(V)) } diff --git a/lib/web/fetch/request.js b/lib/web/fetch/request.js index ca12576b1f0..60626f06a92 100644 --- a/lib/web/fetch/request.js +++ b/lib/web/fetch/request.js @@ -459,8 +459,9 @@ class Request { // 4. If headers is a Headers object, then for each header in its header // list, append header’s name/header’s value to this’s headers. if (headers instanceof HeadersList) { - for (const [key, val] of headers) { - headersList.append(key, val) + for (const { 0: key, 1: val } of headers) { + // Note: The header names are already in lowercase. + headersList.append(key, val, true) } // Note: Copy the `set-cookie` meta-data. headersList.cookies = headers.cookies diff --git a/test/fetch/headers-case.js b/test/fetch/headers-case.js new file mode 100644 index 00000000000..bd9770b755f --- /dev/null +++ b/test/fetch/headers-case.js @@ -0,0 +1,30 @@ +'use strict' + +const { fetch, Headers } = require('../..') +const { createServer } = require('node:http') +const { once } = require('node:events') +const { test } = require('node:test') +const { tspl } = require('@matteo.collina/tspl') + +test('Headers retain keys case-sensitive', async (t) => { + const assert = tspl(t, { plan: 3 }) + + const server = createServer((req, res) => { + assert.ok(req.rawHeaders.includes('Content-Type')) + + res.end() + }).listen(0) + + t.after(() => server.close()) + await once(server, 'listening') + + for (const headers of [ + new Headers([['Content-Type', 'text/plain']]), + { 'Content-Type': 'text/plain' }, + [['Content-Type', 'text/plain']] + ]) { + await fetch(`http://localhost:${server.address().port}`, { + headers + }) + } +})