Skip to content

Commit 3dec08b

Browse files
authored
feat: implement (basic) webidl & headers comments (#1495)
1 parent a1600f9 commit 3dec08b

4 files changed

Lines changed: 289 additions & 64 deletions

File tree

lib/fetch/headers.js

Lines changed: 152 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,35 @@
22

33
'use strict'
44

5-
const { validateHeaderName, validateHeaderValue } = require('http')
65
const { kHeadersList } = require('../core/symbols')
76
const { kGuard } = require('./symbols')
87
const { kEnumerableProperty } = require('../core/util')
9-
const { makeIterator } = require('./util')
8+
const {
9+
makeIterator,
10+
isValidHeaderName,
11+
isValidHeaderValue
12+
} = require('./util')
13+
const { webidl } = require('./webidl')
1014

1115
const kHeadersMap = Symbol('headers map')
1216
const kHeadersSortedMap = Symbol('headers map sorted')
1317

14-
function normalizeAndValidateHeaderName (name) {
15-
if (name === undefined) {
16-
throw new TypeError(`Header name ${name}`)
17-
}
18-
const normalizedHeaderName = name.toLocaleLowerCase()
19-
validateHeaderName(normalizedHeaderName)
20-
return normalizedHeaderName
21-
}
22-
23-
function normalizeAndValidateHeaderValue (name, value) {
24-
if (value === undefined) {
25-
throw new TypeError(value, name)
26-
}
27-
const normalizedHeaderValue = `${value}`.replace(
28-
/^[\n\t\r\x20]+|[\n\t\r\x20]+$/g,
18+
/**
19+
* @see https://fetch.spec.whatwg.org/#concept-header-value-normalize
20+
* @param {string} potentialValue
21+
*/
22+
function headerValueNormalize (potentialValue) {
23+
// To normalize a byte sequence potentialValue, remove
24+
// any leading and trailing HTTP whitespace bytes from
25+
// potentialValue.
26+
return potentialValue.replace(
27+
/^[\r\n\t ]+|[\r\n\t ]+$/g,
2928
''
3029
)
31-
validateHeaderValue(name, normalizedHeaderValue)
32-
return normalizedHeaderValue
3330
}
3431

3532
function fill (headers, object) {
33+
// TODO: use idl converters once implemented
3634
// To fill a Headers object headers with a given object object, run these steps:
3735

3836
if (object[Symbol.iterator]) {
@@ -85,48 +83,75 @@ class HeadersList {
8583
}
8684
}
8785

86+
// https://fetch.spec.whatwg.org/#header-list-contains
87+
contains (name) {
88+
// A header list list contains a header name name if list
89+
// contains a header whose name is a byte-case-insensitive
90+
// match for name.
91+
name = name.toLowerCase()
92+
93+
return this[kHeadersMap].has(name)
94+
}
95+
8896
clear () {
8997
this[kHeadersMap].clear()
9098
this[kHeadersSortedMap] = null
9199
}
92100

101+
// https://fetch.spec.whatwg.org/#concept-header-list-append
93102
append (name, value) {
94103
this[kHeadersSortedMap] = null
95104

96-
const normalizedName = normalizeAndValidateHeaderName(name)
97-
const normalizedValue = normalizeAndValidateHeaderValue(name, value)
98-
99-
const exists = this[kHeadersMap].get(normalizedName)
105+
// 1. If list contains name, then set name to the first such
106+
// header’s name.
107+
name = name.toLowerCase()
108+
const exists = this[kHeadersMap].get(name)
100109

110+
// 2. Append (name, value) to list.
101111
if (exists) {
102-
this[kHeadersMap].set(normalizedName, `${exists}, ${normalizedValue}`)
112+
this[kHeadersMap].set(name, `${exists}, ${value}`)
103113
} else {
104-
this[kHeadersMap].set(normalizedName, `${normalizedValue}`)
114+
this[kHeadersMap].set(name, `${value}`)
105115
}
106116
}
107117

118+
// https://fetch.spec.whatwg.org/#concept-header-list-set
108119
set (name, value) {
109120
this[kHeadersSortedMap] = null
110121

111-
const normalizedName = normalizeAndValidateHeaderName(name)
112-
return this[kHeadersMap].set(normalizedName, value)
122+
// 1. If list contains name, then set the value of
123+
// the first such header to value and remove the
124+
// others.
125+
// 2. Otherwise, append header (name, value) to list.
126+
return this[kHeadersMap].set(name, value)
113127
}
114128

129+
// https://fetch.spec.whatwg.org/#concept-header-list-delete
115130
delete (name) {
116131
this[kHeadersSortedMap] = null
117132

118-
const normalizedName = normalizeAndValidateHeaderName(name)
119-
return this[kHeadersMap].delete(normalizedName)
133+
name = name.toLowerCase()
134+
return this[kHeadersMap].delete(name)
120135
}
121136

137+
// https://fetch.spec.whatwg.org/#concept-header-list-get
122138
get (name) {
123-
const normalizedName = normalizeAndValidateHeaderName(name)
124-
return this[kHeadersMap].get(normalizedName) ?? null
139+
name = name.toLowerCase()
140+
141+
// 1. If list does not contain name, then return null.
142+
if (!this.contains(name)) {
143+
return null
144+
}
145+
146+
// 2. Return the values of all headers in list whose name
147+
// is a byte-case-insensitive match for name,
148+
// separated from each other by 0x2C 0x20, in order.
149+
return this[kHeadersMap].get(name) ?? null
125150
}
126151

127152
has (name) {
128-
const normalizedName = normalizeAndValidateHeaderName(name)
129-
return this[kHeadersMap].has(normalizedName)
153+
name = name.toLowerCase()
154+
return this[kHeadersMap].has(name)
130155
}
131156

132157
keys () {
@@ -186,14 +211,36 @@ class Headers {
186211
)
187212
}
188213

214+
name = webidl.converters.ByteString(name)
215+
value = webidl.converters.ByteString(value)
216+
217+
// 1. Normalize value.
218+
value = headerValueNormalize(value)
219+
220+
// 2. If name is not a header name or value is not a
221+
// header value, then throw a TypeError.
222+
if (!isValidHeaderName(name) || !isValidHeaderValue(value)) {
223+
throw new TypeError()
224+
}
225+
226+
// 3. If headers’s guard is "immutable", then throw a TypeError.
227+
// 4. Otherwise, if headers’s guard is "request" and name is a
228+
// forbidden header name, return.
189229
// Note: undici does not implement forbidden header names
190230
if (this[kGuard] === 'immutable') {
191231
throw new TypeError('immutable')
192232
} else if (this[kGuard] === 'request-no-cors') {
233+
// 5. Otherwise, if headers’s guard is "request-no-cors":
193234
// TODO
194235
}
195236

196-
return this[kHeadersList].append(String(name), String(value))
237+
// 6. Otherwise, if headers’s guard is "response" and name is a
238+
// forbidden response-header name, return.
239+
240+
// 7. Append (name, value) to headers’s header list.
241+
// 8. If headers’s guard is "request-no-cors", then remove
242+
// privileged no-CORS request headers from headers
243+
return this[kHeadersList].append(name, value)
197244
}
198245

199246
// https://fetch.spec.whatwg.org/#dom-headers-delete
@@ -208,14 +255,39 @@ class Headers {
208255
)
209256
}
210257

258+
name = webidl.converters.ByteString(name)
259+
260+
// 1. If name is not a header name, then throw a TypeError.
261+
if (!isValidHeaderName(name)) {
262+
throw new TypeError()
263+
}
264+
265+
// 2. If this’s guard is "immutable", then throw a TypeError.
266+
// 3. Otherwise, if this’s guard is "request" and name is a
267+
// forbidden header name, return.
268+
// 4. Otherwise, if this’s guard is "request-no-cors", name
269+
// is not a no-CORS-safelisted request-header name, and
270+
// name is not a privileged no-CORS request-header name,
271+
// return.
272+
// 5. Otherwise, if this’s guard is "response" and name is
273+
// a forbidden response-header name, return.
211274
// Note: undici does not implement forbidden header names
212275
if (this[kGuard] === 'immutable') {
213276
throw new TypeError('immutable')
214277
} else if (this[kGuard] === 'request-no-cors') {
215278
// TODO
216279
}
217280

218-
return this[kHeadersList].delete(String(name))
281+
// 6. If this’s header list does not contain name, then
282+
// return.
283+
if (!this[kHeadersList].contains(name)) {
284+
return
285+
}
286+
287+
// 7. Delete name from this’s header list.
288+
// 8. If this’s guard is "request-no-cors", then remove
289+
// privileged no-CORS request headers from this.
290+
return this[kHeadersList].delete(name)
219291
}
220292

221293
// https://fetch.spec.whatwg.org/#dom-headers-get
@@ -230,7 +302,16 @@ class Headers {
230302
)
231303
}
232304

233-
return this[kHeadersList].get(String(name))
305+
name = webidl.converters.ByteString(name)
306+
307+
// 1. If name is not a header name, then throw a TypeError.
308+
if (!isValidHeaderName(name)) {
309+
throw new TypeError()
310+
}
311+
312+
// 2. Return the result of getting name from this’s header
313+
// list.
314+
return this[kHeadersList].get(name)
234315
}
235316

236317
// https://fetch.spec.whatwg.org/#dom-headers-has
@@ -245,7 +326,16 @@ class Headers {
245326
)
246327
}
247328

248-
return this[kHeadersList].has(String(name))
329+
name = webidl.converters.ByteString(name)
330+
331+
// 1. If name is not a header name, then throw a TypeError.
332+
if (!isValidHeaderName(name)) {
333+
throw new TypeError()
334+
}
335+
336+
// 2. Return true if this’s header list contains name;
337+
// otherwise false.
338+
return this[kHeadersList].contains(name)
249339
}
250340

251341
// https://fetch.spec.whatwg.org/#dom-headers-set
@@ -260,14 +350,37 @@ class Headers {
260350
)
261351
}
262352

353+
name = webidl.converters.ByteString(name)
354+
value = webidl.converters.ByteString(value)
355+
356+
// 1. Normalize value.
357+
value = headerValueNormalize(value)
358+
359+
// 2. If name is not a header name or value is not a
360+
// header value, then throw a TypeError.
361+
if (!isValidHeaderName(name) || !isValidHeaderValue(value)) {
362+
throw new TypeError()
363+
}
364+
365+
// 3. If this’s guard is "immutable", then throw a TypeError.
366+
// 4. Otherwise, if this’s guard is "request" and name is a
367+
// forbidden header name, return.
368+
// 5. Otherwise, if this’s guard is "request-no-cors" and
369+
// name/value is not a no-CORS-safelisted request-header,
370+
// return.
371+
// 6. Otherwise, if this’s guard is "response" and name is a
372+
// forbidden response-header name, return.
263373
// Note: undici does not implement forbidden header names
264374
if (this[kGuard] === 'immutable') {
265375
throw new TypeError('immutable')
266376
} else if (this[kGuard] === 'request-no-cors') {
267377
// TODO
268378
}
269379

270-
return this[kHeadersList].set(String(name), String(value))
380+
// 7. Set (name, value) in this’s header list.
381+
// 8. If this’s guard is "request-no-cors", then remove
382+
// privileged no-CORS request headers from this
383+
return this[kHeadersList].set(name, value)
271384
}
272385

273386
get [kHeadersSortedMap] () {
@@ -351,7 +464,5 @@ Object.defineProperties(Headers.prototype, {
351464
module.exports = {
352465
fill,
353466
Headers,
354-
HeadersList,
355-
normalizeAndValidateHeaderName,
356-
normalizeAndValidateHeaderValue
467+
HeadersList
357468
}

lib/fetch/util.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,49 @@ function isValidHTTPToken (characters) {
145145
return true
146146
}
147147

148+
// https://fetch.spec.whatwg.org/#header-name
149+
// https://github.com/chromium/chromium/blob/b3d37e6f94f87d59e44662d6078f6a12de845d17/net/http/http_util.cc#L342
150+
function isValidHeaderName (potentialValue) {
151+
if (potentialValue.length === 0) {
152+
return false
153+
}
154+
155+
for (const char of potentialValue) {
156+
if (!isValidHTTPToken(char)) {
157+
return false
158+
}
159+
}
160+
161+
return true
162+
}
163+
164+
/**
165+
* @see https://fetch.spec.whatwg.org/#header-value
166+
* @param {string} potentialValue
167+
*/
168+
function isValidHeaderValue (potentialValue) {
169+
// - Has no leading or trailing HTTP tab or space bytes.
170+
// - Contains no 0x00 (NUL) or HTTP newline bytes.
171+
if (
172+
potentialValue.startsWith('\t') ||
173+
potentialValue.startsWith(' ') ||
174+
potentialValue.endsWith('\t') ||
175+
potentialValue.endsWith(' ')
176+
) {
177+
return false
178+
}
179+
180+
if (
181+
potentialValue.includes('\0') ||
182+
potentialValue.includes('\r') ||
183+
potentialValue.includes('\n')
184+
) {
185+
return false
186+
}
187+
188+
return true
189+
}
190+
148191
// https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect
149192
function setRequestReferrerPolicyOnRedirect (request, actualResponse) {
150193
// Given a request request and a response actualResponse, this algorithm
@@ -418,5 +461,7 @@ module.exports = {
418461
sameOrigin,
419462
normalizeMethod,
420463
serializeJavascriptValueToJSONString,
421-
makeIterator
464+
makeIterator,
465+
isValidHeaderName,
466+
isValidHeaderValue
422467
}

0 commit comments

Comments
 (0)