Spaces:
Sleeping
Sleeping
/** | |
* Headers.js | |
* | |
* Headers class offers convenient helpers | |
*/ | |
import {types} from 'node:util'; | |
import http from 'node:http'; | |
/* c8 ignore next 9 */ | |
const validateHeaderName = typeof http.validateHeaderName === 'function' ? | |
http.validateHeaderName : | |
name => { | |
if (!/^[\^`\-\w!#$%&'*+.|~]+$/.test(name)) { | |
const error = new TypeError(`Header name must be a valid HTTP token [${name}]`); | |
Object.defineProperty(error, 'code', {value: 'ERR_INVALID_HTTP_TOKEN'}); | |
throw error; | |
} | |
}; | |
/* c8 ignore next 9 */ | |
const validateHeaderValue = typeof http.validateHeaderValue === 'function' ? | |
http.validateHeaderValue : | |
(name, value) => { | |
if (/[^\t\u0020-\u007E\u0080-\u00FF]/.test(value)) { | |
const error = new TypeError(`Invalid character in header content ["${name}"]`); | |
Object.defineProperty(error, 'code', {value: 'ERR_INVALID_CHAR'}); | |
throw error; | |
} | |
}; | |
/** | |
* @typedef {Headers | Record<string, string> | Iterable<readonly [string, string]> | Iterable<Iterable<string>>} HeadersInit | |
*/ | |
/** | |
* This Fetch API interface allows you to perform various actions on HTTP request and response headers. | |
* These actions include retrieving, setting, adding to, and removing. | |
* A Headers object has an associated header list, which is initially empty and consists of zero or more name and value pairs. | |
* You can add to this using methods like append() (see Examples.) | |
* In all methods of this interface, header names are matched by case-insensitive byte sequence. | |
* | |
*/ | |
export default class Headers extends URLSearchParams { | |
/** | |
* Headers class | |
* | |
* @constructor | |
* @param {HeadersInit} [init] - Response headers | |
*/ | |
constructor(init) { | |
// Validate and normalize init object in [name, value(s)][] | |
/** @type {string[][]} */ | |
let result = []; | |
if (init instanceof Headers) { | |
const raw = init.raw(); | |
for (const [name, values] of Object.entries(raw)) { | |
result.push(...values.map(value => [name, value])); | |
} | |
} else if (init == null) { // eslint-disable-line no-eq-null, eqeqeq | |
// No op | |
} else if (typeof init === 'object' && !types.isBoxedPrimitive(init)) { | |
const method = init[Symbol.iterator]; | |
// eslint-disable-next-line no-eq-null, eqeqeq | |
if (method == null) { | |
// Record<ByteString, ByteString> | |
result.push(...Object.entries(init)); | |
} else { | |
if (typeof method !== 'function') { | |
throw new TypeError('Header pairs must be iterable'); | |
} | |
// Sequence<sequence<ByteString>> | |
// Note: per spec we have to first exhaust the lists then process them | |
result = [...init] | |
.map(pair => { | |
if ( | |
typeof pair !== 'object' || types.isBoxedPrimitive(pair) | |
) { | |
throw new TypeError('Each header pair must be an iterable object'); | |
} | |
return [...pair]; | |
}).map(pair => { | |
if (pair.length !== 2) { | |
throw new TypeError('Each header pair must be a name/value tuple'); | |
} | |
return [...pair]; | |
}); | |
} | |
} else { | |
throw new TypeError('Failed to construct \'Headers\': The provided value is not of type \'(sequence<sequence<ByteString>> or record<ByteString, ByteString>)'); | |
} | |
// Validate and lowercase | |
result = | |
result.length > 0 ? | |
result.map(([name, value]) => { | |
validateHeaderName(name); | |
validateHeaderValue(name, String(value)); | |
return [String(name).toLowerCase(), String(value)]; | |
}) : | |
undefined; | |
super(result); | |
// Returning a Proxy that will lowercase key names, validate parameters and sort keys | |
// eslint-disable-next-line no-constructor-return | |
return new Proxy(this, { | |
get(target, p, receiver) { | |
switch (p) { | |
case 'append': | |
case 'set': | |
return (name, value) => { | |
validateHeaderName(name); | |
validateHeaderValue(name, String(value)); | |
return URLSearchParams.prototype[p].call( | |
target, | |
String(name).toLowerCase(), | |
String(value) | |
); | |
}; | |
case 'delete': | |
case 'has': | |
case 'getAll': | |
return name => { | |
validateHeaderName(name); | |
return URLSearchParams.prototype[p].call( | |
target, | |
String(name).toLowerCase() | |
); | |
}; | |
case 'keys': | |
return () => { | |
target.sort(); | |
return new Set(URLSearchParams.prototype.keys.call(target)).keys(); | |
}; | |
default: | |
return Reflect.get(target, p, receiver); | |
} | |
} | |
}); | |
/* c8 ignore next */ | |
} | |
get [Symbol.toStringTag]() { | |
return this.constructor.name; | |
} | |
toString() { | |
return Object.prototype.toString.call(this); | |
} | |
get(name) { | |
const values = this.getAll(name); | |
if (values.length === 0) { | |
return null; | |
} | |
let value = values.join(', '); | |
if (/^content-encoding$/i.test(name)) { | |
value = value.toLowerCase(); | |
} | |
return value; | |
} | |
forEach(callback, thisArg = undefined) { | |
for (const name of this.keys()) { | |
Reflect.apply(callback, thisArg, [this.get(name), name, this]); | |
} | |
} | |
* values() { | |
for (const name of this.keys()) { | |
yield this.get(name); | |
} | |
} | |
/** | |
* @type {() => IterableIterator<[string, string]>} | |
*/ | |
* entries() { | |
for (const name of this.keys()) { | |
yield [name, this.get(name)]; | |
} | |
} | |
[Symbol.iterator]() { | |
return this.entries(); | |
} | |
/** | |
* Node-fetch non-spec method | |
* returning all headers and their values as array | |
* @returns {Record<string, string[]>} | |
*/ | |
raw() { | |
return [...this.keys()].reduce((result, key) => { | |
result[key] = this.getAll(key); | |
return result; | |
}, {}); | |
} | |
/** | |
* For better console.log(headers) and also to convert Headers into Node.js Request compatible format | |
*/ | |
[Symbol.for('nodejs.util.inspect.custom')]() { | |
return [...this.keys()].reduce((result, key) => { | |
const values = this.getAll(key); | |
// Http.request() only supports string as Host header. | |
// This hack makes specifying custom Host header possible. | |
if (key === 'host') { | |
result[key] = values[0]; | |
} else { | |
result[key] = values.length > 1 ? values : values[0]; | |
} | |
return result; | |
}, {}); | |
} | |
} | |
/** | |
* Re-shaping object for Web IDL tests | |
* Only need to do it for overridden methods | |
*/ | |
Object.defineProperties( | |
Headers.prototype, | |
['get', 'entries', 'forEach', 'values'].reduce((result, property) => { | |
result[property] = {enumerable: true}; | |
return result; | |
}, {}) | |
); | |
/** | |
* Create a Headers object from an http.IncomingMessage.rawHeaders, ignoring those that do | |
* not conform to HTTP grammar productions. | |
* @param {import('http').IncomingMessage['rawHeaders']} headers | |
*/ | |
export function fromRawHeaders(headers = []) { | |
return new Headers( | |
headers | |
// Split into pairs | |
.reduce((result, value, index, array) => { | |
if (index % 2 === 0) { | |
result.push(array.slice(index, index + 2)); | |
} | |
return result; | |
}, []) | |
.filter(([name, value]) => { | |
try { | |
validateHeaderName(name); | |
validateHeaderValue(name, String(value)); | |
return true; | |
} catch { | |
return false; | |
} | |
}) | |
); | |
} | |