current commit
260925.9 · fix: keep a header named __proto__ across features (#5688)
'use strict'

const { tspl } = require('@matteo.collina/tspl')
const { test } = require('node:test')
const { normalizeHeaders } = require('../../lib/util/cache')

test('normalizeHeaders handles plain object headers with polluted Object.prototype[Symbol.iterator]', (t) => {
  const { strictEqual } = tspl(t, { plan: 2 })

  const originalIterator = Object.prototype[Symbol.iterator]
  // eslint-disable-next-line no-extend-native
  Object.prototype[Symbol.iterator] = function * () {}

  try {
    const headers = normalizeHeaders({
      headers: {
        Authorization: 'Bearer token',
        'X-Test': 'ok'
      }
    })

    strictEqual(headers.authorization, 'Bearer token')
    strictEqual(headers['x-test'], 'ok')
  } finally {
    if (originalIterator === undefined) {
      delete Object.prototype[Symbol.iterator]
    } else {
      // eslint-disable-next-line no-extend-native
      Object.prototype[Symbol.iterator] = originalIterator
    }
  }
})

test('normalizeHeaders handles headers from Map', (t) => {
  const { strictEqual } = tspl(t, { plan: 1 })

  const headers = normalizeHeaders({
    headers: new Map([
      ['X-Test', 'ok']
    ])
  })

  strictEqual(headers['x-test'], 'ok')
})

test('normalizeHeaders preserves repeated iterable headers', (t) => {
  const { ok, strictEqual } = tspl(t, { plan: 4 })

  const headers = normalizeHeaders({
    headers: [
      ['Cache-Control', 'no-store'],
      ['cache-control', 'max-age=60']
    ]
  })

  ok(Array.isArray(headers['cache-control']))
  strictEqual(headers['cache-control'][0], 'no-store')
  strictEqual(headers['cache-control'][1], 'max-age=60')
  strictEqual(headers['cache-control'].length, 2)
})

test('normalizeHeaders preserves repeated plain-object headers with different casing', (t) => {
  const { ok, strictEqual } = tspl(t, { plan: 4 })

  const headers = normalizeHeaders({
    headers: {
      'Cache-Control': 'no-store',
      'cache-control': 'max-age=60'
    }
  })

  ok(Array.isArray(headers['cache-control']))
  strictEqual(headers['cache-control'][0], 'no-store')
  strictEqual(headers['cache-control'][1], 'max-age=60')
  strictEqual(headers['cache-control'].length, 2)
})

test('normalizeHeaders handles empty array', (t) => {
  const { deepEqual } = tspl(t, { plan: 1 })

  const headers = normalizeHeaders({ headers: [] })

  deepEqual(headers, {})
})

test('normalizeHeaders handles flat alternating array (single header)', (t) => {
  const { strictEqual } = tspl(t, { plan: 1 })

  const headers = normalizeHeaders({
    headers: ['host', 'localhost']
  })

  strictEqual(headers.host, 'localhost')
})

test('normalizeHeaders handles flat alternating array (multiple headers)', (t) => {
  const { deepEqual } = tspl(t, { plan: 1 })

  const headers = normalizeHeaders({
    headers: ['host', 'localhost', 'content-type', 'application/json']
  })

  deepEqual(headers, { host: 'localhost', 'content-type': 'application/json' })
})

test('normalizeHeaders handles flat alternating array with array values', (t) => {
  const { deepEqual } = tspl(t, { plan: 1 })

  const headers = normalizeHeaders({
    headers: ['accept', ['application/json', 'text/plain']]
  })

  deepEqual(headers, { accept: ['application/json', 'text/plain'] })
})

test('normalizeHeaders handles array-of-pairs (existing behavior)', (t) => {
  const { strictEqual } = tspl(t, { plan: 1 })

  const headers = normalizeHeaders({
    headers: [['host', 'localhost']]
  })

  strictEqual(headers.host, 'localhost')
})

test('normalizeHeaders throws on odd-length flat array', (t) => {
  const { throws } = require('node:assert')

  throws(() => normalizeHeaders({ headers: ['host'] }), {
    message: 'opts.headers is not a valid header map'
  })
})

test('normalizeHeaders throws on non-string key in flat array', (t) => {
  const { throws } = require('node:assert')

  throws(() => normalizeHeaders({ headers: [42, 'value'] }), {
    message: 'opts.headers is not a valid header map'
  })
})

test('normalizeHeaders keeps a header named __proto__ without reprototyping the result', (t) => {
  const { strictEqual, deepStrictEqual } = tspl(t, { plan: 5 })

  // JSON.parse is the everyday way an own `__proto__` key shows up, e.g.
  // headers read from a config file or a request body.
  const headers = normalizeHeaders({
    headers: JSON.parse('{"__proto__":"a","x-real":"same"}')
  })

  deepStrictEqual(Object.keys(headers).sort(), ['__proto__', 'x-real'])
  strictEqual(Object.getOwnPropertyDescriptor(headers, '__proto__').value, 'a')
  strictEqual(headers['x-real'], 'same')
  strictEqual(Object.getPrototypeOf(headers), Object.prototype)
  // A header named `length` must not resolve through an array prototype.
  strictEqual(headers.length, undefined)
})