Skip to content

Commit 25b6674

Browse files
KhafraDevcrysmags
authored andcommitted
feat: remove headers filtering (nodejs#1469)
* feat: remove headers filtering * add wintercg issue to README * update README * fix(Headers): properly use/set "this's headers" * fix: remove broken guard checks
1 parent 47fa3a0 commit 25b6674

File tree

7 files changed

+78
-185
lines changed

7 files changed

+78
-185
lines changed

README.md

+9
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,15 @@ const headers = await fetch(url)
288288
.then(res => res.headers)
289289
```
290290

291+
##### Forbidden and Safelisted Header Names
292+
293+
* https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name
294+
* https://fetch.spec.whatwg.org/#forbidden-header-name
295+
* https://fetch.spec.whatwg.org/#forbidden-response-header-name
296+
* https://github.com/wintercg/fetch/issues/6
297+
298+
The [Fetch Standard](https://fetch.spec.whatwg.org) requires implementations to exclude certain headers from requests and responses. In browser environments, some headers are forbidden so the user agent remains in full control over them. In Undici, these constraints are removed to give more control to the user.
299+
291300
### `undici.upgrade([url, options]): Promise`
292301

293302
Upgrade to a different protocol. See [MDN - HTTP - Protocol upgrade mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) for more details.

lib/fetch/constants.js

-31
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,5 @@
11
'use strict'
22

3-
const forbiddenHeaderNames = [
4-
'accept-charset',
5-
'accept-encoding',
6-
'access-control-request-headers',
7-
'access-control-request-method',
8-
'connection',
9-
'content-length',
10-
'cookie',
11-
'cookie2',
12-
'date',
13-
'dnt',
14-
'expect',
15-
'host',
16-
'keep-alive',
17-
'origin',
18-
'referer',
19-
'te',
20-
'trailer',
21-
'transfer-encoding',
22-
'upgrade',
23-
'via'
24-
]
25-
263
const corsSafeListedMethods = ['GET', 'HEAD', 'POST']
274

285
const nullBodyStatus = [101, 204, 205, 304]
@@ -58,9 +35,6 @@ const requestCache = [
5835
'only-if-cached'
5936
]
6037

61-
// https://fetch.spec.whatwg.org/#forbidden-response-header-name
62-
const forbiddenResponseHeaderNames = ['set-cookie', 'set-cookie2']
63-
6438
const requestBodyHeader = [
6539
'content-encoding',
6640
'content-language',
@@ -86,20 +60,15 @@ const subresource = [
8660
''
8761
]
8862

89-
const corsSafeListedResponseHeaderNames = [] // TODO
90-
9163
module.exports = {
9264
subresource,
93-
forbiddenResponseHeaderNames,
94-
corsSafeListedResponseHeaderNames,
9565
forbiddenMethods,
9666
requestBodyHeader,
9767
referrerPolicy,
9868
requestRedirect,
9969
requestMode,
10070
requestCredentials,
10171
requestCache,
102-
forbiddenHeaderNames,
10372
redirectStatus,
10473
corsSafeListedMethods,
10574
nullBodyStatus,

lib/fetch/headers.js

+8-38
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ const { validateHeaderName, validateHeaderValue } = require('http')
66
const { kHeadersList } = require('../core/symbols')
77
const { kGuard } = require('./symbols')
88
const { kEnumerableProperty } = require('../core/util')
9-
const {
10-
forbiddenHeaderNames,
11-
forbiddenResponseHeaderNames
12-
} = require('./constants')
139

1410
const kHeadersMap = Symbol('headers map')
1511
const kHeadersSortedMap = Symbol('headers map sorted')
@@ -115,6 +111,11 @@ class HeadersList {
115111
}
116112
}
117113

114+
clear () {
115+
this[kHeadersMap].clear()
116+
this[kHeadersSortedMap] = null
117+
}
118+
118119
append (name, value) {
119120
this[kHeadersSortedMap] = null
120121

@@ -211,22 +212,11 @@ class Headers {
211212
)
212213
}
213214

214-
const normalizedName = normalizeAndValidateHeaderName(String(name))
215-
215+
// Note: undici does not implement forbidden header names
216216
if (this[kGuard] === 'immutable') {
217217
throw new TypeError('immutable')
218-
} else if (
219-
this[kGuard] === 'request' &&
220-
forbiddenHeaderNames.includes(normalizedName)
221-
) {
222-
return
223218
} else if (this[kGuard] === 'request-no-cors') {
224219
// TODO
225-
} else if (
226-
this[kGuard] === 'response' &&
227-
forbiddenResponseHeaderNames.includes(normalizedName)
228-
) {
229-
return
230220
}
231221

232222
return this[kHeadersList].append(String(name), String(value))
@@ -244,22 +234,11 @@ class Headers {
244234
)
245235
}
246236

247-
const normalizedName = normalizeAndValidateHeaderName(String(name))
248-
237+
// Note: undici does not implement forbidden header names
249238
if (this[kGuard] === 'immutable') {
250239
throw new TypeError('immutable')
251-
} else if (
252-
this[kGuard] === 'request' &&
253-
forbiddenHeaderNames.includes(normalizedName)
254-
) {
255-
return
256240
} else if (this[kGuard] === 'request-no-cors') {
257241
// TODO
258-
} else if (
259-
this[kGuard] === 'response' &&
260-
forbiddenResponseHeaderNames.includes(normalizedName)
261-
) {
262-
return
263242
}
264243

265244
return this[kHeadersList].delete(String(name))
@@ -307,20 +286,11 @@ class Headers {
307286
)
308287
}
309288

289+
// Note: undici does not implement forbidden header names
310290
if (this[kGuard] === 'immutable') {
311291
throw new TypeError('immutable')
312-
} else if (
313-
this[kGuard] === 'request' &&
314-
forbiddenHeaderNames.includes(String(name).toLocaleLowerCase())
315-
) {
316-
return
317292
} else if (this[kGuard] === 'request-no-cors') {
318293
// TODO
319-
} else if (
320-
this[kGuard] === 'response' &&
321-
forbiddenResponseHeaderNames.includes(String(name).toLocaleLowerCase())
322-
) {
323-
return
324294
}
325295

326296
return this[kHeadersList].set(String(name), String(value))

lib/fetch/request.js

+5-6
Original file line numberDiff line numberDiff line change
@@ -384,8 +384,8 @@ class Request {
384384
// Realm, whose header list is request’s header list and guard is
385385
// "request".
386386
this[kHeaders] = new Headers()
387-
this[kHeaders][kGuard] = 'request'
388387
this[kHeaders][kHeadersList] = request.headersList
388+
this[kHeaders][kGuard] = 'request'
389389
this[kHeaders][kRealm] = this[kRealm]
390390

391391
// 31. If this’s request’s mode is "no-cors", then:
@@ -406,26 +406,25 @@ class Request {
406406
if (Object.keys(init).length !== 0) {
407407
// 1. Let headers be a copy of this’s headers and its associated header
408408
// list.
409-
let headers = new Headers(this.headers)
409+
let headers = new Headers(this[kHeaders])
410410

411411
// 2. If init["headers"] exists, then set headers to init["headers"].
412412
if (init.headers !== undefined) {
413413
headers = init.headers
414414
}
415415

416416
// 3. Empty this’s headers’s header list.
417-
this[kState].headersList = new HeadersList()
418-
this[kHeaders][kHeadersList] = this[kState].headersList
417+
this[kHeaders][kHeadersList].clear()
419418

420419
// 4. If headers is a Headers object, then for each header in its header
421420
// list, append header’s name/header’s value to this’s headers.
422421
if (headers.constructor.name === 'Headers') {
423-
for (const [key, val] of headers[kHeadersList] || headers) {
422+
for (const [key, val] of headers) {
424423
this[kHeaders].append(key, val)
425424
}
426425
} else {
427426
// 5. Otherwise, fill this’s headers with headers.
428-
fillHeaders(this[kState].headersList, headers)
427+
fillHeaders(this[kHeaders], headers)
429428
}
430429
}
431430

lib/fetch/response.js

+6-31
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ const { kEnumerableProperty } = util
88
const { responseURL, isValidReasonPhrase, toUSVString, isCancelled, isAborted, serializeJavascriptValueToJSONString } = require('./util')
99
const {
1010
redirectStatus,
11-
nullBodyStatus,
12-
forbiddenResponseHeaderNames,
13-
corsSafeListedResponseHeaderNames
11+
nullBodyStatus
1412
} = require('./constants')
1513
const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
1614
const { kHeadersList } = require('../core/symbols')
@@ -380,28 +378,6 @@ function makeFilteredResponse (response, state) {
380378
})
381379
}
382380

383-
function makeFilteredHeadersList (headersList, filter) {
384-
return new Proxy(headersList, {
385-
get (target, prop) {
386-
// Override methods used by Headers class.
387-
if (prop === 'get' || prop === 'has') {
388-
const defaultReturn = prop === 'has' ? false : null
389-
return (name) => filter(name) ? target[prop](name) : defaultReturn
390-
} else if (prop === Symbol.iterator) {
391-
return function * () {
392-
for (const entry of target) {
393-
if (filter(entry[0])) {
394-
yield entry
395-
}
396-
}
397-
}
398-
} else {
399-
return target[prop]
400-
}
401-
}
402-
})
403-
}
404-
405381
// https://fetch.spec.whatwg.org/#concept-filtered-response
406382
function filterResponse (response, type) {
407383
// Set response to the following filtered response with response as its
@@ -411,22 +387,21 @@ function filterResponse (response, type) {
411387
// and header list excludes any headers in internal response’s header list
412388
// whose name is a forbidden response-header name.
413389

390+
// Note: undici does not implement forbidden response-header names
414391
return makeFilteredResponse(response, {
415392
type: 'basic',
416-
headersList: makeFilteredHeadersList(
417-
response.headersList,
418-
(name) => !forbiddenResponseHeaderNames.includes(name.toLowerCase())
419-
)
393+
headersList: response.headersList
420394
})
421395
} else if (type === 'cors') {
422396
// A CORS filtered response is a filtered response whose type is "cors"
423397
// and header list excludes any headers in internal response’s header
424398
// list whose name is not a CORS-safelisted response-header name, given
425399
// internal response’s CORS-exposed header-name list.
426400

401+
// Note: undici does not implement CORS-safelisted response-header names
427402
return makeFilteredResponse(response, {
428403
type: 'cors',
429-
headersList: makeFilteredHeadersList(response.headersList, (name) => !corsSafeListedResponseHeaderNames.includes(name))
404+
headersList: response.headersList
430405
})
431406
} else if (type === 'opaque') {
432407
// An opaque filtered response is a filtered response whose type is
@@ -449,7 +424,7 @@ function filterResponse (response, type) {
449424
type: 'opaqueredirect',
450425
status: 0,
451426
statusText: '',
452-
headersList: makeFilteredHeadersList(response.headersList, () => false),
427+
headersList: [],
453428
body: null
454429
})
455430
} else {

test/fetch/cookies.js

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict'
2+
3+
const { once } = require('events')
4+
const { createServer } = require('http')
5+
const { test } = require('tap')
6+
const { fetch, Headers } = require('../..')
7+
8+
test('Can receive set-cookie headers from a server using fetch - issue #1262', async (t) => {
9+
const server = createServer((req, res) => {
10+
res.setHeader('set-cookie', 'name=value; Domain=example.com')
11+
res.end()
12+
}).listen(0)
13+
14+
t.teardown(server.close.bind(server))
15+
await once(server, 'listening')
16+
17+
const response = await fetch(`http://localhost:${server.address().port}`)
18+
19+
t.equal(response.headers.get('set-cookie'), 'name=value; Domain=example.com')
20+
21+
const response2 = await fetch(`http://localhost:${server.address().port}`, {
22+
credentials: 'include'
23+
})
24+
25+
t.equal(response2.headers.get('set-cookie'), 'name=value; Domain=example.com')
26+
27+
t.end()
28+
})
29+
30+
test('Can send cookies to a server with fetch - issue #1463', async (t) => {
31+
const server = createServer((req, res) => {
32+
t.equal(req.headers.cookie, 'value')
33+
res.end()
34+
}).listen(0)
35+
36+
t.teardown(server.close.bind(server))
37+
await once(server, 'listening')
38+
39+
const headersInit = [
40+
new Headers([['cookie', 'value']]),
41+
{ cookie: 'value' },
42+
[['cookie', 'value']]
43+
]
44+
45+
for (const headers of headersInit) {
46+
await fetch(`http://localhost:${server.address().port}`, { headers })
47+
}
48+
49+
t.end()
50+
})

0 commit comments

Comments
 (0)