Skip to content

Commit 232905f

Browse files
authored
fix(fetch): send headers in the case that they were sent (nodejs#1784)
* fix(fetch): treat headers as case sensitive * fix: mark test as flaky
1 parent 41d60d6 commit 232905f

File tree

6 files changed

+61
-18
lines changed

6 files changed

+61
-18
lines changed

lib/fetch/headers.js

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
'use strict'
44

55
const { kHeadersList } = require('../core/symbols')
6-
const { kGuard } = require('./symbols')
6+
const { kGuard, kHeadersCaseInsensitive } = require('./symbols')
77
const { kEnumerableProperty } = require('../core/util')
88
const {
99
makeIterator,
@@ -96,27 +96,27 @@ class HeadersList {
9696

9797
// 1. If list contains name, then set name to the first such
9898
// header’s name.
99-
name = name.toLowerCase()
100-
const exists = this[kHeadersMap].get(name)
99+
const lowercaseName = name.toLowerCase()
100+
const exists = this[kHeadersMap].get(lowercaseName)
101101

102102
// 2. Append (name, value) to list.
103103
if (exists) {
104-
this[kHeadersMap].set(name, `${exists}, ${value}`)
104+
this[kHeadersMap].set(lowercaseName, { name: exists.name, value: `${exists.value}, ${value}` })
105105
} else {
106-
this[kHeadersMap].set(name, `${value}`)
106+
this[kHeadersMap].set(lowercaseName, { name, value })
107107
}
108108
}
109109

110110
// https://fetch.spec.whatwg.org/#concept-header-list-set
111111
set (name, value) {
112112
this[kHeadersSortedMap] = null
113-
name = name.toLowerCase()
113+
const lowercaseName = name.toLowerCase()
114114

115115
// 1. If list contains name, then set the value of
116116
// the first such header to value and remove the
117117
// others.
118118
// 2. Otherwise, append header (name, value) to list.
119-
return this[kHeadersMap].set(name, value)
119+
return this[kHeadersMap].set(lowercaseName, { name, value })
120120
}
121121

122122
// https://fetch.spec.whatwg.org/#concept-header-list-delete
@@ -137,14 +137,26 @@ class HeadersList {
137137
// 2. Return the values of all headers in list whose name
138138
// is a byte-case-insensitive match for name,
139139
// separated from each other by 0x2C 0x20, in order.
140-
return this[kHeadersMap].get(name.toLowerCase()) ?? null
140+
return this[kHeadersMap].get(name.toLowerCase())?.value ?? null
141141
}
142142

143143
* [Symbol.iterator] () {
144-
for (const pair of this[kHeadersMap]) {
145-
yield pair
144+
// use the lowercased name
145+
for (const [name, { value }] of this[kHeadersMap]) {
146+
yield [name, value]
146147
}
147148
}
149+
150+
get [kHeadersCaseInsensitive] () {
151+
/** @type {string[]} */
152+
const flatList = []
153+
154+
for (const { name, value } of this[kHeadersMap].values()) {
155+
flatList.push(name, value)
156+
}
157+
158+
return flatList
159+
}
148160
}
149161

150162
// https://fetch.spec.whatwg.org/#headers-class

lib/fetch/index.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const {
3939
readableStreamClose,
4040
isomorphicEncode
4141
} = require('./util')
42-
const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
42+
const { kState, kHeaders, kGuard, kRealm, kHeadersCaseInsensitive } = require('./symbols')
4343
const assert = require('assert')
4444
const { safelyExtractBody } = require('./body')
4545
const {
@@ -843,8 +843,8 @@ async function schemeFetch (fetchParams) {
843843
const response = makeResponse({
844844
statusText: 'OK',
845845
headersList: [
846-
['content-length', length],
847-
['content-type', type]
846+
['content-length', { name: 'Content-Length', value: length }],
847+
['content-type', { name: 'Content-Type', value: type }]
848848
]
849849
})
850850

@@ -873,7 +873,7 @@ async function schemeFetch (fetchParams) {
873873
return makeResponse({
874874
statusText: 'OK',
875875
headersList: [
876-
['content-type', mimeType]
876+
['content-type', { name: 'Content-Type', value: mimeType }]
877877
],
878878
body: safelyExtractBody(dataURLStruct.body)[0]
879879
})
@@ -1941,7 +1941,7 @@ async function httpNetworkFetch (
19411941
origin: url.origin,
19421942
method: request.method,
19431943
body: fetchParams.controller.dispatcher.isMockActive ? request.body && request.body.source : body,
1944-
headers: [...request.headersList].flat(),
1944+
headers: request.headersList[kHeadersCaseInsensitive],
19451945
maxRedirections: 0,
19461946
bodyTimeout: 300_000,
19471947
headersTimeout: 300_000

lib/fetch/symbols.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ module.exports = {
66
kSignal: Symbol('signal'),
77
kState: Symbol('state'),
88
kGuard: Symbol('guard'),
9-
kRealm: Symbol('realm')
9+
kRealm: Symbol('realm'),
10+
kHeadersCaseInsensitive: Symbol('headers case insensitive')
1011
}

test/wpt/server/server.mjs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,23 @@ const server = createServer(async (req, res) => {
316316
res.end('none')
317317
break
318318
}
319+
case '/xhr/resources/echo-headers.py': {
320+
res.statusCode = 200
321+
res.setHeader('Content-Type', 'text/plain')
322+
323+
// wpt runner sends this as 1 chunk
324+
let body = ''
325+
326+
for (let i = 0; i < req.rawHeaders.length; i += 2) {
327+
const key = req.rawHeaders[i]
328+
const value = req.rawHeaders[i + 1]
329+
330+
body += `${key}: ${value}`
331+
}
332+
333+
res.end(body)
334+
break
335+
}
319336
default: {
320337
res.statusCode = 200
321338
res.end('body')

test/wpt/status/fetch.status.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@
4949
"header-value-combining.any.js": {
5050
"fail": [
5151
"response.headers.get('content-length') expects 0, 0",
52-
"response.headers.get('double-trouble') expects , ",
5352
"response.headers.get('foo-test') expects 1, 2, 3",
5453
"response.headers.get('heya') expects , \\x0B\f, 1, , , 2"
5554
],
5655
"flaky": [
5756
"response.headers.get('content-length') expects 0",
57+
"response.headers.get('double-trouble') expects , ",
5858
"response.headers.get('www-authenticate') expects 1, 2, 3, 4"
5959
]
6060
},
@@ -207,4 +207,4 @@
207207
"fetch() with value %1F"
208208
]
209209
}
210-
}
210+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// META: global=window,worker
2+
3+
promise_test(() => {
4+
return fetch("/xhr/resources/echo-headers.py", {headers: [["THIS-is-A-test", 1], ["THIS-IS-A-TEST", 2]] }).then(res => res.text()).then(body => {
5+
assert_regexp_match(body, /THIS-is-A-test: 1, 2/)
6+
})
7+
}, "Multiple headers with the same name, different case (THIS-is-A-test first)")
8+
9+
promise_test(() => {
10+
return fetch("/xhr/resources/echo-headers.py", {headers: [["THIS-IS-A-TEST", 1], ["THIS-is-A-test", 2]] }).then(res => res.text()).then(body => {
11+
assert_regexp_match(body, /THIS-IS-A-TEST: 1, 2/)
12+
})
13+
}, "Multiple headers with the same name, different case (THIS-IS-A-TEST first)")

0 commit comments

Comments
 (0)