Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(headers): Improve Headers #2397

Merged
merged 9 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 67 additions & 49 deletions lib/fetch/headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ const assert = require('assert')
const kHeadersMap = Symbol('headers map')
const kHeadersSortedMap = Symbol('headers map sorted')

/**
* @param {number} code
*/
function isHTTPWhiteSpaceCharCode (code) {
return code === 0x00a || code === 0x00d || code === 0x009 || code === 0x020
}

/**
* @see https://fetch.spec.whatwg.org/#concept-header-value-normalize
* @param {string} potentialValue
Expand All @@ -24,12 +31,12 @@ function headerValueNormalize (potentialValue) {
// To normalize a byte sequence potentialValue, remove
// any leading and trailing HTTP whitespace bytes from
// potentialValue.
let i = 0; let j = potentialValue.length

while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(j - 1))) --j
while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i

// 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 ]+/, '')
return i === 0 && j === potentialValue.length ? potentialValue : potentialValue.substring(i, j)
}

function fill (headers, object) {
Expand All @@ -38,7 +45,8 @@ function fill (headers, object) {
// 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) {
for (let i = 0; i < object.length; ++i) {
const header = object[i]
// 1. If header does not contain exactly two items, then throw a TypeError.
if (header.length !== 2) {
throw webidl.errors.exception({
Expand All @@ -48,15 +56,16 @@ function fill (headers, object) {
}

// 2. Append (header’s first item, header’s second item) to headers.
headers.append(header[0], header[1])
appendHeader(headers, 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)
const keys = Object.keys(object)
for (let i = 0; i < keys.length; ++i) {
appendHeader(headers, keys[i], object[keys[i]])
}
} else {
throw webidl.errors.conversionFailed({
Expand All @@ -67,6 +76,50 @@ function fill (headers, object) {
}
}

/**
* @see https://fetch.spec.whatwg.org/#concept-headers-append
*/
function appendHeader (headers, name, 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 (headers[kGuard] === 'immutable') {
throw new TypeError('immutable')
} else if (headers[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.
return headers[kHeadersList].append(name, value)

// 8. If headers’s guard is "request-no-cors", then remove
// privileged no-CORS request headers from headers
}

class HeadersList {
/** @type {[string, string][]|null} */
cookies = null
Expand Down Expand Up @@ -212,43 +265,7 @@ class Headers {
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)
return appendHeader(this, name, value)
}

// https://fetch.spec.whatwg.org/#dom-headers-delete
Expand Down Expand Up @@ -422,16 +439,17 @@ class Headers {
const cookies = this[kHeadersList].cookies

// 3. For each name of names:
for (const [name, value] of names) {
for (let i = 0; i < names.length; ++i) {
const [name, value] = names[i]
// 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])
for (let j = 0; j < cookies.length; ++j) {
headers.push([name, cookies[j]])
}
} else {
// 2. Otherwise:
Expand Down
6 changes: 2 additions & 4 deletions lib/fetch/webidl.js
Original file line number Diff line number Diff line change
Expand Up @@ -427,12 +427,10 @@ webidl.converters.ByteString = function (V) {
// 2. If the value of any element of x is greater than
// 255, then throw a TypeError.
for (let index = 0; index < x.length; index++) {
const charCode = x.charCodeAt(index)

if (charCode > 255) {
if (x.charCodeAt(index) > 255) {
throw new TypeError(
'Cannot convert argument to a ByteString because the character at ' +
`index ${index} has a value of ${charCode} which is greater than 255.`
`index ${index} has a value of ${x.charCodeAt(index)} which is greater than 255.`
)
}
}
Expand Down
Loading