Skip to content

Commit

Permalink
perf(headers): Improve Headers (#2397)
Browse files Browse the repository at this point in the history
* perf(headers): Improve Headers

* perf: reduce variables in the for loop

* perf: use `===`

* perf: use `===`

* feat: Revert `avoid re-stringify`

* perf: use `substring`

* fix: fixing trimming issues

* perf: avoid two duplicate calls of `ByteString`.

* fix: comment position
  • Loading branch information
tsctx committed Nov 7, 2023
1 parent 7b5c851 commit 0f319a0
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 53 deletions.
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

0 comments on commit 0f319a0

Please sign in to comment.