summaryrefslogtreecommitdiffstats
path: root/test/fetch/headers.js
diff options
context:
space:
mode:
Diffstat (limited to 'test/fetch/headers.js')
-rw-r--r--test/fetch/headers.js743
1 files changed, 743 insertions, 0 deletions
diff --git a/test/fetch/headers.js b/test/fetch/headers.js
new file mode 100644
index 0000000..4846110
--- /dev/null
+++ b/test/fetch/headers.js
@@ -0,0 +1,743 @@
+'use strict'
+
+const tap = require('tap')
+const { Headers, fill } = require('../../lib/fetch/headers')
+const { kGuard } = require('../../lib/fetch/symbols')
+const { once } = require('events')
+const { fetch } = require('../..')
+const { createServer } = require('http')
+
+tap.test('Headers initialization', t => {
+ t.plan(8)
+
+ t.test('allows undefined', t => {
+ t.plan(1)
+
+ t.doesNotThrow(() => new Headers())
+ })
+
+ t.test('with array of header entries', t => {
+ t.plan(3)
+
+ t.test('fails on invalid array-based init', t => {
+ t.plan(3)
+ t.throws(
+ () => new Headers([['undici', 'fetch'], ['fetch']]),
+ TypeError('Headers constructor: expected name/value pair to be length 2, found 1.')
+ )
+ t.throws(() => new Headers(['undici', 'fetch', 'fetch']), TypeError)
+ t.throws(
+ () => new Headers([0, 1, 2]),
+ TypeError('Sequence: Value of type Number is not an Object.')
+ )
+ })
+
+ t.test('allows even length init', t => {
+ t.plan(1)
+ const init = [['undici', 'fetch'], ['fetch', 'undici']]
+ t.doesNotThrow(() => new Headers(init))
+ })
+
+ t.test('fails for event flattened init', t => {
+ t.plan(1)
+ const init = ['undici', 'fetch', 'fetch', 'undici']
+ t.throws(
+ () => new Headers(init),
+ TypeError('Sequence: Value of type String is not an Object.')
+ )
+ })
+ })
+
+ t.test('with object of header entries', t => {
+ t.plan(1)
+ const init = {
+ undici: 'fetch',
+ fetch: 'undici'
+ }
+ t.doesNotThrow(() => new Headers(init))
+ })
+
+ t.test('fails silently if a boxed primitive object is passed', t => {
+ t.plan(3)
+ /* eslint-disable no-new-wrappers */
+ t.doesNotThrow(() => new Headers(new Number()))
+ t.doesNotThrow(() => new Headers(new Boolean()))
+ t.doesNotThrow(() => new Headers(new String()))
+ /* eslint-enable no-new-wrappers */
+ })
+
+ t.test('fails if primitive is passed', t => {
+ t.plan(2)
+ const expectedTypeError = TypeError
+ t.throws(() => new Headers(1), expectedTypeError)
+ t.throws(() => new Headers('1'), expectedTypeError)
+ })
+
+ t.test('allows some weird stuff (because of webidl)', t => {
+ t.doesNotThrow(() => {
+ new Headers(function () {}) // eslint-disable-line no-new
+ })
+
+ t.doesNotThrow(() => {
+ new Headers(Function) // eslint-disable-line no-new
+ })
+
+ t.end()
+ })
+
+ t.test('allows a myriad of header values to be passed', t => {
+ t.plan(4)
+
+ // Headers constructor uses Headers.append
+
+ t.doesNotThrow(() => new Headers([
+ ['a', ['b', 'c']],
+ ['d', ['e', 'f']]
+ ]), 'allows any array values')
+ t.doesNotThrow(() => new Headers([
+ ['key', null]
+ ]), 'allows null values')
+ t.throws(() => new Headers([
+ ['key']
+ ]), 'throws when 2 arguments are not passed')
+ t.throws(() => new Headers([
+ ['key', 'value', 'value2']
+ ]), 'throws when too many arguments are passed')
+ })
+
+ t.test('accepts headers as objects with array values', t => {
+ t.plan(1)
+ const headers = new Headers({
+ c: '5',
+ b: ['3', '4'],
+ a: ['1', '2']
+ })
+
+ t.same([...headers.entries()], [
+ ['a', '1,2'],
+ ['b', '3,4'],
+ ['c', '5']
+ ])
+ })
+})
+
+tap.test('Headers append', t => {
+ t.plan(3)
+
+ t.test('adds valid header entry to instance', t => {
+ t.plan(2)
+ const headers = new Headers()
+
+ const name = 'undici'
+ const value = 'fetch'
+ t.doesNotThrow(() => headers.append(name, value))
+ t.equal(headers.get(name), value)
+ })
+
+ t.test('adds valid header to existing entry', t => {
+ t.plan(4)
+ const headers = new Headers()
+
+ const name = 'undici'
+ const value1 = 'fetch1'
+ const value2 = 'fetch2'
+ const value3 = 'fetch3'
+ headers.append(name, value1)
+ t.equal(headers.get(name), value1)
+ t.doesNotThrow(() => headers.append(name, value2))
+ t.doesNotThrow(() => headers.append(name, value3))
+ t.equal(headers.get(name), [value1, value2, value3].join(', '))
+ })
+
+ t.test('throws on invalid entry', t => {
+ t.plan(3)
+ const headers = new Headers()
+
+ t.throws(() => headers.append(), 'throws on missing name and value')
+ t.throws(() => headers.append('undici'), 'throws on missing value')
+ t.throws(() => headers.append('invalid @ header ? name', 'valid value'), 'throws on invalid name')
+ })
+})
+
+tap.test('Headers delete', t => {
+ t.plan(4)
+
+ t.test('deletes valid header entry from instance', t => {
+ t.plan(3)
+ const headers = new Headers()
+
+ const name = 'undici'
+ const value = 'fetch'
+ headers.append(name, value)
+ t.equal(headers.get(name), value)
+ t.doesNotThrow(() => headers.delete(name))
+ t.equal(headers.get(name), null)
+ })
+
+ t.test('does not mutate internal list when no match is found', t => {
+ t.plan(3)
+
+ const headers = new Headers()
+ const name = 'undici'
+ const value = 'fetch'
+ headers.append(name, value)
+ t.equal(headers.get(name), value)
+ t.doesNotThrow(() => headers.delete('not-undici'))
+ t.equal(headers.get(name), value)
+ })
+
+ t.test('throws on invalid entry', t => {
+ t.plan(2)
+ const headers = new Headers()
+
+ t.throws(() => headers.delete(), 'throws on missing namee')
+ t.throws(() => headers.delete('invalid @ header ? name'), 'throws on invalid name')
+ })
+
+ // https://github.com/nodejs/undici/issues/2429
+ t.test('`Headers#delete` returns undefined', t => {
+ t.plan(2)
+ const headers = new Headers({ test: 'test' })
+
+ t.same(headers.delete('test'), undefined)
+ t.same(headers.delete('test2'), undefined)
+ })
+})
+
+tap.test('Headers get', t => {
+ t.plan(3)
+
+ t.test('returns null if not found in instance', t => {
+ t.plan(1)
+ const headers = new Headers()
+ headers.append('undici', 'fetch')
+
+ t.equal(headers.get('not-undici'), null)
+ })
+
+ t.test('returns header values from valid header name', t => {
+ t.plan(2)
+ const headers = new Headers()
+
+ const name = 'undici'; const value1 = 'fetch1'; const value2 = 'fetch2'
+ headers.append(name, value1)
+ t.equal(headers.get(name), value1)
+ headers.append(name, value2)
+ t.equal(headers.get(name), [value1, value2].join(', '))
+ })
+
+ t.test('throws on invalid entry', t => {
+ t.plan(2)
+ const headers = new Headers()
+
+ t.throws(() => headers.get(), 'throws on missing name')
+ t.throws(() => headers.get('invalid @ header ? name'), 'throws on invalid name')
+ })
+})
+
+tap.test('Headers has', t => {
+ t.plan(2)
+
+ t.test('returns boolean existence for a header name', t => {
+ t.plan(2)
+ const headers = new Headers()
+
+ const name = 'undici'
+ headers.append('not-undici', 'fetch')
+ t.equal(headers.has(name), false)
+ headers.append(name, 'fetch')
+ t.equal(headers.has(name), true)
+ })
+
+ t.test('throws on invalid entry', t => {
+ t.plan(2)
+ const headers = new Headers()
+
+ t.throws(() => headers.has(), 'throws on missing name')
+ t.throws(() => headers.has('invalid @ header ? name'), 'throws on invalid name')
+ })
+})
+
+tap.test('Headers set', t => {
+ t.plan(5)
+
+ t.test('sets valid header entry to instance', t => {
+ t.plan(2)
+ const headers = new Headers()
+
+ const name = 'undici'
+ const value = 'fetch'
+ headers.append('not-undici', 'fetch')
+ t.doesNotThrow(() => headers.set(name, value))
+ t.equal(headers.get(name), value)
+ })
+
+ t.test('overwrites existing entry', t => {
+ t.plan(4)
+ const headers = new Headers()
+
+ const name = 'undici'
+ const value1 = 'fetch1'
+ const value2 = 'fetch2'
+ t.doesNotThrow(() => headers.set(name, value1))
+ t.equal(headers.get(name), value1)
+ t.doesNotThrow(() => headers.set(name, value2))
+ t.equal(headers.get(name), value2)
+ })
+
+ t.test('allows setting a myriad of values', t => {
+ t.plan(4)
+ const headers = new Headers()
+
+ t.doesNotThrow(() => headers.set('a', ['b', 'c']), 'sets array values properly')
+ t.doesNotThrow(() => headers.set('b', null), 'allows setting null values')
+ t.throws(() => headers.set('c'), 'throws when 2 arguments are not passed')
+ t.doesNotThrow(() => headers.set('c', 'd', 'e'), 'ignores extra arguments')
+ })
+
+ t.test('throws on invalid entry', t => {
+ t.plan(3)
+ const headers = new Headers()
+
+ t.throws(() => headers.set(), 'throws on missing name and value')
+ t.throws(() => headers.set('undici'), 'throws on missing value')
+ t.throws(() => headers.set('invalid @ header ? name', 'valid value'), 'throws on invalid name')
+ })
+
+ // https://github.com/nodejs/undici/issues/2431
+ t.test('`Headers#set` returns undefined', t => {
+ t.plan(2)
+ const headers = new Headers()
+
+ t.same(headers.set('a', 'b'), undefined)
+
+ t.notOk(headers.set('c', 'd') instanceof Map)
+ })
+})
+
+tap.test('Headers forEach', t => {
+ const headers = new Headers([['a', 'b'], ['c', 'd']])
+
+ t.test('standard', t => {
+ t.equal(typeof headers.forEach, 'function')
+
+ headers.forEach((value, key, headerInstance) => {
+ t.ok(value === 'b' || value === 'd')
+ t.ok(key === 'a' || key === 'c')
+ t.equal(headers, headerInstance)
+ })
+
+ t.end()
+ })
+
+ t.test('when no thisArg is set, it is globalThis', (t) => {
+ headers.forEach(function () {
+ t.equal(this, globalThis)
+ })
+
+ t.end()
+ })
+
+ t.test('with thisArg', t => {
+ const thisArg = { a: Math.random() }
+ headers.forEach(function () {
+ t.equal(this, thisArg)
+ }, thisArg)
+
+ t.end()
+ })
+
+ t.end()
+})
+
+tap.test('Headers as Iterable', t => {
+ t.plan(7)
+
+ t.test('should freeze values while iterating', t => {
+ t.plan(1)
+ const init = [
+ ['foo', '123'],
+ ['bar', '456']
+ ]
+ const expected = [
+ ['foo', '123'],
+ ['x-x-bar', '456']
+ ]
+ const headers = new Headers(init)
+ for (const [key, val] of headers) {
+ headers.delete(key)
+ headers.set(`x-${key}`, val)
+ }
+ t.strictSame([...headers], expected)
+ })
+
+ t.test('returns combined and sorted entries using .forEach()', t => {
+ t.plan(8)
+ const init = [
+ ['a', '1'],
+ ['b', '2'],
+ ['c', '3'],
+ ['abc', '4'],
+ ['b', '5']
+ ]
+ const expected = [
+ ['a', '1'],
+ ['abc', '4'],
+ ['b', '2, 5'],
+ ['c', '3']
+ ]
+ const headers = new Headers(init)
+ const that = {}
+ let i = 0
+ headers.forEach(function (value, key, _headers) {
+ t.strictSame(expected[i++], [key, value])
+ t.equal(this, that)
+ }, that)
+ })
+
+ t.test('returns combined and sorted entries using .entries()', t => {
+ t.plan(4)
+ const init = [
+ ['a', '1'],
+ ['b', '2'],
+ ['c', '3'],
+ ['abc', '4'],
+ ['b', '5']
+ ]
+ const expected = [
+ ['a', '1'],
+ ['abc', '4'],
+ ['b', '2, 5'],
+ ['c', '3']
+ ]
+ const headers = new Headers(init)
+ let i = 0
+ for (const header of headers.entries()) {
+ t.strictSame(header, expected[i++])
+ }
+ })
+
+ t.test('returns combined and sorted keys using .keys()', t => {
+ t.plan(4)
+ const init = [
+ ['a', '1'],
+ ['b', '2'],
+ ['c', '3'],
+ ['abc', '4'],
+ ['b', '5']
+ ]
+ const expected = ['a', 'abc', 'b', 'c']
+ const headers = new Headers(init)
+ let i = 0
+ for (const key of headers.keys()) {
+ t.strictSame(key, expected[i++])
+ }
+ })
+
+ t.test('returns combined and sorted values using .values()', t => {
+ t.plan(4)
+ const init = [
+ ['a', '1'],
+ ['b', '2'],
+ ['c', '3'],
+ ['abc', '4'],
+ ['b', '5']
+ ]
+ const expected = ['1', '4', '2, 5', '3']
+ const headers = new Headers(init)
+ let i = 0
+ for (const value of headers.values()) {
+ t.strictSame(value, expected[i++])
+ }
+ })
+
+ t.test('returns combined and sorted entries using for...of loop', t => {
+ t.plan(5)
+ const init = [
+ ['a', '1'],
+ ['b', '2'],
+ ['c', '3'],
+ ['abc', '4'],
+ ['b', '5'],
+ ['d', ['6', '7']]
+ ]
+ const expected = [
+ ['a', '1'],
+ ['abc', '4'],
+ ['b', '2, 5'],
+ ['c', '3'],
+ ['d', '6,7']
+ ]
+ let i = 0
+ for (const header of new Headers(init)) {
+ t.strictSame(header, expected[i++])
+ }
+ })
+
+ t.test('validate append ordering', t => {
+ t.plan(1)
+ const headers = new Headers([['b', '2'], ['c', '3'], ['e', '5']])
+ headers.append('d', '4')
+ headers.append('a', '1')
+ headers.append('f', '6')
+ headers.append('c', '7')
+ headers.append('abc', '8')
+
+ const expected = [...new Map([
+ ['a', '1'],
+ ['abc', '8'],
+ ['b', '2'],
+ ['c', '3, 7'],
+ ['d', '4'],
+ ['e', '5'],
+ ['f', '6']
+ ])]
+
+ t.same([...headers], expected)
+ })
+})
+
+tap.test('arg validation', (t) => {
+ // fill
+ t.throws(() => {
+ fill({}, 0)
+ }, TypeError)
+
+ const headers = new Headers()
+
+ // constructor
+ t.throws(() => {
+ // eslint-disable-next-line
+ new Headers(0)
+ }, TypeError)
+
+ // get [Symbol.toStringTag]
+ t.doesNotThrow(() => {
+ Object.prototype.toString.call(Headers.prototype)
+ })
+
+ // toString
+ t.doesNotThrow(() => {
+ Headers.prototype.toString.call(null)
+ })
+
+ // append
+ t.throws(() => {
+ Headers.prototype.append.call(null)
+ }, TypeError)
+ t.throws(() => {
+ headers.append()
+ }, TypeError)
+
+ // delete
+ t.throws(() => {
+ Headers.prototype.delete.call(null)
+ }, TypeError)
+ t.throws(() => {
+ headers.delete()
+ }, TypeError)
+
+ // get
+ t.throws(() => {
+ Headers.prototype.get.call(null)
+ }, TypeError)
+ t.throws(() => {
+ headers.get()
+ }, TypeError)
+
+ // has
+ t.throws(() => {
+ Headers.prototype.has.call(null)
+ }, TypeError)
+ t.throws(() => {
+ headers.has()
+ }, TypeError)
+
+ // set
+ t.throws(() => {
+ Headers.prototype.set.call(null)
+ }, TypeError)
+ t.throws(() => {
+ headers.set()
+ }, TypeError)
+
+ // forEach
+ t.throws(() => {
+ Headers.prototype.forEach.call(null)
+ }, TypeError)
+ t.throws(() => {
+ headers.forEach()
+ }, TypeError)
+ t.throws(() => {
+ headers.forEach(1)
+ }, TypeError)
+
+ // inspect
+ t.throws(() => {
+ Headers.prototype[Symbol.for('nodejs.util.inspect.custom')].call(null)
+ }, TypeError)
+
+ t.end()
+})
+
+tap.test('function signature verification', (t) => {
+ t.test('function length', (t) => {
+ t.equal(Headers.prototype.append.length, 2)
+ t.equal(Headers.prototype.constructor.length, 0)
+ t.equal(Headers.prototype.delete.length, 1)
+ t.equal(Headers.prototype.entries.length, 0)
+ t.equal(Headers.prototype.forEach.length, 1)
+ t.equal(Headers.prototype.get.length, 1)
+ t.equal(Headers.prototype.has.length, 1)
+ t.equal(Headers.prototype.keys.length, 0)
+ t.equal(Headers.prototype.set.length, 2)
+ t.equal(Headers.prototype.values.length, 0)
+ t.equal(Headers.prototype[Symbol.iterator].length, 0)
+ t.equal(Headers.prototype.toString.length, 0)
+
+ t.end()
+ })
+
+ t.test('function equality', (t) => {
+ t.equal(Headers.prototype.entries, Headers.prototype[Symbol.iterator])
+ t.equal(Headers.prototype.toString, Object.prototype.toString)
+
+ t.end()
+ })
+
+ t.test('toString and Symbol.toStringTag', (t) => {
+ t.equal(Object.prototype.toString.call(Headers.prototype), '[object Headers]')
+ t.equal(Headers.prototype[Symbol.toStringTag], 'Headers')
+ t.equal(Headers.prototype.toString.call(null), '[object Null]')
+
+ t.end()
+ })
+
+ t.end()
+})
+
+tap.test('various init paths of Headers', (t) => {
+ const h1 = new Headers()
+ const h2 = new Headers({})
+ const h3 = new Headers(undefined)
+ t.equal([...h1.entries()].length, 0)
+ t.equal([...h2.entries()].length, 0)
+ t.equal([...h3.entries()].length, 0)
+
+ t.end()
+})
+
+tap.test('immutable guard', (t) => {
+ const headers = new Headers()
+ headers.set('key', 'val')
+ headers[kGuard] = 'immutable'
+
+ t.throws(() => {
+ headers.set('asd', 'asd')
+ })
+ t.throws(() => {
+ headers.append('asd', 'asd')
+ })
+ t.throws(() => {
+ headers.delete('asd')
+ })
+ t.equal(headers.get('key'), 'val')
+ t.equal(headers.has('key'), true)
+
+ t.end()
+})
+
+tap.test('request-no-cors guard', (t) => {
+ const headers = new Headers()
+ headers[kGuard] = 'request-no-cors'
+ t.doesNotThrow(() => { headers.set('key', 'val') })
+ t.doesNotThrow(() => { headers.append('key', 'val') })
+ t.doesNotThrow(() => { headers.delete('key') })
+ t.end()
+})
+
+tap.test('invalid headers', (t) => {
+ t.doesNotThrow(() => new Headers({ "abcdefghijklmnopqrstuvwxyz0123456789!#$%&'*+-.^_`|~": 'test' }))
+
+ const chars = '"(),/:;<=>?@[\\]{}'.split('')
+
+ for (const char of chars) {
+ t.throws(() => new Headers({ [char]: 'test' }), TypeError, `The string "${char}" should throw an error.`)
+ }
+
+ for (const byte of ['\r', '\n', '\t', ' ', String.fromCharCode(128), '']) {
+ t.throws(() => {
+ new Headers().set(byte, 'test')
+ }, TypeError, 'invalid header name')
+ }
+
+ for (const byte of [
+ '\0',
+ '\r',
+ '\n'
+ ]) {
+ t.throws(() => {
+ new Headers().set('a', `a${byte}b`)
+ }, TypeError, 'not allowed at all in header value')
+ }
+
+ t.doesNotThrow(() => {
+ new Headers().set('a', '\r')
+ })
+
+ t.doesNotThrow(() => {
+ new Headers().set('a', '\n')
+ })
+
+ t.throws(() => {
+ new Headers().set('a', Symbol('symbol'))
+ }, TypeError, 'symbols should throw')
+
+ t.end()
+})
+
+tap.test('headers that might cause a ReDoS', (t) => {
+ t.doesNotThrow(() => {
+ // This test will time out if the ReDoS attack is successful.
+ const headers = new Headers()
+ const attack = 'a' + '\t'.repeat(500_000) + '\ta'
+ headers.append('fhqwhgads', attack)
+ })
+
+ t.end()
+})
+
+tap.test('Headers.prototype.getSetCookie', (t) => {
+ t.test('Mutating the returned list does not affect the set-cookie list', (t) => {
+ const h = new Headers([
+ ['set-cookie', 'a=b'],
+ ['set-cookie', 'c=d']
+ ])
+
+ const old = h.getSetCookie()
+ h.getSetCookie().push('oh=no')
+ const now = h.getSetCookie()
+
+ t.same(old, now)
+ t.end()
+ })
+
+ // https://github.com/nodejs/undici/issues/1935
+ t.test('When Headers are cloned, so are the cookies', async (t) => {
+ const server = createServer((req, res) => {
+ res.setHeader('Set-Cookie', 'test=onetwo')
+ res.end('Hello World!')
+ }).listen(0)
+
+ await once(server, 'listening')
+ t.teardown(server.close.bind(server))
+
+ const res = await fetch(`http://localhost:${server.address().port}`)
+ const entries = Object.fromEntries(res.headers.entries())
+
+ t.same(res.headers.getSetCookie(), ['test=onetwo'])
+ t.ok('set-cookie' in entries)
+ })
+
+ t.end()
+})