From 232905f476913da9c73df03bac41a0a96d48b128 Mon Sep 17 00:00:00 2001 From: Khafra Date: Tue, 29 Nov 2022 04:04:27 -0500 Subject: [PATCH] fix(fetch): send headers in the case that they were sent (#1784) * fix(fetch): treat headers as case sensitive * fix: mark test as flaky --- lib/fetch/headers.js | 32 +++++++++++++------ lib/fetch/index.js | 10 +++--- lib/fetch/symbols.js | 3 +- test/wpt/server/server.mjs | 17 ++++++++++ test/wpt/status/fetch.status.json | 4 +-- .../api/basic/request-headers-case.any.js | 13 ++++++++ 6 files changed, 61 insertions(+), 18 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 56fe31a13c8..a10f1fd9823 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 { @@ -843,8 +843,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 }] ] }) @@ -873,7 +873,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] }) @@ -1941,7 +1941,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 86edcf65150..e931d8aef17 100644 --- a/test/wpt/server/server.mjs +++ b/test/wpt/server/server.mjs @@ -316,6 +316,23 @@ const server = createServer(async (req, res) => { res.end('none') 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/status/fetch.status.json b/test/wpt/status/fetch.status.json index b9d689f4778..3a16a27eeb9 100644 --- a/test/wpt/status/fetch.status.json +++ b/test/wpt/status/fetch.status.json @@ -49,12 +49,12 @@ "header-value-combining.any.js": { "fail": [ "response.headers.get('content-length') expects 0, 0", - "response.headers.get('double-trouble') expects , ", "response.headers.get('foo-test') expects 1, 2, 3", "response.headers.get('heya') expects , \\x0B\f, 1, , , 2" ], "flaky": [ "response.headers.get('content-length') expects 0", + "response.headers.get('double-trouble') expects , ", "response.headers.get('www-authenticate') expects 1, 2, 3, 4" ] }, @@ -207,4 +207,4 @@ "fetch() with value %1F" ] } -} \ No newline at end of file +} 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)")