From e2914c6c32aea0392310a8211dbae1bb89ffb010 Mon Sep 17 00:00:00 2001 From: Khafra <42794878+KhafraDev@users.noreply.github.com> Date: Thu, 24 Nov 2022 19:08:39 -0500 Subject: [PATCH] fix(fetch): treat headers as case sensitive --- lib/fetch/headers.js | 32 +++++++++++++------ lib/fetch/index.js | 10 +++--- lib/fetch/symbols.js | 3 +- test/wpt/server/server.mjs | 17 ++++++++++ .../api/basic/request-headers-case.any.js | 13 ++++++++ 5 files changed, 59 insertions(+), 16 deletions(-) create mode 100644 test/wpt/tests/fetch/api/basic/request-headers-case.any.js diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index 76d5cde578b..60d6de72a54 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -3,7 +3,7 @@ 'use strict' const { kHeadersList } = require('../core/symbols') -const { kGuard } = require('./symbols') +const { kGuard, kHeadersCaseInsensitive } = require('./symbols') const { kEnumerableProperty } = require('../core/util') const { makeIterator, @@ -96,27 +96,27 @@ class HeadersList { // 1. If list contains name, then set name to the first such // header’s name. - name = name.toLowerCase() - const exists = this[kHeadersMap].get(name) + const lowercaseName = name.toLowerCase() + const exists = this[kHeadersMap].get(lowercaseName) // 2. Append (name, value) to list. if (exists) { - this[kHeadersMap].set(name, `${exists}, ${value}`) + this[kHeadersMap].set(lowercaseName, { name: exists.name, value: `${exists.value}, ${value}` }) } else { - this[kHeadersMap].set(name, `${value}`) + this[kHeadersMap].set(lowercaseName, { name, value }) } } // https://fetch.spec.whatwg.org/#concept-header-list-set set (name, value) { this[kHeadersSortedMap] = null - name = name.toLowerCase() + const lowercaseName = name.toLowerCase() // 1. If list contains name, then set the value of // the first such header to value and remove the // others. // 2. Otherwise, append header (name, value) to list. - return this[kHeadersMap].set(name, value) + return this[kHeadersMap].set(lowercaseName, { name, value }) } // https://fetch.spec.whatwg.org/#concept-header-list-delete @@ -137,14 +137,26 @@ class HeadersList { // 2. Return the values of all headers in list whose name // is a byte-case-insensitive match for name, // separated from each other by 0x2C 0x20, in order. - return this[kHeadersMap].get(name.toLowerCase()) ?? null + return this[kHeadersMap].get(name.toLowerCase())?.value ?? null } * [Symbol.iterator] () { - for (const pair of this[kHeadersMap]) { - yield pair + // use the lowercased name + for (const [name, { value }] of this[kHeadersMap]) { + yield [name, value] } } + + get [kHeadersCaseInsensitive] () { + /** @type {string[]} */ + const flatList = [] + + for (const { name, value } of this[kHeadersMap].values()) { + flatList.push(name, value) + } + + return flatList + } } // https://fetch.spec.whatwg.org/#headers-class diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 94f2d1e0aca..ad95239562e 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -39,7 +39,7 @@ const { readableStreamClose, isomorphicEncode } = require('./util') -const { kState, kHeaders, kGuard, kRealm } = require('./symbols') +const { kState, kHeaders, kGuard, kRealm, kHeadersCaseInsensitive } = require('./symbols') const assert = require('assert') const { safelyExtractBody } = require('./body') const { @@ -840,8 +840,8 @@ async function schemeFetch (fetchParams) { const response = makeResponse({ statusText: 'OK', headersList: [ - ['content-length', length], - ['content-type', type] + ['content-length', { name: 'Content-Length', value: length }], + ['content-type', { name: 'Content-Type', value: type }] ] }) @@ -870,7 +870,7 @@ async function schemeFetch (fetchParams) { return makeResponse({ statusText: 'OK', headersList: [ - ['content-type', mimeType] + ['content-type', { name: 'Content-Type', value: mimeType }] ], body: safelyExtractBody(dataURLStruct.body)[0] }) @@ -1930,7 +1930,7 @@ async function httpNetworkFetch ( origin: url.origin, method: request.method, body: fetchParams.controller.dispatcher.isMockActive ? request.body && request.body.source : body, - headers: [...request.headersList].flat(), + headers: request.headersList[kHeadersCaseInsensitive], maxRedirections: 0, bodyTimeout: 300_000, headersTimeout: 300_000 diff --git a/lib/fetch/symbols.js b/lib/fetch/symbols.js index 0b947d55bad..e841ac730a7 100644 --- a/lib/fetch/symbols.js +++ b/lib/fetch/symbols.js @@ -6,5 +6,6 @@ module.exports = { kSignal: Symbol('signal'), kState: Symbol('state'), kGuard: Symbol('guard'), - kRealm: Symbol('realm') + kRealm: Symbol('realm'), + kHeadersCaseInsensitive: Symbol('headers case insensitive') } diff --git a/test/wpt/server/server.mjs b/test/wpt/server/server.mjs index c62f9658053..7165f0b9ca1 100644 --- a/test/wpt/server/server.mjs +++ b/test/wpt/server/server.mjs @@ -294,6 +294,23 @@ const server = createServer(async (req, res) => { res.end('not actually gzip') break } + case '/xhr/resources/echo-headers.py': { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/plain') + + // wpt runner sends this as 1 chunk + let body = '' + + for (let i = 0; i < req.rawHeaders.length; i += 2) { + const key = req.rawHeaders[i] + const value = req.rawHeaders[i + 1] + + body += `${key}: ${value}` + } + + res.end(body) + break + } default: { res.statusCode = 200 res.end('body') diff --git a/test/wpt/tests/fetch/api/basic/request-headers-case.any.js b/test/wpt/tests/fetch/api/basic/request-headers-case.any.js new file mode 100644 index 00000000000..4c10e717f8c --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/request-headers-case.any.js @@ -0,0 +1,13 @@ +// META: global=window,worker + +promise_test(() => { + return fetch("/xhr/resources/echo-headers.py", {headers: [["THIS-is-A-test", 1], ["THIS-IS-A-TEST", 2]] }).then(res => res.text()).then(body => { + assert_regexp_match(body, /THIS-is-A-test: 1, 2/) + }) +}, "Multiple headers with the same name, different case (THIS-is-A-test first)") + +promise_test(() => { + return fetch("/xhr/resources/echo-headers.py", {headers: [["THIS-IS-A-TEST", 1], ["THIS-is-A-test", 2]] }).then(res => res.text()).then(body => { + assert_regexp_match(body, /THIS-IS-A-TEST: 1, 2/) + }) +}, "Multiple headers with the same name, different case (THIS-IS-A-TEST first)")