Skip to content
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
42 changes: 16 additions & 26 deletions src/headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
135 changes: 131 additions & 4 deletions test/headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -23,24 +42,132 @@ 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',
referer: 'bar',
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 })
})
12 changes: 12 additions & 0 deletions test/hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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')
})
Loading