Spaces:
Runtime error
Runtime error
// https://github.com/Ethan-Arrowood/undici-fetch | |
const { kHeadersList } = require('../core/symbols') | |
const { kGuard } = require('./symbols') | |
const { kEnumerableProperty } = require('../core/util') | |
const { | |
makeIterator, | |
isValidHeaderName, | |
isValidHeaderValue | |
} = require('./util') | |
const { webidl } = require('./webidl') | |
const assert = require('assert') | |
const kHeadersMap = Symbol('headers map') | |
const kHeadersSortedMap = Symbol('headers map sorted') | |
/** | |
* @see https://fetch.spec.whatwg.org/#concept-header-value-normalize | |
* @param {string} potentialValue | |
*/ | |
function headerValueNormalize (potentialValue) { | |
// To normalize a byte sequence potentialValue, remove | |
// any leading and trailing HTTP whitespace bytes from | |
// potentialValue. | |
// Trimming the end with `.replace()` and a RegExp is typically subject to | |
// ReDoS. This is safer and faster. | |
let i = potentialValue.length | |
while (/[\r\n\t ]/.test(potentialValue.charAt(--i))); | |
return potentialValue.slice(0, i + 1).replace(/^[\r\n\t ]+/, '') | |
} | |
function fill (headers, object) { | |
// To fill a Headers object headers with a given object object, run these steps: | |
// 1. If object is a sequence, then for each header in object: | |
// Note: webidl conversion to array has already been done. | |
if (Array.isArray(object)) { | |
for (const header of object) { | |
// 1. If header does not contain exactly two items, then throw a TypeError. | |
if (header.length !== 2) { | |
throw webidl.errors.exception({ | |
header: 'Headers constructor', | |
message: `expected name/value pair to be length 2, found ${header.length}.` | |
}) | |
} | |
// 2. Append (header’s first item, header’s second item) to headers. | |
headers.append(header[0], header[1]) | |
} | |
} else if (typeof object === 'object' && object !== null) { | |
// Note: null should throw | |
// 2. Otherwise, object is a record, then for each key → value in object, | |
// append (key, value) to headers | |
for (const [key, value] of Object.entries(object)) { | |
headers.append(key, value) | |
} | |
} else { | |
throw webidl.errors.conversionFailed({ | |
prefix: 'Headers constructor', | |
argument: 'Argument 1', | |
types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>'] | |
}) | |
} | |
} | |
class HeadersList { | |
/** @type {[string, string][]|null} */ | |
cookies = null | |
constructor (init) { | |
if (init instanceof HeadersList) { | |
this[kHeadersMap] = new Map(init[kHeadersMap]) | |
this[kHeadersSortedMap] = init[kHeadersSortedMap] | |
this.cookies = init.cookies | |
} else { | |
this[kHeadersMap] = new Map(init) | |
this[kHeadersSortedMap] = null | |
} | |
} | |
// https://fetch.spec.whatwg.org/#header-list-contains | |
contains (name) { | |
// A header list list contains a header name name if list | |
// contains a header whose name is a byte-case-insensitive | |
// match for name. | |
name = name.toLowerCase() | |
return this[kHeadersMap].has(name) | |
} | |
clear () { | |
this[kHeadersMap].clear() | |
this[kHeadersSortedMap] = null | |
this.cookies = null | |
} | |
// https://fetch.spec.whatwg.org/#concept-header-list-append | |
append (name, value) { | |
this[kHeadersSortedMap] = null | |
// 1. If list contains name, then set name to the first such | |
// header’s name. | |
const lowercaseName = name.toLowerCase() | |
const exists = this[kHeadersMap].get(lowercaseName) | |
// 2. Append (name, value) to list. | |
if (exists) { | |
const delimiter = lowercaseName === 'cookie' ? '; ' : ', ' | |
this[kHeadersMap].set(lowercaseName, { | |
name: exists.name, | |
value: `${exists.value}${delimiter}${value}` | |
}) | |
} else { | |
this[kHeadersMap].set(lowercaseName, { name, value }) | |
} | |
if (lowercaseName === 'set-cookie') { | |
this.cookies ??= [] | |
this.cookies.push(value) | |
} | |
} | |
// https://fetch.spec.whatwg.org/#concept-header-list-set | |
set (name, value) { | |
this[kHeadersSortedMap] = null | |
const lowercaseName = name.toLowerCase() | |
if (lowercaseName === 'set-cookie') { | |
this.cookies = [value] | |
} | |
// 1. If list contains name, then set the value of | |
// the first such header to value and remove the | |
// others. | |
// 2. Otherwise, append header (name, value) to list. | |
return this[kHeadersMap].set(lowercaseName, { name, value }) | |
} | |
// https://fetch.spec.whatwg.org/#concept-header-list-delete | |
delete (name) { | |
this[kHeadersSortedMap] = null | |
name = name.toLowerCase() | |
if (name === 'set-cookie') { | |
this.cookies = null | |
} | |
return this[kHeadersMap].delete(name) | |
} | |
// https://fetch.spec.whatwg.org/#concept-header-list-get | |
get (name) { | |
// 1. If list does not contain name, then return null. | |
if (!this.contains(name)) { | |
return null | |
} | |
// 2. Return the values of all headers in list whose name | |
// is a byte-case-insensitive match for name, | |
// separated from each other by 0x2C 0x20, in order. | |
return this[kHeadersMap].get(name.toLowerCase())?.value ?? null | |
} | |
* [Symbol.iterator] () { | |
// use the lowercased name | |
for (const [name, { value }] of this[kHeadersMap]) { | |
yield [name, value] | |
} | |
} | |
get entries () { | |
const headers = {} | |
if (this[kHeadersMap].size) { | |
for (const { name, value } of this[kHeadersMap].values()) { | |
headers[name] = value | |
} | |
} | |
return headers | |
} | |
} | |
// https://fetch.spec.whatwg.org/#headers-class | |
class Headers { | |
constructor (init = undefined) { | |
this[kHeadersList] = new HeadersList() | |
// The new Headers(init) constructor steps are: | |
// 1. Set this’s guard to "none". | |
this[kGuard] = 'none' | |
// 2. If init is given, then fill this with init. | |
if (init !== undefined) { | |
init = webidl.converters.HeadersInit(init) | |
fill(this, init) | |
} | |
} | |
// https://fetch.spec.whatwg.org/#dom-headers-append | |
append (name, value) { | |
webidl.brandCheck(this, Headers) | |
webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.append' }) | |
name = webidl.converters.ByteString(name) | |
value = webidl.converters.ByteString(value) | |
// 1. Normalize value. | |
value = headerValueNormalize(value) | |
// 2. If name is not a header name or value is not a | |
// header value, then throw a TypeError. | |
if (!isValidHeaderName(name)) { | |
throw webidl.errors.invalidArgument({ | |
prefix: 'Headers.append', | |
value: name, | |
type: 'header name' | |
}) | |
} else if (!isValidHeaderValue(value)) { | |
throw webidl.errors.invalidArgument({ | |
prefix: 'Headers.append', | |
value, | |
type: 'header value' | |
}) | |
} | |
// 3. If headers’s guard is "immutable", then throw a TypeError. | |
// 4. Otherwise, if headers’s guard is "request" and name is a | |
// forbidden header name, return. | |
// Note: undici does not implement forbidden header names | |
if (this[kGuard] === 'immutable') { | |
throw new TypeError('immutable') | |
} else if (this[kGuard] === 'request-no-cors') { | |
// 5. Otherwise, if headers’s guard is "request-no-cors": | |
// TODO | |
} | |
// 6. Otherwise, if headers’s guard is "response" and name is a | |
// forbidden response-header name, return. | |
// 7. Append (name, value) to headers’s header list. | |
// 8. If headers’s guard is "request-no-cors", then remove | |
// privileged no-CORS request headers from headers | |
return this[kHeadersList].append(name, value) | |
} | |
// https://fetch.spec.whatwg.org/#dom-headers-delete | |
delete (name) { | |
webidl.brandCheck(this, Headers) | |
webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.delete' }) | |
name = webidl.converters.ByteString(name) | |
// 1. If name is not a header name, then throw a TypeError. | |
if (!isValidHeaderName(name)) { | |
throw webidl.errors.invalidArgument({ | |
prefix: 'Headers.delete', | |
value: name, | |
type: 'header name' | |
}) | |
} | |
// 2. If this’s guard is "immutable", then throw a TypeError. | |
// 3. Otherwise, if this’s guard is "request" and name is a | |
// forbidden header name, return. | |
// 4. Otherwise, if this’s guard is "request-no-cors", name | |
// is not a no-CORS-safelisted request-header name, and | |
// name is not a privileged no-CORS request-header name, | |
// return. | |
// 5. Otherwise, if this’s guard is "response" and name is | |
// a forbidden response-header name, return. | |
// Note: undici does not implement forbidden header names | |
if (this[kGuard] === 'immutable') { | |
throw new TypeError('immutable') | |
} else if (this[kGuard] === 'request-no-cors') { | |
// TODO | |
} | |
// 6. If this’s header list does not contain name, then | |
// return. | |
if (!this[kHeadersList].contains(name)) { | |
return | |
} | |
// 7. Delete name from this’s header list. | |
// 8. If this’s guard is "request-no-cors", then remove | |
// privileged no-CORS request headers from this. | |
return this[kHeadersList].delete(name) | |
} | |
// https://fetch.spec.whatwg.org/#dom-headers-get | |
get (name) { | |
webidl.brandCheck(this, Headers) | |
webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.get' }) | |
name = webidl.converters.ByteString(name) | |
// 1. If name is not a header name, then throw a TypeError. | |
if (!isValidHeaderName(name)) { | |
throw webidl.errors.invalidArgument({ | |
prefix: 'Headers.get', | |
value: name, | |
type: 'header name' | |
}) | |
} | |
// 2. Return the result of getting name from this’s header | |
// list. | |
return this[kHeadersList].get(name) | |
} | |
// https://fetch.spec.whatwg.org/#dom-headers-has | |
has (name) { | |
webidl.brandCheck(this, Headers) | |
webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.has' }) | |
name = webidl.converters.ByteString(name) | |
// 1. If name is not a header name, then throw a TypeError. | |
if (!isValidHeaderName(name)) { | |
throw webidl.errors.invalidArgument({ | |
prefix: 'Headers.has', | |
value: name, | |
type: 'header name' | |
}) | |
} | |
// 2. Return true if this’s header list contains name; | |
// otherwise false. | |
return this[kHeadersList].contains(name) | |
} | |
// https://fetch.spec.whatwg.org/#dom-headers-set | |
set (name, value) { | |
webidl.brandCheck(this, Headers) | |
webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.set' }) | |
name = webidl.converters.ByteString(name) | |
value = webidl.converters.ByteString(value) | |
// 1. Normalize value. | |
value = headerValueNormalize(value) | |
// 2. If name is not a header name or value is not a | |
// header value, then throw a TypeError. | |
if (!isValidHeaderName(name)) { | |
throw webidl.errors.invalidArgument({ | |
prefix: 'Headers.set', | |
value: name, | |
type: 'header name' | |
}) | |
} else if (!isValidHeaderValue(value)) { | |
throw webidl.errors.invalidArgument({ | |
prefix: 'Headers.set', | |
value, | |
type: 'header value' | |
}) | |
} | |
// 3. If this’s guard is "immutable", then throw a TypeError. | |
// 4. Otherwise, if this’s guard is "request" and name is a | |
// forbidden header name, return. | |
// 5. Otherwise, if this’s guard is "request-no-cors" and | |
// name/value is not a no-CORS-safelisted request-header, | |
// return. | |
// 6. Otherwise, if this’s guard is "response" and name is a | |
// forbidden response-header name, return. | |
// Note: undici does not implement forbidden header names | |
if (this[kGuard] === 'immutable') { | |
throw new TypeError('immutable') | |
} else if (this[kGuard] === 'request-no-cors') { | |
// TODO | |
} | |
// 7. Set (name, value) in this’s header list. | |
// 8. If this’s guard is "request-no-cors", then remove | |
// privileged no-CORS request headers from this | |
return this[kHeadersList].set(name, value) | |
} | |
// https://fetch.spec.whatwg.org/#dom-headers-getsetcookie | |
getSetCookie () { | |
webidl.brandCheck(this, Headers) | |
// 1. If this’s header list does not contain `Set-Cookie`, then return « ». | |
// 2. Return the values of all headers in this’s header list whose name is | |
// a byte-case-insensitive match for `Set-Cookie`, in order. | |
const list = this[kHeadersList].cookies | |
if (list) { | |
return [...list] | |
} | |
return [] | |
} | |
// https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine | |
get [kHeadersSortedMap] () { | |
if (this[kHeadersList][kHeadersSortedMap]) { | |
return this[kHeadersList][kHeadersSortedMap] | |
} | |
// 1. Let headers be an empty list of headers with the key being the name | |
// and value the value. | |
const headers = [] | |
// 2. Let names be the result of convert header names to a sorted-lowercase | |
// set with all the names of the headers in list. | |
const names = [...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1) | |
const cookies = this[kHeadersList].cookies | |
// 3. For each name of names: | |
for (const [name, value] of names) { | |
// 1. If name is `set-cookie`, then: | |
if (name === 'set-cookie') { | |
// 1. Let values be a list of all values of headers in list whose name | |
// is a byte-case-insensitive match for name, in order. | |
// 2. For each value of values: | |
// 1. Append (name, value) to headers. | |
for (const value of cookies) { | |
headers.push([name, value]) | |
} | |
} else { | |
// 2. Otherwise: | |
// 1. Let value be the result of getting name from list. | |
// 2. Assert: value is non-null. | |
assert(value !== null) | |
// 3. Append (name, value) to headers. | |
headers.push([name, value]) | |
} | |
} | |
this[kHeadersList][kHeadersSortedMap] = headers | |
// 4. Return headers. | |
return headers | |
} | |
keys () { | |
webidl.brandCheck(this, Headers) | |
return makeIterator( | |
() => [...this[kHeadersSortedMap].values()], | |
'Headers', | |
'key' | |
) | |
} | |
values () { | |
webidl.brandCheck(this, Headers) | |
return makeIterator( | |
() => [...this[kHeadersSortedMap].values()], | |
'Headers', | |
'value' | |
) | |
} | |
entries () { | |
webidl.brandCheck(this, Headers) | |
return makeIterator( | |
() => [...this[kHeadersSortedMap].values()], | |
'Headers', | |
'key+value' | |
) | |
} | |
/** | |
* @param {(value: string, key: string, self: Headers) => void} callbackFn | |
* @param {unknown} thisArg | |
*/ | |
forEach (callbackFn, thisArg = globalThis) { | |
webidl.brandCheck(this, Headers) | |
webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.forEach' }) | |
if (typeof callbackFn !== 'function') { | |
throw new TypeError( | |
"Failed to execute 'forEach' on 'Headers': parameter 1 is not of type 'Function'." | |
) | |
} | |
for (const [key, value] of this) { | |
callbackFn.apply(thisArg, [value, key, this]) | |
} | |
} | |
[Symbol.for('nodejs.util.inspect.custom')] () { | |
webidl.brandCheck(this, Headers) | |
return this[kHeadersList] | |
} | |
} | |
Headers.prototype[Symbol.iterator] = Headers.prototype.entries | |
Object.defineProperties(Headers.prototype, { | |
append: kEnumerableProperty, | |
delete: kEnumerableProperty, | |
get: kEnumerableProperty, | |
has: kEnumerableProperty, | |
set: kEnumerableProperty, | |
getSetCookie: kEnumerableProperty, | |
keys: kEnumerableProperty, | |
values: kEnumerableProperty, | |
entries: kEnumerableProperty, | |
forEach: kEnumerableProperty, | |
[Symbol.iterator]: { enumerable: false }, | |
[Symbol.toStringTag]: { | |
value: 'Headers', | |
configurable: true | |
} | |
}) | |
webidl.converters.HeadersInit = function (V) { | |
if (webidl.util.Type(V) === 'Object') { | |
if (V[Symbol.iterator]) { | |
return webidl.converters['sequence<sequence<ByteString>>'](V) | |
} | |
return webidl.converters['record<ByteString, ByteString>'](V) | |
} | |
throw webidl.errors.conversionFailed({ | |
prefix: 'Headers constructor', | |
argument: 'Argument 1', | |
types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>'] | |
}) | |
} | |
module.exports = { | |
fill, | |
Headers, | |
HeadersList | |
} | |