source: node_modules/undici/lib/fetch/headers.js

main
Last change on this file was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

  • Property mode set to 100644
File size: 16.7 KB
Line 
1// https://github.com/Ethan-Arrowood/undici-fetch
2
3'use strict'
4
5const { kHeadersList, kConstruct } = require('../core/symbols')
6const { kGuard } = require('./symbols')
7const { kEnumerableProperty } = require('../core/util')
8const {
9 makeIterator,
10 isValidHeaderName,
11 isValidHeaderValue
12} = require('./util')
13const { webidl } = require('./webidl')
14const assert = require('assert')
15
16const kHeadersMap = Symbol('headers map')
17const kHeadersSortedMap = Symbol('headers map sorted')
18
19/**
20 * @param {number} code
21 */
22function isHTTPWhiteSpaceCharCode (code) {
23 return code === 0x00a || code === 0x00d || code === 0x009 || code === 0x020
24}
25
26/**
27 * @see https://fetch.spec.whatwg.org/#concept-header-value-normalize
28 * @param {string} potentialValue
29 */
30function headerValueNormalize (potentialValue) {
31 // To normalize a byte sequence potentialValue, remove
32 // any leading and trailing HTTP whitespace bytes from
33 // potentialValue.
34 let i = 0; let j = potentialValue.length
35
36 while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(j - 1))) --j
37 while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i
38
39 return i === 0 && j === potentialValue.length ? potentialValue : potentialValue.substring(i, j)
40}
41
42function fill (headers, object) {
43 // To fill a Headers object headers with a given object object, run these steps:
44
45 // 1. If object is a sequence, then for each header in object:
46 // Note: webidl conversion to array has already been done.
47 if (Array.isArray(object)) {
48 for (let i = 0; i < object.length; ++i) {
49 const header = object[i]
50 // 1. If header does not contain exactly two items, then throw a TypeError.
51 if (header.length !== 2) {
52 throw webidl.errors.exception({
53 header: 'Headers constructor',
54 message: `expected name/value pair to be length 2, found ${header.length}.`
55 })
56 }
57
58 // 2. Append (header’s first item, header’s second item) to headers.
59 appendHeader(headers, header[0], header[1])
60 }
61 } else if (typeof object === 'object' && object !== null) {
62 // Note: null should throw
63
64 // 2. Otherwise, object is a record, then for each key → value in object,
65 // append (key, value) to headers
66 const keys = Object.keys(object)
67 for (let i = 0; i < keys.length; ++i) {
68 appendHeader(headers, keys[i], object[keys[i]])
69 }
70 } else {
71 throw webidl.errors.conversionFailed({
72 prefix: 'Headers constructor',
73 argument: 'Argument 1',
74 types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
75 })
76 }
77}
78
79/**
80 * @see https://fetch.spec.whatwg.org/#concept-headers-append
81 */
82function appendHeader (headers, name, value) {
83 // 1. Normalize value.
84 value = headerValueNormalize(value)
85
86 // 2. If name is not a header name or value is not a
87 // header value, then throw a TypeError.
88 if (!isValidHeaderName(name)) {
89 throw webidl.errors.invalidArgument({
90 prefix: 'Headers.append',
91 value: name,
92 type: 'header name'
93 })
94 } else if (!isValidHeaderValue(value)) {
95 throw webidl.errors.invalidArgument({
96 prefix: 'Headers.append',
97 value,
98 type: 'header value'
99 })
100 }
101
102 // 3. If headers’s guard is "immutable", then throw a TypeError.
103 // 4. Otherwise, if headers’s guard is "request" and name is a
104 // forbidden header name, return.
105 // Note: undici does not implement forbidden header names
106 if (headers[kGuard] === 'immutable') {
107 throw new TypeError('immutable')
108 } else if (headers[kGuard] === 'request-no-cors') {
109 // 5. Otherwise, if headers’s guard is "request-no-cors":
110 // TODO
111 }
112
113 // 6. Otherwise, if headers’s guard is "response" and name is a
114 // forbidden response-header name, return.
115
116 // 7. Append (name, value) to headers’s header list.
117 return headers[kHeadersList].append(name, value)
118
119 // 8. If headers’s guard is "request-no-cors", then remove
120 // privileged no-CORS request headers from headers
121}
122
123class HeadersList {
124 /** @type {[string, string][]|null} */
125 cookies = null
126
127 constructor (init) {
128 if (init instanceof HeadersList) {
129 this[kHeadersMap] = new Map(init[kHeadersMap])
130 this[kHeadersSortedMap] = init[kHeadersSortedMap]
131 this.cookies = init.cookies === null ? null : [...init.cookies]
132 } else {
133 this[kHeadersMap] = new Map(init)
134 this[kHeadersSortedMap] = null
135 }
136 }
137
138 // https://fetch.spec.whatwg.org/#header-list-contains
139 contains (name) {
140 // A header list list contains a header name name if list
141 // contains a header whose name is a byte-case-insensitive
142 // match for name.
143 name = name.toLowerCase()
144
145 return this[kHeadersMap].has(name)
146 }
147
148 clear () {
149 this[kHeadersMap].clear()
150 this[kHeadersSortedMap] = null
151 this.cookies = null
152 }
153
154 // https://fetch.spec.whatwg.org/#concept-header-list-append
155 append (name, value) {
156 this[kHeadersSortedMap] = null
157
158 // 1. If list contains name, then set name to the first such
159 // header’s name.
160 const lowercaseName = name.toLowerCase()
161 const exists = this[kHeadersMap].get(lowercaseName)
162
163 // 2. Append (name, value) to list.
164 if (exists) {
165 const delimiter = lowercaseName === 'cookie' ? '; ' : ', '
166 this[kHeadersMap].set(lowercaseName, {
167 name: exists.name,
168 value: `${exists.value}${delimiter}${value}`
169 })
170 } else {
171 this[kHeadersMap].set(lowercaseName, { name, value })
172 }
173
174 if (lowercaseName === 'set-cookie') {
175 this.cookies ??= []
176 this.cookies.push(value)
177 }
178 }
179
180 // https://fetch.spec.whatwg.org/#concept-header-list-set
181 set (name, value) {
182 this[kHeadersSortedMap] = null
183 const lowercaseName = name.toLowerCase()
184
185 if (lowercaseName === 'set-cookie') {
186 this.cookies = [value]
187 }
188
189 // 1. If list contains name, then set the value of
190 // the first such header to value and remove the
191 // others.
192 // 2. Otherwise, append header (name, value) to list.
193 this[kHeadersMap].set(lowercaseName, { name, value })
194 }
195
196 // https://fetch.spec.whatwg.org/#concept-header-list-delete
197 delete (name) {
198 this[kHeadersSortedMap] = null
199
200 name = name.toLowerCase()
201
202 if (name === 'set-cookie') {
203 this.cookies = null
204 }
205
206 this[kHeadersMap].delete(name)
207 }
208
209 // https://fetch.spec.whatwg.org/#concept-header-list-get
210 get (name) {
211 const value = this[kHeadersMap].get(name.toLowerCase())
212
213 // 1. If list does not contain name, then return null.
214 // 2. Return the values of all headers in list whose name
215 // is a byte-case-insensitive match for name,
216 // separated from each other by 0x2C 0x20, in order.
217 return value === undefined ? null : value.value
218 }
219
220 * [Symbol.iterator] () {
221 // use the lowercased name
222 for (const [name, { value }] of this[kHeadersMap]) {
223 yield [name, value]
224 }
225 }
226
227 get entries () {
228 const headers = {}
229
230 if (this[kHeadersMap].size) {
231 for (const { name, value } of this[kHeadersMap].values()) {
232 headers[name] = value
233 }
234 }
235
236 return headers
237 }
238}
239
240// https://fetch.spec.whatwg.org/#headers-class
241class Headers {
242 constructor (init = undefined) {
243 if (init === kConstruct) {
244 return
245 }
246 this[kHeadersList] = new HeadersList()
247
248 // The new Headers(init) constructor steps are:
249
250 // 1. Set this’s guard to "none".
251 this[kGuard] = 'none'
252
253 // 2. If init is given, then fill this with init.
254 if (init !== undefined) {
255 init = webidl.converters.HeadersInit(init)
256 fill(this, init)
257 }
258 }
259
260 // https://fetch.spec.whatwg.org/#dom-headers-append
261 append (name, value) {
262 webidl.brandCheck(this, Headers)
263
264 webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.append' })
265
266 name = webidl.converters.ByteString(name)
267 value = webidl.converters.ByteString(value)
268
269 return appendHeader(this, name, value)
270 }
271
272 // https://fetch.spec.whatwg.org/#dom-headers-delete
273 delete (name) {
274 webidl.brandCheck(this, Headers)
275
276 webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.delete' })
277
278 name = webidl.converters.ByteString(name)
279
280 // 1. If name is not a header name, then throw a TypeError.
281 if (!isValidHeaderName(name)) {
282 throw webidl.errors.invalidArgument({
283 prefix: 'Headers.delete',
284 value: name,
285 type: 'header name'
286 })
287 }
288
289 // 2. If this’s guard is "immutable", then throw a TypeError.
290 // 3. Otherwise, if this’s guard is "request" and name is a
291 // forbidden header name, return.
292 // 4. Otherwise, if this’s guard is "request-no-cors", name
293 // is not a no-CORS-safelisted request-header name, and
294 // name is not a privileged no-CORS request-header name,
295 // return.
296 // 5. Otherwise, if this’s guard is "response" and name is
297 // a forbidden response-header name, return.
298 // Note: undici does not implement forbidden header names
299 if (this[kGuard] === 'immutable') {
300 throw new TypeError('immutable')
301 } else if (this[kGuard] === 'request-no-cors') {
302 // TODO
303 }
304
305 // 6. If this’s header list does not contain name, then
306 // return.
307 if (!this[kHeadersList].contains(name)) {
308 return
309 }
310
311 // 7. Delete name from this’s header list.
312 // 8. If this’s guard is "request-no-cors", then remove
313 // privileged no-CORS request headers from this.
314 this[kHeadersList].delete(name)
315 }
316
317 // https://fetch.spec.whatwg.org/#dom-headers-get
318 get (name) {
319 webidl.brandCheck(this, Headers)
320
321 webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.get' })
322
323 name = webidl.converters.ByteString(name)
324
325 // 1. If name is not a header name, then throw a TypeError.
326 if (!isValidHeaderName(name)) {
327 throw webidl.errors.invalidArgument({
328 prefix: 'Headers.get',
329 value: name,
330 type: 'header name'
331 })
332 }
333
334 // 2. Return the result of getting name from this’s header
335 // list.
336 return this[kHeadersList].get(name)
337 }
338
339 // https://fetch.spec.whatwg.org/#dom-headers-has
340 has (name) {
341 webidl.brandCheck(this, Headers)
342
343 webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.has' })
344
345 name = webidl.converters.ByteString(name)
346
347 // 1. If name is not a header name, then throw a TypeError.
348 if (!isValidHeaderName(name)) {
349 throw webidl.errors.invalidArgument({
350 prefix: 'Headers.has',
351 value: name,
352 type: 'header name'
353 })
354 }
355
356 // 2. Return true if this’s header list contains name;
357 // otherwise false.
358 return this[kHeadersList].contains(name)
359 }
360
361 // https://fetch.spec.whatwg.org/#dom-headers-set
362 set (name, value) {
363 webidl.brandCheck(this, Headers)
364
365 webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.set' })
366
367 name = webidl.converters.ByteString(name)
368 value = webidl.converters.ByteString(value)
369
370 // 1. Normalize value.
371 value = headerValueNormalize(value)
372
373 // 2. If name is not a header name or value is not a
374 // header value, then throw a TypeError.
375 if (!isValidHeaderName(name)) {
376 throw webidl.errors.invalidArgument({
377 prefix: 'Headers.set',
378 value: name,
379 type: 'header name'
380 })
381 } else if (!isValidHeaderValue(value)) {
382 throw webidl.errors.invalidArgument({
383 prefix: 'Headers.set',
384 value,
385 type: 'header value'
386 })
387 }
388
389 // 3. If this’s guard is "immutable", then throw a TypeError.
390 // 4. Otherwise, if this’s guard is "request" and name is a
391 // forbidden header name, return.
392 // 5. Otherwise, if this’s guard is "request-no-cors" and
393 // name/value is not a no-CORS-safelisted request-header,
394 // return.
395 // 6. Otherwise, if this’s guard is "response" and name is a
396 // forbidden response-header name, return.
397 // Note: undici does not implement forbidden header names
398 if (this[kGuard] === 'immutable') {
399 throw new TypeError('immutable')
400 } else if (this[kGuard] === 'request-no-cors') {
401 // TODO
402 }
403
404 // 7. Set (name, value) in this’s header list.
405 // 8. If this’s guard is "request-no-cors", then remove
406 // privileged no-CORS request headers from this
407 this[kHeadersList].set(name, value)
408 }
409
410 // https://fetch.spec.whatwg.org/#dom-headers-getsetcookie
411 getSetCookie () {
412 webidl.brandCheck(this, Headers)
413
414 // 1. If this’s header list does not contain `Set-Cookie`, then return « ».
415 // 2. Return the values of all headers in this’s header list whose name is
416 // a byte-case-insensitive match for `Set-Cookie`, in order.
417
418 const list = this[kHeadersList].cookies
419
420 if (list) {
421 return [...list]
422 }
423
424 return []
425 }
426
427 // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
428 get [kHeadersSortedMap] () {
429 if (this[kHeadersList][kHeadersSortedMap]) {
430 return this[kHeadersList][kHeadersSortedMap]
431 }
432
433 // 1. Let headers be an empty list of headers with the key being the name
434 // and value the value.
435 const headers = []
436
437 // 2. Let names be the result of convert header names to a sorted-lowercase
438 // set with all the names of the headers in list.
439 const names = [...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1)
440 const cookies = this[kHeadersList].cookies
441
442 // 3. For each name of names:
443 for (let i = 0; i < names.length; ++i) {
444 const [name, value] = names[i]
445 // 1. If name is `set-cookie`, then:
446 if (name === 'set-cookie') {
447 // 1. Let values be a list of all values of headers in list whose name
448 // is a byte-case-insensitive match for name, in order.
449
450 // 2. For each value of values:
451 // 1. Append (name, value) to headers.
452 for (let j = 0; j < cookies.length; ++j) {
453 headers.push([name, cookies[j]])
454 }
455 } else {
456 // 2. Otherwise:
457
458 // 1. Let value be the result of getting name from list.
459
460 // 2. Assert: value is non-null.
461 assert(value !== null)
462
463 // 3. Append (name, value) to headers.
464 headers.push([name, value])
465 }
466 }
467
468 this[kHeadersList][kHeadersSortedMap] = headers
469
470 // 4. Return headers.
471 return headers
472 }
473
474 keys () {
475 webidl.brandCheck(this, Headers)
476
477 if (this[kGuard] === 'immutable') {
478 const value = this[kHeadersSortedMap]
479 return makeIterator(() => value, 'Headers',
480 'key')
481 }
482
483 return makeIterator(
484 () => [...this[kHeadersSortedMap].values()],
485 'Headers',
486 'key'
487 )
488 }
489
490 values () {
491 webidl.brandCheck(this, Headers)
492
493 if (this[kGuard] === 'immutable') {
494 const value = this[kHeadersSortedMap]
495 return makeIterator(() => value, 'Headers',
496 'value')
497 }
498
499 return makeIterator(
500 () => [...this[kHeadersSortedMap].values()],
501 'Headers',
502 'value'
503 )
504 }
505
506 entries () {
507 webidl.brandCheck(this, Headers)
508
509 if (this[kGuard] === 'immutable') {
510 const value = this[kHeadersSortedMap]
511 return makeIterator(() => value, 'Headers',
512 'key+value')
513 }
514
515 return makeIterator(
516 () => [...this[kHeadersSortedMap].values()],
517 'Headers',
518 'key+value'
519 )
520 }
521
522 /**
523 * @param {(value: string, key: string, self: Headers) => void} callbackFn
524 * @param {unknown} thisArg
525 */
526 forEach (callbackFn, thisArg = globalThis) {
527 webidl.brandCheck(this, Headers)
528
529 webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.forEach' })
530
531 if (typeof callbackFn !== 'function') {
532 throw new TypeError(
533 "Failed to execute 'forEach' on 'Headers': parameter 1 is not of type 'Function'."
534 )
535 }
536
537 for (const [key, value] of this) {
538 callbackFn.apply(thisArg, [value, key, this])
539 }
540 }
541
542 [Symbol.for('nodejs.util.inspect.custom')] () {
543 webidl.brandCheck(this, Headers)
544
545 return this[kHeadersList]
546 }
547}
548
549Headers.prototype[Symbol.iterator] = Headers.prototype.entries
550
551Object.defineProperties(Headers.prototype, {
552 append: kEnumerableProperty,
553 delete: kEnumerableProperty,
554 get: kEnumerableProperty,
555 has: kEnumerableProperty,
556 set: kEnumerableProperty,
557 getSetCookie: kEnumerableProperty,
558 keys: kEnumerableProperty,
559 values: kEnumerableProperty,
560 entries: kEnumerableProperty,
561 forEach: kEnumerableProperty,
562 [Symbol.iterator]: { enumerable: false },
563 [Symbol.toStringTag]: {
564 value: 'Headers',
565 configurable: true
566 }
567})
568
569webidl.converters.HeadersInit = function (V) {
570 if (webidl.util.Type(V) === 'Object') {
571 if (V[Symbol.iterator]) {
572 return webidl.converters['sequence<sequence<ByteString>>'](V)
573 }
574
575 return webidl.converters['record<ByteString, ByteString>'](V)
576 }
577
578 throw webidl.errors.conversionFailed({
579 prefix: 'Headers constructor',
580 argument: 'Argument 1',
581 types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
582 })
583}
584
585module.exports = {
586 fill,
587 Headers,
588 HeadersList
589}
Note: See TracBrowser for help on using the repository browser.