Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
headers.js688 linesDownload Raw Back to fetch
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 
codekingpro/portable-devtools · Team Ai