codekingpro/portable-devtools
114k
1// https://github.com/Ethan-Arrowood/undici-fetch
2
3'use strict'
4
5const { kConstruct } = require('../../core/symbols')
6const { kEnumerableProperty } = require('../../core/util')
7const {
8 iteratorMixin,
9 isValidHeaderName,
10 isValidHeaderValue
11} = require('./util')
12const { webidl } = require('./webidl')
13const assert = require('node:assert')
14const util = require('node:util')
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 // 5. Otherwise, if headers’s guard is "request-no-cors":
106 // TODO
107 // Note: undici does not implement forbidden header names
108 if (getHeadersGuard(headers) === 'immutable') {
109 throw new TypeError('immutable')
110 }
111
112 // 6. Otherwise, if headers’s guard is "response" and name is a
113 // forbidden response-header name, return.
114
115 // 7. Append (name, value) to headers’s header list.
116 return getHeadersList(headers).append(name, value, false)
117
118 // 8. If headers’s guard is "request-no-cors", then remove
119 // privileged no-CORS request headers from headers
120}
121
122function compareHeaderName (a, b) {
123 return a[0] < b[0] ? -1 : 1
124}
125
126class HeadersList {
127 /** @type {[string, string][]|null} */
128 cookies = null
129
130 constructor (init) {
131 if (init instanceof HeadersList) {
132 this[kHeadersMap] = new Map(init[kHeadersMap])
133 this[kHeadersSortedMap] = init[kHeadersSortedMap]
134 this.cookies = init.cookies === null ? null : [...init.cookies]
135 } else {
136 this[kHeadersMap] = new Map(init)
137 this[kHeadersSortedMap] = null
138 }
139 }
140
141 /**
142 * @see https://fetch.spec.whatwg.org/#header-list-contains
143 * @param {string} name
144 * @param {boolean} isLowerCase
145 */
146 contains (name, isLowerCase) {
147 // A header list list contains a header name name if list
148 // contains a header whose name is a byte-case-insensitive
149 // match for name.
150
151 return this[kHeadersMap].has(isLowerCase ? name : name.toLowerCase())
152 }
153
154 clear () {
155 this[kHeadersMap].clear()
156 this[kHeadersSortedMap] = null
157 this.cookies = null
158 }
159
160 /**
161 * @see https://fetch.spec.whatwg.org/#concept-header-list-append
162 * @param {string} name
163 * @param {string} value
164 * @param {boolean} isLowerCase
165 */
166 append (name, value, isLowerCase) {
167 this[kHeadersSortedMap] = null
168
169 // 1. If list contains name, then set name to the first such
170 // header’s name.
171 const lowercaseName = isLowerCase ? name : name.toLowerCase()
172 const exists = this[kHeadersMap].get(lowercaseName)
173
174 // 2. Append (name, value) to list.
175 if (exists) {
176 const delimiter = lowercaseName === 'cookie' ? '; ' : ', '
177 this[kHeadersMap].set(lowercaseName, {
178 name: exists.name,
179 value: `${exists.value}${delimiter}${value}`
180 })
181 } else {
182 this[kHeadersMap].set(lowercaseName, { name, value })
183 }
184
185 if (lowercaseName === 'set-cookie') {
186 (this.cookies ??= []).push(value)
187 }
188 }
189
190 /**
191 * @see https://fetch.spec.whatwg.org/#concept-header-list-set
192 * @param {string} name
193 * @param {string} value
194 * @param {boolean} isLowerCase
195 */
196 set (name, value, isLowerCase) {
197 this[kHeadersSortedMap] = null
198 const lowercaseName = isLowerCase ? name : name.toLowerCase()
199
200 if (lowercaseName === 'set-cookie') {
201 this.cookies = [value]
202 }
203
204 // 1. If list contains name, then set the value of
205 // the first such header to value and remove the
206 // others.
207 // 2. Otherwise, append header (name, value) to list.
208 this[kHeadersMap].set(lowercaseName, { name, value })
209 }
210
211 /**
212 * @see https://fetch.spec.whatwg.org/#concept-header-list-delete
213 * @param {string} name
214 * @param {boolean} isLowerCase
215 */
216 delete (name, isLowerCase) {
217 this[kHeadersSortedMap] = null
218 if (!isLowerCase) name = name.toLowerCase()
219
220 if (name === 'set-cookie') {
221 this.cookies = null
222 }
223
224 this[kHeadersMap].delete(name)
225 }
226
227 /**
228 * @see https://fetch.spec.whatwg.org/#concept-header-list-get
229 * @param {string} name
230 * @param {boolean} isLowerCase
231 * @returns {string | null}
232 */
233 get (name, isLowerCase) {
234 // 1. If list does not contain name, then return null.
235 // 2. Return the values of all headers in list whose name
236 // is a byte-case-insensitive match for name,
237 // separated from each other by 0x2C 0x20, in order.
238 return this[kHeadersMap].get(isLowerCase ? name : name.toLowerCase())?.value ?? null
239 }
240
241 * [Symbol.iterator] () {
242 // use the lowercased name
243 for (const { 0: name, 1: { value } } of this[kHeadersMap]) {
244 yield [name, value]
245 }
246 }
247
248 get entries () {
249 const headers = {}
250
251 if (this[kHeadersMap].size !== 0) {
252 for (const { name, value } of this[kHeadersMap].values()) {
253 headers[name] = value
254 }
255 }
256
257 return headers
258 }
259
260 rawValues () {
261 return this[kHeadersMap].values()
262 }
263
264 get entriesList () {
265 const headers = []
266
267 if (this[kHeadersMap].size !== 0) {
268 for (const { 0: lowerName, 1: { name, value } } of this[kHeadersMap]) {
269 if (lowerName === 'set-cookie') {
270 for (const cookie of this.cookies) {
271 headers.push([name, cookie])
272 }
273 } else {
274 headers.push([name, value])
275 }
276 }
277 }
278
279 return headers
280 }
281
282 // https://fetch.spec.whatwg.org/#convert-header-names-to-a-sorted-lowercase-set
283 toSortedArray () {
284 const size = this[kHeadersMap].size
285 const array = new Array(size)
286 // In most cases, you will use the fast-path.
287 // fast-path: Use binary insertion sort for small arrays.
288 if (size <= 32) {
289 if (size === 0) {
290 // If empty, it is an empty array. To avoid the first index assignment.
291 return array
292 }
293 // Improve performance by unrolling loop and avoiding double-loop.
294 // Double-loop-less version of the binary insertion sort.
295 const iterator = this[kHeadersMap][Symbol.iterator]()
296 const firstValue = iterator.next().value
297 // set [name, value] to first index.
298 array[0] = [firstValue[0], firstValue[1].value]
299 // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
300 // 3.2.2. Assert: value is non-null.
301 assert(firstValue[1].value !== null)
302 for (
303 let i = 1, j = 0, right = 0, left = 0, pivot = 0, x, value;
304 i < size;
305 ++i
306 ) {
307 // get next value
308 value = iterator.next().value
309 // set [name, value] to current index.
310 x = array[i] = [value[0], value[1].value]
311 // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
312 // 3.2.2. Assert: value is non-null.
313 assert(x[1] !== null)
314 left = 0
315 right = i
316 // binary search
317 while (left < right) {
318 // middle index
319 pivot = left + ((right - left) >> 1)
320 // compare header name
321 if (array[pivot][0] <= x[0]) {
322 left = pivot + 1
323 } else {
324 right = pivot
325 }
326 }
327 if (i !== pivot) {
328 j = i
329 while (j > left) {
330 array[j] = array[--j]
331 }
332 array[left] = x
333 }
334 }
335 /* c8 ignore next 4 */
336 if (!iterator.next().done) {
337 // This is for debugging and will never be called.
338 throw new TypeError('Unreachable')
339 }
340 return array
341 } else {
342 // This case would be a rare occurrence.
343 // slow-path: fallback
344 let i = 0
345 for (const { 0: name, 1: { value } } of this[kHeadersMap]) {
346 array[i++] = [name, value]
347 // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
348 // 3.2.2. Assert: value is non-null.
349 assert(value !== null)
350 }
351 return array.sort(compareHeaderName)
352 }
353 }
354}
355
356// https://fetch.spec.whatwg.org/#headers-class
357class Headers {
358 #guard
359 #headersList
360
361 constructor (init = undefined) {
362 webidl.util.markAsUncloneable(this)
363
364 if (init === kConstruct) {
365 return
366 }
367
368 this.#headersList = new HeadersList()
369
370 // The new Headers(init) constructor steps are:
371
372 // 1. Set this’s guard to "none".
373 this.#guard = 'none'
374
375 // 2. If init is given, then fill this with init.
376 if (init !== undefined) {
377 init = webidl.converters.HeadersInit(init, 'Headers contructor', 'init')
378 fill(this, init)
379 }
380 }
381
382 // https://fetch.spec.whatwg.org/#dom-headers-append
383 append (name, value) {
384 webidl.brandCheck(this, Headers)
385
386 webidl.argumentLengthCheck(arguments, 2, 'Headers.append')
387
388 const prefix = 'Headers.append'
389 name = webidl.converters.ByteString(name, prefix, 'name')
390 value = webidl.converters.ByteString(value, prefix, 'value')
391
392 return appendHeader(this, name, value)
393 }
394
395 // https://fetch.spec.whatwg.org/#dom-headers-delete
396 delete (name) {
397 webidl.brandCheck(this, Headers)
398
399 webidl.argumentLengthCheck(arguments, 1, 'Headers.delete')
400
401 const prefix = 'Headers.delete'
402 name = webidl.converters.ByteString(name, prefix, 'name')
403
404 // 1. If name is not a header name, then throw a TypeError.
405 if (!isValidHeaderName(name)) {
406 throw webidl.errors.invalidArgument({
407 prefix: 'Headers.delete',
408 value: name,
409 type: 'header name'
410 })
411 }
412
413 // 2. If this’s guard is "immutable", then throw a TypeError.
414 // 3. Otherwise, if this’s guard is "request" and name is a
415 // forbidden header name, return.
416 // 4. Otherwise, if this’s guard is "request-no-cors", name
417 // is not a no-CORS-safelisted request-header name, and
418 // name is not a privileged no-CORS request-header name,
419 // return.
420 // 5. Otherwise, if this’s guard is "response" and name is
421 // a forbidden response-header name, return.
422 // Note: undici does not implement forbidden header names
423 if (this.#guard === 'immutable') {
424 throw new TypeError('immutable')
425 }
426
427 // 6. If this’s header list does not contain name, then
428 // return.
429 if (!this.#headersList.contains(name, false)) {
430 return
431 }
432
433 // 7. Delete name from this’s header list.
434 // 8. If this’s guard is "request-no-cors", then remove
435 // privileged no-CORS request headers from this.
436 this.#headersList.delete(name, false)
437 }
438
439 // https://fetch.spec.whatwg.org/#dom-headers-get
440 get (name) {
441 webidl.brandCheck(this, Headers)
442
443 webidl.argumentLengthCheck(arguments, 1, 'Headers.get')
444
445 const prefix = 'Headers.get'
446 name = webidl.converters.ByteString(name, prefix, 'name')
447
448 // 1. If name is not a header name, then throw a TypeError.
449 if (!isValidHeaderName(name)) {
450 throw webidl.errors.invalidArgument({
451 prefix,
452 value: name,
453 type: 'header name'
454 })
455 }
456
457 // 2. Return the result of getting name from this’s header
458 // list.
459 return this.#headersList.get(name, false)
460 }
461
462 // https://fetch.spec.whatwg.org/#dom-headers-has
463 has (name) {
464 webidl.brandCheck(this, Headers)
465
466 webidl.argumentLengthCheck(arguments, 1, 'Headers.has')
467
468 const prefix = 'Headers.has'
469 name = webidl.converters.ByteString(name, prefix, 'name')
470
471 // 1. If name is not a header name, then throw a TypeError.
472 if (!isValidHeaderName(name)) {
473 throw webidl.errors.invalidArgument({
474 prefix,
475 value: name,
476 type: 'header name'
477 })
478 }
479
480 // 2. Return true if this’s header list contains name;
481 // otherwise false.
482 return this.#headersList.contains(name, false)
483 }
484
485 // https://fetch.spec.whatwg.org/#dom-headers-set
486 set (name, value) {
487 webidl.brandCheck(this, Headers)
488
489 webidl.argumentLengthCheck(arguments, 2, 'Headers.set')
490
491 const prefix = 'Headers.set'
492 name = webidl.converters.ByteString(name, prefix, 'name')
493 value = webidl.converters.ByteString(value, prefix, 'value')
494
495 // 1. Normalize value.
496 value = headerValueNormalize(value)
497
498 // 2. If name is not a header name or value is not a
499 // header value, then throw a TypeError.
500 if (!isValidHeaderName(name)) {
501 throw webidl.errors.invalidArgument({
502 prefix,
503 value: name,
504 type: 'header name'
505 })
506 } else if (!isValidHeaderValue(value)) {
507 throw webidl.errors.invalidArgument({
508 prefix,
509 value,
510 type: 'header value'
511 })
512 }
513
514 // 3. If this’s guard is "immutable", then throw a TypeError.
515 // 4. Otherwise, if this’s guard is "request" and name is a
516 // forbidden header name, return.
517 // 5. Otherwise, if this’s guard is "request-no-cors" and
518 // name/value is not a no-CORS-safelisted request-header,
519 // return.
520 // 6. Otherwise, if this’s guard is "response" and name is a
521 // forbidden response-header name, return.
522 // Note: undici does not implement forbidden header names
523 if (this.#guard === 'immutable') {
524 throw new TypeError('immutable')
525 }
526
527 // 7. Set (name, value) in this’s header list.
528 // 8. If this’s guard is "request-no-cors", then remove
529 // privileged no-CORS request headers from this
530 this.#headersList.set(name, value, false)
531 }
532
533 // https://fetch.spec.whatwg.org/#dom-headers-getsetcookie
534 getSetCookie () {
535 webidl.brandCheck(this, Headers)
536
537 // 1. If this’s header list does not contain `Set-Cookie`, then return « ».
538 // 2. Return the values of all headers in this’s header list whose name is
539 // a byte-case-insensitive match for `Set-Cookie`, in order.
540
541 const list = this.#headersList.cookies
542
543 if (list) {
544 return [...list]
545 }
546
547 return []
548 }
549
550 // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
551 get [kHeadersSortedMap] () {
552 if (this.#headersList[kHeadersSortedMap]) {
553 return this.#headersList[kHeadersSortedMap]
554 }
555
556 // 1. Let headers be an empty list of headers with the key being the name
557 // and value the value.
558 const headers = []
559
560 // 2. Let names be the result of convert header names to a sorted-lowercase
561 // set with all the names of the headers in list.
562 const names = this.#headersList.toSortedArray()
563
564 const cookies = this.#headersList.cookies
565
566 // fast-path
567 if (cookies === null || cookies.length === 1) {
568 // Note: The non-null assertion of value has already been done by `HeadersList#toSortedArray`
569 return (this.#headersList[kHeadersSortedMap] = names)
570 }
571
572 // 3. For each name of names:
573 for (let i = 0; i < names.length; ++i) {
574 const { 0: name, 1: value } = names[i]
575 // 1. If name is `set-cookie`, then:
576 if (name === 'set-cookie') {
577 // 1. Let values be a list of all values of headers in list whose name
578 // is a byte-case-insensitive match for name, in order.
579
580 // 2. For each value of values:
581 // 1. Append (name, value) to headers.
582 for (let j = 0; j < cookies.length; ++j) {
583 headers.push([name, cookies[j]])
584 }
585 } else {
586 // 2. Otherwise:
587
588 // 1. Let value be the result of getting name from list.
589
590 // 2. Assert: value is non-null.
591 // Note: This operation was done by `HeadersList#toSortedArray`.
592
593 // 3. Append (name, value) to headers.
594 headers.push([name, value])
595 }
596 }
597
598 // 4. Return headers.
599 return (this.#headersList[kHeadersSortedMap] = headers)
600 }
601
602 [util.inspect.custom] (depth, options) {
603 options.depth ??= depth
604
605 return `Headers ${util.formatWithOptions(options, this.#headersList.entries)}`
606 }
607
608 static getHeadersGuard (o) {
609 return o.#guard
610 }
611
612 static setHeadersGuard (o, guard) {
613 o.#guard = guard
614 }
615
616 static getHeadersList (o) {
617 return o.#headersList
618 }
619
620 static setHeadersList (o, list) {
621 o.#headersList = list
622 }
623}
624
625const { getHeadersGuard, setHeadersGuard, getHeadersList, setHeadersList } = Headers
626Reflect.deleteProperty(Headers, 'getHeadersGuard')
627Reflect.deleteProperty(Headers, 'setHeadersGuard')
628Reflect.deleteProperty(Headers, 'getHeadersList')
629Reflect.deleteProperty(Headers, 'setHeadersList')
630
631iteratorMixin('Headers', Headers, kHeadersSortedMap, 0, 1)
632
633Object.defineProperties(Headers.prototype, {
634 append: kEnumerableProperty,
635 delete: kEnumerableProperty,
636 get: kEnumerableProperty,
637 has: kEnumerableProperty,
638 set: kEnumerableProperty,
639 getSetCookie: kEnumerableProperty,
640 [Symbol.toStringTag]: {
641 value: 'Headers',
642 configurable: true
643 },
644 [util.inspect.custom]: {
645 enumerable: false
646 }
647})
648
649webidl.converters.HeadersInit = function (V, prefix, argument) {
650 if (webidl.util.Type(V) === 'Object') {
651 const iterator = Reflect.get(V, Symbol.iterator)
652
653 // A work-around to ensure we send the properly-cased Headers when V is a Headers object.
654 // Read https://github.com/nodejs/undici/pull/3159#issuecomment-2075537226 before touching, please.
655 if (!util.types.isProxy(V) && iterator === Headers.prototype.entries) { // Headers object
656 try {
657 return getHeadersList(V).entriesList
658 } catch {
659 // fall-through
660 }
661 }
662
663 if (typeof iterator === 'function') {
664 return webidl.converters['sequence<sequence<ByteString>>'](V, prefix, argument, iterator.bind(V))
665 }
666
667 return webidl.converters['record<ByteString, ByteString>'](V, prefix, argument)
668 }
669
670 throw webidl.errors.conversionFailed({
671 prefix: 'Headers constructor',
672 argument: 'Argument 1',
673 types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
674 })
675}
676
677module.exports = {
678 fill,
679 // for test.
680 compareHeaderName,
681 Headers,
682 HeadersList,
683 getHeadersGuard,
684 setHeadersGuard,
685 setHeadersList,
686 getHeadersList
687}
688 