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
194 changes: 136 additions & 58 deletions lib/api/headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,11 @@
const { types } = require('util')
const { validateHeaderName, validateHeaderValue } = require('http')
const { kHeadersList } = require('../core/symbols')

const headerRegex = /^[\n\t\r\x20]+|[\n\t\r\x20]+$/g

function normalizeAndValidateHeaderName (name) {
const normalizedHeaderName = name.toLowerCase()
validateHeaderName(normalizedHeaderName)
return normalizedHeaderName
}
const { InvalidHTTPTokenError, HTTPInvalidHeaderValueError, InvalidArgumentError, InvalidThisError } = require('../core/errors')

function binarySearch (arr, val) {
let low = 0
let high = arr.length / 2
let high = Math.floor(arr.length / 2)

while (high > low) {
const mid = (high + low) >>> 1
Expand All @@ -31,132 +24,217 @@ function binarySearch (arr, val) {
return low * 2
}

function normalizeAndValidateHeaderName (name) {
if (name === undefined) {
throw new InvalidHTTPTokenError(`Header name ${name}`)
}
const normalizedHeaderName = name.toLocaleLowerCase()
validateHeaderName(normalizedHeaderName)
return normalizedHeaderName
}

function normalizeAndValidateHeaderValue (name, value) {
const normalizedHeaderValue = `${value}`.replace(headerRegex, '')
if (value === undefined) {
throw new HTTPInvalidHeaderValueError(value, name)
}
const normalizedHeaderValue = `${value}`.replace(/^[\n\t\r\x20]+|[\n\t\r\x20]+$/g, '')
validateHeaderValue(name, normalizedHeaderValue)
return normalizedHeaderValue
}

function isHeaders (object) {
return kHeadersList in object
}

function fill (headers, object) {
if (Array.isArray(object)) {
if (isHeaders(object)) {
// Object is instance of Headers
headers[kHeadersList] = Array.splice(object[kHeadersList])
} else if (Array.isArray(object)) {
// Support both 1D and 2D arrays of header entries
if (Array.isArray(object[0])) {
for (let i = 0, header = object[0]; i < object.length; i++, header = object[i]) {
if (header.length !== 2) throw TypeError('header entry must be of length two')
headers.append(header[0], header[1])
// Array of arrays
for (let i = 0; i < object.length; i++) {
if (object[i].length !== 2) {
throw new InvalidArgumentError(`The argument 'init' is not of length 2. Received ${object[i]}`)
}
headers.append(object[i][0], object[i][1])
}
} else if (typeof object[0] === 'string' || Buffer.isBuffer(object[0])) {
if (object.length % 2 !== 0) throw TypeError('flattened header init must have even length')
// Flat array of strings or Buffers
if (object.length % 2 !== 0) {
throw new InvalidArgumentError(`The argument 'init' is not even in length. Received ${object}`)
}
for (let i = 0; i < object.length; i += 2) {
headers.append(object[i].toString('utf-8'), object[i + 1].toString('utf-8'))
headers.append(
object[i].toString('utf-8'),
object[i + 1].toString('utf-8')
)
}
} else {
if (object.length !== 0) throw TypeError('invalid array-based header init')
// All other array based entries
throw new InvalidArgumentError(`The argument 'init' is not a valid array entry. Received ${object}`)
}
} else if (kHeadersList in object) {
headers[kHeadersList] = new Array(...object[kHeadersList])
} else if (!types.isBoxedPrimitive(object)) {
for (const [name, value] of Object.entries(object)) {
headers.append(name, value)
// Object of key/value entries
const entries = Object.entries(object)
for (let i = 0; i < entries.length; i++) {
headers.append(entries[i][0], entries[i][1])
}
}
}

class Headers {
constructor (init) {
this[kHeadersList] = []
function validateArgumentLength (found, expected) {
if (found !== expected) {
throw new TypeError(`${expected} ${expected > 1 ? 'arguments' : 'argument'} required, but only ${found} present`)
}
}

if (init && typeof init === 'object') {
fill(this, init)
class Headers {
constructor (init = {}) {
// validateObject allowArray = true
if (!Array.isArray(init) && typeof init !== 'object') {
throw new InvalidArgumentError('The argument \'init\' must be one of type Object or Array')
}
this[kHeadersList] = []
fill(this, init)
}

append (...args) {
if (args.length !== 2) throw TypeError('Expected at least 2 arguments!')
if (!isHeaders(this)) {
throw new InvalidThisError('Header')
}

validateArgumentLength(args.length, 2)

const [name, value] = args
const normalizedName = normalizeAndValidateHeaderName(name)
const normalizedValue = normalizeAndValidateHeaderValue(name, value)

const i = binarySearch(this[kHeadersList], normalizedName)
const index = binarySearch(this[kHeadersList], normalizedName)

if (this[kHeadersList][i] === normalizedName) {
this[kHeadersList][i + 1] += `, ${normalizedValue}`
if (this[kHeadersList][index] === normalizedName) {
this[kHeadersList][index + 1] += `, ${normalizedValue}`
} else {
this[kHeadersList].splice(i, 0, normalizedName, normalizedValue)
this[kHeadersList].splice(index, 0, normalizedName, normalizedValue)
}
}

delete (name) {
delete (...args) {
if (!isHeaders(this)) {
throw new InvalidThisError('Header')
}

validateArgumentLength(args.length, 1)

const [name] = args

const normalizedName = normalizeAndValidateHeaderName(name)

const i = binarySearch(this[kHeadersList], normalizedName)
const index = binarySearch(this[kHeadersList], normalizedName)

if (this[kHeadersList][i] === normalizedName) {
this[kHeadersList].splice(i, 2)
if (this[kHeadersList][index] === normalizedName) {
this[kHeadersList].splice(index, 2)
}
}

get (name) {
get (...args) {
if (!isHeaders(this)) {
throw new InvalidThisError('Header')
}

validateArgumentLength(args.length, 1)

const [name] = args

const normalizedName = normalizeAndValidateHeaderName(name)

const i = binarySearch(this[kHeadersList], normalizedName)
const index = binarySearch(this[kHeadersList], normalizedName)

if (this[kHeadersList][i] === normalizedName) {
return this[kHeadersList][i + 1]
if (this[kHeadersList][index] === normalizedName) {
return this[kHeadersList][index + 1]
}

return null
}

has (name) {
has (...args) {
if (!isHeaders(this)) {
throw new InvalidThisError('Header')
}

validateArgumentLength(args.length, 1)

const [name] = args

const normalizedName = normalizeAndValidateHeaderName(name)

const i = binarySearch(this[kHeadersList], normalizedName)
const index = binarySearch(this[kHeadersList], normalizedName)

return this[kHeadersList][i] === normalizedName
return this[kHeadersList][index] === normalizedName
}

set (...args) {
if (args.length !== 2) throw TypeError('Expected at least 2 arguments!')
if (!isHeaders(this)) {
throw new InvalidThisError('Header')
}

validateArgumentLength(args.length, 2)

const [name, value] = args

const normalizedName = normalizeAndValidateHeaderName(name)
const normalizedValue = normalizeAndValidateHeaderValue(name, value)

const i = binarySearch(this[kHeadersList], normalizedName)
if (this[kHeadersList][i] === normalizedName) {
this[kHeadersList][i + 1] = normalizedValue
const index = binarySearch(this[kHeadersList], normalizedName)
if (this[kHeadersList][index] === normalizedName) {
this[kHeadersList][index + 1] = normalizedValue
} else {
this[kHeadersList].splice(i, 0, normalizedName, normalizedValue)
this[kHeadersList].splice(index, 0, normalizedName, normalizedValue)
}
}

* keys () {
for (const header of this) {
yield header[0]
if (!isHeaders(this)) {
throw new InvalidThisError('Headers')
}

for (let index = 0; index < this[kHeadersList].length; index += 2) {
yield this[kHeadersList][index]
}
}

* values () {
for (const header of this) {
yield header[1]
if (!isHeaders(this)) {
throw new InvalidThisError('Headers')
}

for (let index = 1; index < this[kHeadersList].length; index += 2) {
yield this[kHeadersList][index]
}
}

* entries () {
yield * this
if (!isHeaders(this)) {
throw new InvalidThisError('Headers')
}

for (let index = 0; index < this[kHeadersList].length; index += 2) {
yield [this[kHeadersList][index], this[kHeadersList][index + 1]]
}
}

forEach (callback, thisArg) {
for (let i = 0; i < this[kHeadersList].length; i += 2) {
callback.call(thisArg, this[kHeadersList][i + 1], this[kHeadersList][i], this)
if (!isHeaders(this)) {
throw new InvalidThisError('Headers')
}
}

* [Symbol.iterator] () {
for (let i = 0; i < this[kHeadersList].length; i += 2) {
yield [this[kHeadersList][i], this[kHeadersList][i + 1]]
for (let index = 0; index < this[kHeadersList].length; index += 2) {
callback.call(thisArg, this[kHeadersList][index + 1], this[kHeadersList][index], this)
}
}
}

Headers.prototype[Symbol.iterator] = Headers.prototype.entries

module.exports = Headers
32 changes: 31 additions & 1 deletion lib/core/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,33 @@ class HTTPParserError extends Error {
}
}

class InvalidHTTPTokenError extends TypeError {
constructor (name, token) {
super(`${name} must be a valid HTTP token ["${token}"]`)
Error.captureStackTrace(this, InvalidHTTPTokenError)
this.name = 'InvalidHTTPToken'
this.code = 'INVALID_HTTP_TOKEN'
}
}

class HTTPInvalidHeaderValueError extends TypeError {
constructor (name, value) {
super(`Invalid value "${value}" for header "${name}"`)
Error.captureStackTrace(this, HTTPInvalidHeaderValueError)
this.name = 'HTTPInvalidHeaderValue'
this.code = 'HTTP_INVALID_HEADER_VALUE'
}
}

class InvalidThisError extends TypeError {
constructor (type) {
super(`Value of "this" must be of type ${type}`)
Error.captureStackTrace(this, InvalidThisError)
this.name = 'InvalidThis'
this.code = 'INVALID_THIS'
}
}

module.exports = {
HTTPParserError,
UndiciError,
Expand All @@ -185,5 +212,8 @@ module.exports = {
InformationalError,
SocketError,
NotSupportedError,
ResponseContentLengthMismatchError
ResponseContentLengthMismatchError,
InvalidHTTPTokenError,
HTTPInvalidHeaderValueError,
InvalidThisError
}