From 5e99a4f77d6adb745644de229f609ff4c4363a7d Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Tue, 27 Oct 2026 20:37:40 +0100 Subject: [PATCH 1/2] perf(headers): precompute per-browser header order sets sortHeaders built `new Set(order)` from the browser's header order on every call, and getHeader allocated an `[key, value]` pair per header through Object.entries. The order Sets now get built once when the module loads, and getHeader walks Object.keys and returns on the first match. Output is unchanged: same key order and values as master on 80,024 checks (bench fixtures, 20k random header sets with mixed-case and numeric keys, missing or empty user agents, null-prototype and inherited-property objects, null and undefined input). Local A/B, node v24.16.0 arm64, 6 alternating runs per side, median of medians: | case | base | change | delta | | ----------------------- | ------ | ------ | ------ | | sortHeaders chrome | 90.3ms | 50.4ms | -44.2% | | sortHeaders firefox | 79.9ms | 42.9ms | -46.3% | | sortHeaders safari | 66.1ms | 40.6ms | -38.6% | | getBrowser (unchanged) | 155.5ms | 156.4ms | +0.6% | | tls options (unchanged) | 103.9ms | 104.8ms | +0.9% | Co-Authored-By: Claude Opus 5.5 --- src/headers.js | 42 ++++++++++++++++-------------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/src/headers.js b/src/headers.js index 3a701a7..ab5c37d 100644 --- a/src/headers.js +++ b/src/headers.js @@ -4,43 +4,33 @@ const getBrowser = require('./browser') const HEADERS_ORDER = require('./headers-order.json') -const getHeader = (key, headers) => { - let result +const NO_ORDER = { keys: [], set: new Set() } - for (const [header, value] of Object.entries(headers)) { - if (header.toLowerCase() === key) { - result = value - break - } - } +const ORDERS = Object.fromEntries( + Object.entries(HEADERS_ORDER).map(([browser, keys]) => [browser, { keys, set: new Set(keys) }]) +) - return result +const getHeader = (key, headers) => { + const headerKeys = Object.keys(headers) + for (let i = 0; i < headerKeys.length; i++) { + if (headerKeys[i].toLowerCase() === key) return headers[headerKeys[i]] + } } const sortHeaders = headers => { const userAgent = getHeader('user-agent', headers) - const browser = getBrowser(userAgent) - const order = HEADERS_ORDER[browser] || [] + const { keys, set } = ORDERS[getBrowser(userAgent)] || NO_ORDER const orderedHeaders = {} - // Pre-calculate order as a Set for O(1) lookup - const orderSet = new Set(order) - const headerKeys = Object.keys(headers) - const headersLength = headerKeys.length - const orderLength = order.length - let i = 0 - let key - - // Add ordered headers - for (; i < orderLength; i++) { - key = order[i] + for (let i = 0; i < keys.length; i++) { + const key = keys[i] if (key in headers) orderedHeaders[key] = headers[key] } - // Add remaining headers - for (i = 0; i < headersLength; i++) { - key = headerKeys[i] - if (!orderSet.has(key)) orderedHeaders[key] = headers[key] + const headerKeys = Object.keys(headers) + for (let i = 0; i < headerKeys.length; i++) { + const key = headerKeys[i] + if (!set.has(key)) orderedHeaders[key] = headers[key] } return orderedHeaders From c98ee70dba31e02b874ecab33f93a543f4ccc17a Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Tue, 27 Oct 2026 21:49:16 +0000 Subject: [PATCH 2/2] test(headers): lock order and the lookup branches ava's deepEqual ignores key order, so the sort tests stayed green when sorting was replaced with a copy. Assert key order, and cover the no-browser fallback, inherited keys, and the hook's header lookup. Co-authored-by: Cursor --- test/headers.js | 135 ++++++++++++++++++++++++++++++++++++++++++++++-- test/hook.js | 12 +++++ 2 files changed, 143 insertions(+), 4 deletions(-) diff --git a/test/headers.js b/test/headers.js index 33fe34c..65a2cfa 100644 --- a/test/headers.js +++ b/test/headers.js @@ -6,13 +6,32 @@ const { sortHeaders, getHeader } = require('..') const { UA } = require('./helpers') +const assertHeaders = (t, actual, expected) => { + t.deepEqual(Object.keys(actual), Object.keys(expected)) + t.deepEqual(actual, expected) +} + test('.getHeader', t => { t.is(getHeader('user-agent', { 'user-agent': 'googlebot' }), 'googlebot') t.is(getHeader('user-agent', { 'User-Agent': 'googlebot' }), 'googlebot') + t.is( + getHeader('user-agent', { + accept: 'a', + 'User-Agent': 'first', + 'user-agent': 'second' + }), + 'first' + ) + t.is(getHeader('user-agent', { accept: 'a', host: 'h' }), undefined) + t.is(getHeader('User-Agent', { 'user-agent': 'googlebot' }), undefined) + t.is(getHeader('user-agent', { accept: 'a', 'user-agent': '' }), '') + t.throws(() => getHeader('user-agent', null), { instanceOf: TypeError }) + t.throws(() => getHeader('user-agent', undefined), { instanceOf: TypeError }) }) test('.sortHeaders', t => { - t.deepEqual( + assertHeaders( + t, sortHeaders({ 'x-random': 'foo', 'user-agent': UA.CHROME, @@ -23,13 +42,14 @@ test('.sortHeaders', t => { { Host: 'foo', 'user-agent': UA.CHROME, - referer: 'bar', accept: 'foo', + referer: 'bar', 'x-random': 'foo' } ) - t.deepEqual( + assertHeaders( + t, sortHeaders({ 'x-random': 'foo', 'user-agent': 'safari', @@ -37,10 +57,117 @@ test('.sortHeaders', t => { accept: 'foo' }), { - accept: 'foo', referer: 'bar', + accept: 'foo', 'user-agent': 'safari', 'x-random': 'foo' } ) }) + +test('sortHeaders orders firefox headers', t => { + assertHeaders( + t, + sortHeaders({ + 'x-random': 'foo', + 'User-Agent': UA.FIREFOX, + accept: 'foo', + Host: 'foo' + }), + { + Host: 'foo', + 'User-Agent': UA.FIREFOX, + accept: 'foo', + 'x-random': 'foo' + } + ) +}) + +test('sortHeaders keeps insertion order when no browser matches', t => { + const headers = { 'z-last': '1', accept: 'a', Host: 'h' } + const sorted = sortHeaders(headers) + assertHeaders(t, sorted, { 'z-last': '1', accept: 'a', Host: 'h' }) + t.not(sorted, headers) + + assertHeaders(t, sortHeaders({ 'user-agent': '', accept: 'a', host: 'h' }), { + 'user-agent': '', + accept: 'a', + host: 'h' + }) + assertHeaders(t, sortHeaders({}), {}) +}) + +test('sortHeaders ignores an inherited user-agent', t => { + const headers = Object.create({ 'user-agent': UA.CHROME, accept: 'inherited' }) + headers['x-own'] = 'yes' + + assertHeaders(t, sortHeaders(headers), { 'x-own': 'yes' }) +}) + +test('sortHeaders copies inherited ordered headers and skips inherited extras', t => { + const headers = Object.create({ accept: 'inherited', 'x-inherited': 'no' }) + headers['user-agent'] = UA.CHROME + headers['x-own'] = 'yes' + + assertHeaders(t, sortHeaders(headers), { + 'user-agent': UA.CHROME, + accept: 'inherited', + 'x-own': 'yes' + }) +}) + +test('sortHeaders keeps a casing that is not in the browser order', t => { + assertHeaders( + t, + sortHeaders({ + host: 'lower', + 'user-agent': UA.CHROME, + Host: 'upper' + }), + { + Host: 'upper', + 'user-agent': UA.CHROME, + host: 'lower' + } + ) +}) + +test('sortHeaders reads an ordered header once', t => { + let reads = 0 + const headers = { 'user-agent': UA.CHROME } + Object.defineProperty(headers, 'accept', { + enumerable: true, + get () { + reads += 1 + return 'foo' + } + }) + headers['x-random'] = 'z' + + assertHeaders(t, sortHeaders(headers), { + 'user-agent': UA.CHROME, + accept: 'foo', + 'x-random': 'z' + }) + t.is(reads, 1) +}) + +test('sortHeaders orders a null-prototype map', t => { + const headers = Object.create(null) + headers.Host = 'h' + headers['x-random'] = 'z' + headers['user-agent'] = UA.CHROME + + const sorted = sortHeaders(headers) + assertHeaders(t, sorted, { + Host: 'h', + 'user-agent': UA.CHROME, + 'x-random': 'z' + }) + t.not(sorted, headers) +}) + +test('sortHeaders throws on null and undefined', t => { + t.throws(() => sortHeaders(null), { instanceOf: TypeError }) + t.throws(() => sortHeaders(undefined), { instanceOf: TypeError }) +}) diff --git a/test/hook.js b/test/hook.js index 92ff940..ac64d7f 100644 --- a/test/hook.js +++ b/test/hook.js @@ -5,6 +5,7 @@ const userAgents = require('top-user-agents') const test = require('ava').default const decorate = require('../hook') +const { UA } = require('./helpers') const randomUserAgent = uniqueRandomArray(userAgents) @@ -29,3 +30,14 @@ test('does nothing if user agent is not present', t => { t.deepEqual(Object.keys(options.https), []) }) + +test('reads a mixed-case user-agent that is not the first header', t => { + const options = { + https: {}, + headers: { accept: 'text/html', 'User-Agent': UA.CHROME } + } + + decorate(options) + + t.is(options.https.minVersion, 'TLSv1') +})