• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}
590