diff --git a/benchmarks/fetch/headers-length32.mjs b/benchmarks/fetch/headers-length32.mjs index 16062022111..af8169bf859 100644 --- a/benchmarks/fetch/headers-length32.mjs +++ b/benchmarks/fetch/headers-length32.mjs @@ -40,12 +40,8 @@ const headers = new Headers( const headersList = getHeadersList(headers) -const kHeadersSortedMap = Reflect.ownKeys(headersList).find( - (c) => String(c) === 'Symbol(headers map sorted)' -) - bench('Headers@@iterator', () => { - headersList[kHeadersSortedMap] = null + headersList.sortedMap = null return [...headers] }) diff --git a/benchmarks/fetch/headers-methods.mjs b/benchmarks/fetch/headers-methods.mjs new file mode 100644 index 00000000000..324b62caa06 --- /dev/null +++ b/benchmarks/fetch/headers-methods.mjs @@ -0,0 +1,70 @@ +import { bench, group, run } from 'mitata' +import { Headers } from '../../lib/web/fetch/headers.js' + +const objectInit = { + Accept: 'application/json', + 'Content-Type': 'text/plain', + 'User-Agent': 'benchmark', + Authorization: 'Bearer token', + Cookie: 'a=1', + 'X-Request-Id': 'abc', + 'Cache-Control': 'no-cache', + Host: 'example.com' +} + +const headers = new Headers(objectInit) +const copySource = new Headers(objectInit) + +group('Headers methods', () => { + bench('construct empty', () => { + return new Headers() + }) + + bench('construct object', () => { + return new Headers(objectInit) + }) + + bench('construct headers', () => { + return new Headers(copySource) + }) + + bench('get custom', () => { + return headers.get('x-request-id') + }) + + bench('get common', () => { + return headers.get('content-type') + }) + + bench('has', () => { + return headers.has('authorization') + }) + + bench('set', () => { + headers.set('x-count', '1') + return headers + }) + + bench('append', () => { + const current = new Headers() + current.append('Accept', 'text/html') + current.append('X-Custom', '1') + return current + }) + + bench('delete', () => { + const current = new Headers(objectInit) + current.delete('content-type') + return current + }) + + bench('iterate', () => { + let result + for (const entry of headers) { + result = entry + } + return result + }) +}) + +await run() diff --git a/benchmarks/fetch/headers.mjs b/benchmarks/fetch/headers.mjs index 7f9047b6e2e..8fcb05e360f 100644 --- a/benchmarks/fetch/headers.mjs +++ b/benchmarks/fetch/headers.mjs @@ -31,20 +31,16 @@ for (const [name, length] of Object.entries(settings)) { const headersListSorted = getHeadersList(headersSorted) - const kHeadersSortedMap = Reflect.ownKeys(headersList).find( - (c) => String(c) === 'Symbol(headers map sorted)' - ) - group(`length ${length} #${name}`, () => { bench('Headers@@iterator', () => { // prevention of memoization of results - headersList[kHeadersSortedMap] = null + headersList.sortedMap = null return [...headers] }) bench('Headers@@iterator (sorted)', () => { // prevention of memoization of results - headersListSorted[kHeadersSortedMap] = null + headersListSorted.sortedMap = null return [...headersSorted] }) }) diff --git a/lib/web/fetch/headers.js b/lib/web/fetch/headers.js index 024d1989588..b4c6130c252 100644 --- a/lib/web/fetch/headers.js +++ b/lib/web/fetch/headers.js @@ -4,15 +4,106 @@ const { kConstruct } = require('../../core/symbols') const { kEnumerableProperty } = require('../../core/util') -const { - iteratorMixin, - isValidHeaderName, - isValidHeaderValue -} = require('./util') +const { iteratorMixin } = require('./util') const { webidl } = require('../webidl') -const assert = require('node:assert') const util = require('node:util') +/** + * HTTP token code points (RFC 7230). + * @see https://tools.ietf.org/html/rfc7230#section-3.2.6 + */ +const TOKEN_CHARS = new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31 + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32-47 (!"#$%&'()*+,-./) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48-63 (0-9:;<=>?) + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64-79 (@A-O) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80-95 (P-Z[\]^_) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96-111 (`a-o) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, // 112-127 (p-z{|}~) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128-143 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 144-159 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 160-175 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 176-191 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 192-207 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 208-223 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 224-239 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 240-255 +]) + +/** + * Pre-validated lowercase names. `Set#has` avoids a token scan + toLowerCase + * on the hottest Headers read path (get/has/delete of common headers). + */ +const COMMON_HEADER_NAMES = new Set([ + 'accept', + 'accept-encoding', + 'accept-language', + 'accept-ranges', + 'access-control-allow-credentials', + 'access-control-allow-headers', + 'access-control-allow-methods', + 'access-control-allow-origin', + 'access-control-expose-headers', + 'access-control-max-age', + 'access-control-request-headers', + 'access-control-request-method', + 'age', + 'authorization', + 'cache-control', + 'connection', + 'content-disposition', + 'content-encoding', + 'content-language', + 'content-length', + 'content-range', + 'content-security-policy', + 'content-type', + 'cookie', + 'date', + 'etag', + 'expect', + 'expires', + 'forwarded', + 'host', + 'if-match', + 'if-modified-since', + 'if-none-match', + 'if-range', + 'if-unmodified-since', + 'keep-alive', + 'last-modified', + 'link', + 'location', + 'origin', + 'pragma', + 'proxy-authorization', + 'range', + 'referer', + 'referrer-policy', + 'sec-fetch-mode', + 'sec-websocket-accept', + 'sec-websocket-extensions', + 'sec-websocket-key', + 'sec-websocket-protocol', + 'sec-websocket-version', + 'server', + 'set-cookie', + 'strict-transport-security', + 'te', + 'trailer', + 'transfer-encoding', + 'upgrade', + 'user-agent', + 'vary', + 'via', + 'www-authenticate', + 'x-forwarded-for', + 'x-forwarded-host', + 'x-forwarded-proto', + 'x-request-id' +]) + /** * @param {number} code * @returns {code is (0x0a | 0x0d | 0x09 | 0x20)} @@ -21,21 +112,166 @@ function isHTTPWhiteSpaceCharCode (code) { return code === 0x0a || code === 0x0d || code === 0x09 || code === 0x20 } +function brandCheckHeaders (V) { + // `instanceof` is cheaper than webidl's @@hasInstance bind and has no + // per-instance WeakSet cost on the constructor path. + if (!(V instanceof Headers)) { + const err = new TypeError('Illegal invocation') + err.code = 'ERR_INVALID_THIS' + throw err + } +} + +function throwArgumentLength (prefix, min, length) { + throw webidl.errors.exception({ + message: `${min} argument${min !== 1 ? 's' : ''} required, ` + + `but${length ? ' only' : ''} ${length} found.`, + header: prefix + }) +} + +function throwByteStringChar (index, code) { + throw new TypeError( + 'Cannot convert argument to a ByteString because the character at ' + + `index ${index} has a value of ${code} which is greater than 255.` + ) +} + +/** + * WebIDL ByteString conversion without iterating the string twice. + * @param {unknown} V + * @param {string} prefix + * @param {string} argument + * @returns {string} + */ +function toByteString (V, prefix, argument) { + if (typeof V === 'symbol') { + throw webidl.errors.exception({ + header: prefix, + message: `${argument} is a symbol, which cannot be converted to a ByteString.` + }) + } + + return typeof V === 'string' ? V : String(V) +} + +/** + * Validate a header name and return it lowercased. + * ByteString (char > 255) is checked before token validity, matching WebIDL. + * @param {string} name + * @param {string} prefix + * @param {boolean} checkHighChars + * @returns {string} + */ +function canonicalizeHeaderName (name, prefix, checkHighChars) { + const len = name.length + let hasUpper = false + let valid = len !== 0 + + for (let i = 0; i < len; ++i) { + const c = name.charCodeAt(i) + if (checkHighChars && c > 255) { + throwByteStringChar(i, c) + } + if (TOKEN_CHARS[c] !== 1) { + valid = false + } else if (c >= 65 && c <= 90) { + hasUpper = true + } + } + + if (!valid) { + throw webidl.errors.invalidArgument({ + prefix, + value: name, + type: 'header name' + }) + } + + return hasUpper ? name.toLowerCase() : name +} + +/** + * @param {unknown} V + * @param {string} prefix + * @param {string} argument + * @returns {string} lowercase header name + */ +function convertHeaderName (V, prefix, argument) { + if (typeof V === 'string' && COMMON_HEADER_NAMES.has(V)) { + return V + } + + return canonicalizeHeaderName(toByteString(V, prefix, argument), prefix, true) +} + /** * @see https://fetch.spec.whatwg.org/#concept-header-value-normalize - * @param {string} potentialValue + * Normalize and validate a header value. After trimming HTTP whitespace, + * only NUL / CR / LF remain as value errors (leading/trailing SP/HTAB are gone). + * @param {string} value + * @param {string} prefix + * @param {boolean} checkHighChars * @returns {string} */ -function headerValueNormalize (potentialValue) { +function canonicalizeHeaderValue (value, prefix, checkHighChars) { // To normalize a byte sequence potentialValue, remove // any leading and trailing HTTP whitespace bytes from // potentialValue. - let i = 0; let j = potentialValue.length + const len = value.length + let start = 0 + let end = len + + if (checkHighChars) { + for (let i = 0; i < len; ++i) { + const c = value.charCodeAt(i) + if (c > 255) { + throwByteStringChar(i, c) + } + } + } + + while (end > start && isHTTPWhiteSpaceCharCode(value.charCodeAt(end - 1))) --end + while (end > start && isHTTPWhiteSpaceCharCode(value.charCodeAt(start))) ++start + + for (let i = start; i < end; ++i) { + const c = value.charCodeAt(i) + if (c === 0x00 || c === 0x0a || c === 0x0d) { + const normalized = start === 0 && end === len ? value : value.substring(start, end) + throw webidl.errors.invalidArgument({ + prefix, + value: normalized, + type: 'header value' + }) + } + } + + return start === 0 && end === len ? value : value.substring(start, end) +} - while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(j - 1))) --j - while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i +/** + * @param {unknown} V + * @param {string} prefix + * @param {string} argument + * @returns {string} + */ +function convertHeaderValue (V, prefix, argument) { + return canonicalizeHeaderValue(toByteString(V, prefix, argument), prefix, true) +} - return i === 0 && j === potentialValue.length ? potentialValue : potentialValue.substring(i, j) +/** + * @param {boolean} isLowerCase + * @param {string} name + * @returns {string} + */ +function lowercaseHeaderName (name, isLowerCase) { + if (isLowerCase === true) { + return name + } + if (typeof isLowerCase === 'string') { + return isLowerCase + } + return name.toLowerCase() } /** @@ -44,6 +280,8 @@ function headerValueNormalize (potentialValue) { */ function fill (headers, object) { // To fill a Headers object headers with a given object object, run these steps: + const list = getHeadersList(headers) + const immutable = getHeadersGuard(headers) === 'immutable' // 1. If object is a sequence, then for each header in object: // Note: webidl conversion to array has already been done. @@ -59,7 +297,7 @@ function fill (headers, object) { } // 2. Append (header’s first item, header’s second item) to headers. - appendHeader(headers, header[0], header[1]) + appendHeaderToList(list, header[0], header[1], immutable) } } else if (typeof object === 'object' && object !== null) { // Note: null should throw @@ -68,7 +306,7 @@ function fill (headers, object) { // append (key, value) to headers const keys = Object.keys(object) for (let i = 0; i < keys.length; ++i) { - appendHeader(headers, keys[i], object[keys[i]]) + appendHeaderToList(list, keys[i], object[keys[i]], immutable) } } else { throw webidl.errors.conversionFailed({ @@ -82,28 +320,17 @@ function fill (headers, object) { /** * @see https://fetch.spec.whatwg.org/#concept-headers-append * @param {Headers} headers - * @param {string} name - * @param {string} value + * @param {string} name already a ByteString + * @param {string} value already a ByteString */ -function appendHeader (headers, name, value) { +function appendHeaderToList (list, name, value, immutable) { // 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' - }) - } + const lower = typeof name === 'string' && COMMON_HEADER_NAMES.has(name) + ? name + : canonicalizeHeaderName(name, 'Headers.append', false) + const normalized = canonicalizeHeaderValue(value, 'Headers.append', false) // 3. If headers’s guard is "immutable", then throw a TypeError. // 4. Otherwise, if headers’s guard is "request" and name is a @@ -111,7 +338,7 @@ function appendHeader (headers, name, value) { // 5. Otherwise, if headers’s guard is "request-no-cors": // TODO // Note: undici does not implement forbidden header names - if (getHeadersGuard(headers) === 'immutable') { + if (immutable) { throw new TypeError('immutable') } @@ -119,7 +346,7 @@ function appendHeader (headers, name, value) { // forbidden response-header name, return. // 7. Append (name, value) to headers’s header list. - return getHeadersList(headers).append(name, value, false) + list.append(name, normalized, lower) // 8. If headers’s guard is "request-no-cors", then remove // privileged no-CORS request headers from headers @@ -191,17 +418,24 @@ function compareHeaderName (a, b) { } class HeadersList { - /** @type {[string, string][]|null} */ + /** @type {string[]|null} */ cookies = null + /** @type {[string, string][]|null} */ sortedMap + + /** @type {Map} lowercase name → combined value */ headersMap + /** @type {Map|null} lowercase name → first-seen original name */ + originalNames = null + constructor (init) { if (init instanceof HeadersList) { this.headersMap = new Map(init.headersMap) this.sortedMap = init.sortedMap this.cookies = init.cookies === null ? null : [...init.cookies] + this.originalNames = init.originalNames === null ? null : new Map(init.originalNames) } else { this.headersMap = new Map(init) this.sortedMap = null @@ -218,13 +452,14 @@ class HeadersList { // contains a header whose name is a byte-case-insensitive // match for name. - return this.headersMap.has(isLowerCase ? name : name.toLowerCase()) + return this.headersMap.has(lowercaseHeaderName(name, isLowerCase)) } clear () { this.headersMap.clear() this.sortedMap = null this.cookies = null + this.originalNames = null } /** @@ -234,22 +469,26 @@ class HeadersList { * @param {boolean} isLowerCase */ append (name, value, isLowerCase) { - this.sortedMap = null + if (this.sortedMap !== null) { + this.sortedMap = null + } // 1. If list contains name, then set name to the first such // header’s name. - const lowercaseName = isLowerCase ? name : name.toLowerCase() + const lowercaseName = lowercaseHeaderName(name, isLowerCase) const exists = this.headersMap.get(lowercaseName) // 2. Append (name, value) to list. - if (exists) { - const delimiter = lowercaseName === 'cookie' ? '; ' : ', ' - this.headersMap.set(lowercaseName, { - name: exists.name, - value: `${exists.value}${delimiter}${value}` - }) + if (exists !== undefined) { + this.headersMap.set( + lowercaseName, + lowercaseName === 'cookie' ? exists + '; ' + value : exists + ', ' + value + ) } else { - this.headersMap.set(lowercaseName, { name, value }) + this.headersMap.set(lowercaseName, value) + if (name !== lowercaseName) { + (this.originalNames ??= new Map()).set(lowercaseName, name) + } } if (lowercaseName === 'set-cookie') { @@ -264,8 +503,11 @@ class HeadersList { * @param {boolean} isLowerCase */ set (name, value, isLowerCase) { - this.sortedMap = null - const lowercaseName = isLowerCase ? name : name.toLowerCase() + if (this.sortedMap !== null) { + this.sortedMap = null + } + + const lowercaseName = lowercaseHeaderName(name, isLowerCase) if (lowercaseName === 'set-cookie') { this.cookies = [value] @@ -275,7 +517,13 @@ class HeadersList { // the first such header to value and remove the // others. // 2. Otherwise, append header (name, value) to list. - this.headersMap.set(lowercaseName, { name, value }) + this.headersMap.set(lowercaseName, value) + + if (name !== lowercaseName) { + (this.originalNames ??= new Map()).set(lowercaseName, name) + } else if (this.originalNames !== null) { + this.originalNames.delete(lowercaseName) + } } /** @@ -284,14 +532,18 @@ class HeadersList { * @param {boolean} isLowerCase */ delete (name, isLowerCase) { - this.sortedMap = null - if (!isLowerCase) name = name.toLowerCase() + if (this.sortedMap !== null) { + this.sortedMap = null + } - if (name === 'set-cookie') { + const lowercaseName = lowercaseHeaderName(name, isLowerCase) + + if (lowercaseName === 'set-cookie') { this.cookies = null } - this.headersMap.delete(name) + this.headersMap.delete(lowercaseName) + this.originalNames?.delete(lowercaseName) } /** @@ -305,44 +557,50 @@ class HeadersList { // 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.headersMap.get(isLowerCase ? name : name.toLowerCase())?.value ?? null + const value = this.headersMap.get(lowercaseHeaderName(name, isLowerCase)) + return value !== undefined ? value : null } - * [Symbol.iterator] () { + [Symbol.iterator] () { // use the lowercased name - for (const { 0: name, 1: { value } } of this.headersMap) { - yield [name, value] - } + return this.headersMap[Symbol.iterator]() } get entries () { const headers = {} + const originalNames = this.originalNames - if (this.headersMap.size !== 0) { - for (const { name, value } of this.headersMap.values()) { - headers[name] = value - } + for (const { 0: lower, 1: value } of this.headersMap) { + headers[originalNames !== null ? (originalNames.get(lower) ?? lower) : lower] = value } return headers } - rawValues () { - return this.headersMap.values() + * rawValues () { + const originalNames = this.originalNames + + for (const { 0: lower, 1: value } of this.headersMap) { + yield { + name: originalNames !== null ? (originalNames.get(lower) ?? lower) : lower, + value + } + } } get entriesList () { const headers = [] - - if (this.headersMap.size !== 0) { - for (const { 0: lowerName, 1: { name, value } } of this.headersMap) { - if (lowerName === 'set-cookie') { - for (const cookie of this.cookies) { - headers.push([name, cookie]) - } - } else { - headers.push([name, value]) + const originalNames = this.originalNames + const cookies = this.cookies + + for (const { 0: lowerName, 1: value } of this.headersMap) { + const name = originalNames !== null ? (originalNames.get(lowerName) ?? lowerName) : lowerName + if (lowerName === 'set-cookie') { + for (let i = 0; i < cookies.length; ++i) { + headers.push([name, cookies[i]]) } + } else { + headers.push([name, value]) } } @@ -365,10 +623,9 @@ class HeadersList { const iterator = this.headersMap[Symbol.iterator]() const firstValue = iterator.next().value // set [name, value] to first index. - array[0] = [firstValue[0], firstValue[1].value] + array[0] = [firstValue[0], firstValue[1]] // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine // 3.2.2. Assert: value is non-null. - assert(firstValue[1].value !== null) for ( let i = 1, j = 0, right = 0, left = 0, pivot = 0, x, value; i < size; @@ -377,10 +634,9 @@ class HeadersList { // get next value value = iterator.next().value // set [name, value] to current index. - x = array[i] = [value[0], value[1].value] + x = array[i] = [value[0], value[1]] // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine // 3.2.2. Assert: value is non-null. - assert(x[1] !== null) left = 0 right = i // binary search @@ -412,11 +668,10 @@ class HeadersList { // This case would be a rare occurrence. // slow-path: fallback let i = 0 - for (const { 0: name, 1: { value } } of this.headersMap) { + for (const { 0: name, 1: value } of this.headersMap) { array[i++] = [name, value] // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine // 3.2.2. Assert: value is non-null. - assert(value !== null) } return array.sort(compareHeaderName) } @@ -451,41 +706,74 @@ class Headers { // 2. If init is given, then fill this with init. if (init !== undefined) { - init = webidl.converters.HeadersInit(init, 'Headers constructor', 'init') - fill(this, init) + this.#initialize(init) } } + #initialize (init) { + // Fast-path: copy an existing Headers list without re-validating. + // Keep the iterator check so subclasses that override entries() still + // go through the HeadersInit conversion path. + if ( + init !== null && + typeof init === 'object' && + !util.types.isProxy(init) && + init instanceof Headers && + Reflect.get(init, Symbol.iterator) === Headers.prototype.entries + ) { + this.#headersList = new HeadersList(getHeadersList(init)) + return + } + + fill(this, webidl.converters.HeadersInit(init, 'Headers constructor', 'init')) + } + // https://fetch.spec.whatwg.org/#dom-headers-append append (name, value) { - webidl.brandCheck(this, Headers) + brandCheckHeaders(this) + + if (arguments.length < 2) { + throwArgumentLength('Headers.append', 2, arguments.length) + } - webidl.argumentLengthCheck(arguments, 2, 'Headers.append') + // 1. Normalize value. + // 2. If name is not a header name or value is not a + // header value, then throw a TypeError. + const rawName = toByteString(name, 'Headers.append', 'name') + const lower = typeof rawName === 'string' && COMMON_HEADER_NAMES.has(rawName) + ? rawName + : canonicalizeHeaderName(rawName, 'Headers.append', true) + const normalized = convertHeaderValue(value, 'Headers.append', '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. + // 5. Otherwise, if headers’s guard is "request-no-cors": + // TODO + // Note: undici does not implement forbidden header names + if (this.#guard === 'immutable') { + throw new TypeError('immutable') + } - const prefix = 'Headers.append' - name = webidl.converters.ByteString(name, prefix, 'name') - value = webidl.converters.ByteString(value, prefix, 'value') + // 6. Otherwise, if headers’s guard is "response" and name is a + // forbidden response-header name, return. - return appendHeader(this, name, value) + // 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.#headersList.append(rawName, normalized, lower) } // https://fetch.spec.whatwg.org/#dom-headers-delete delete (name) { - webidl.brandCheck(this, Headers) + brandCheckHeaders(this) - webidl.argumentLengthCheck(arguments, 1, 'Headers.delete') - - const prefix = 'Headers.delete' - name = webidl.converters.ByteString(name, prefix, 'name') + if (arguments.length < 1) { + throwArgumentLength('Headers.delete', 1, arguments.length) + } // 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' - }) - } + const lower = convertHeaderName(name, 'Headers.delete', 'name') // 2. If this’s guard is "immutable", then throw a TypeError. // 3. Otherwise, if this’s guard is "request" and name is a @@ -503,90 +791,60 @@ class Headers { // 6. If this’s header list does not contain name, then // return. - if (!this.#headersList.contains(name, false)) { + if (!this.#headersList.contains(lower, true)) { 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. - this.#headersList.delete(name, false) + this.#headersList.delete(lower, true) } // https://fetch.spec.whatwg.org/#dom-headers-get get (name) { - webidl.brandCheck(this, Headers) - - webidl.argumentLengthCheck(arguments, 1, 'Headers.get') - - const prefix = 'Headers.get' - name = webidl.converters.ByteString(name, prefix, 'name') + brandCheckHeaders(this) - // 1. If name is not a header name, then throw a TypeError. - if (!isValidHeaderName(name)) { - throw webidl.errors.invalidArgument({ - prefix, - value: name, - type: 'header name' - }) + if (arguments.length < 1) { + throwArgumentLength('Headers.get', 1, arguments.length) } + // 1. If name is not a header name, then throw a TypeError. // 2. Return the result of getting name from this’s header // list. - return this.#headersList.get(name, false) + return this.#headersList.get(convertHeaderName(name, 'Headers.get', 'name'), true) } // https://fetch.spec.whatwg.org/#dom-headers-has has (name) { - webidl.brandCheck(this, Headers) - - webidl.argumentLengthCheck(arguments, 1, 'Headers.has') - - const prefix = 'Headers.has' - name = webidl.converters.ByteString(name, prefix, 'name') + brandCheckHeaders(this) - // 1. If name is not a header name, then throw a TypeError. - if (!isValidHeaderName(name)) { - throw webidl.errors.invalidArgument({ - prefix, - value: name, - type: 'header name' - }) + if (arguments.length < 1) { + throwArgumentLength('Headers.has', 1, arguments.length) } + // 1. If name is not a header name, then throw a TypeError. // 2. Return true if this’s header list contains name; // otherwise false. - return this.#headersList.contains(name, false) + return this.#headersList.contains(convertHeaderName(name, 'Headers.has', 'name'), true) } // https://fetch.spec.whatwg.org/#dom-headers-set set (name, value) { - webidl.brandCheck(this, Headers) + brandCheckHeaders(this) - webidl.argumentLengthCheck(arguments, 2, 'Headers.set') - - const prefix = 'Headers.set' - name = webidl.converters.ByteString(name, prefix, 'name') - value = webidl.converters.ByteString(value, prefix, 'value') + if (arguments.length < 2) { + throwArgumentLength('Headers.set', 2, arguments.length) + } // 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, - value: name, - type: 'header name' - }) - } else if (!isValidHeaderValue(value)) { - throw webidl.errors.invalidArgument({ - prefix, - value, - type: 'header value' - }) - } + const rawName = toByteString(name, 'Headers.set', 'name') + const lower = typeof rawName === 'string' && COMMON_HEADER_NAMES.has(rawName) + ? rawName + : canonicalizeHeaderName(rawName, 'Headers.set', true) + const normalized = convertHeaderValue(value, 'Headers.set', 'value') // 3. If this’s guard is "immutable", then throw a TypeError. // 4. Otherwise, if this’s guard is "request" and name is a @@ -604,12 +862,12 @@ class Headers { // 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 - this.#headersList.set(name, value, false) + this.#headersList.set(rawName, normalized, lower) } // https://fetch.spec.whatwg.org/#dom-headers-getsetcookie getSetCookie () { - webidl.brandCheck(this, Headers) + brandCheckHeaders(this) // 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