rsnext/packages/next/server/web/spec-compliant/headers.ts
Javi Velasco a815ba9f79
Implement Middleware RFC (#30081)
This PR adds support for [Middleware as per RFC ](https://github.com/vercel/next.js/discussions/29750). 

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes
2021-10-20 17:52:11 +00:00

239 lines
5.5 KiB
TypeScript

import { isIterable } from '../is'
export type HeadersInit = Headers | string[][] | { [key: string]: string }
const MAP = Symbol('map')
const INTERNAL = Symbol('internal')
const INVALID_TOKEN_REGEX = /[^^_`a-zA-Z\-0-9!#$%&'*+.|~]/
const INVALID_HEADER_CHAR_REGEX = /[^\t\x20-\x7e\x80-\xff]/
class BaseHeaders implements Headers {
[MAP]: { [k: string]: string[] } = {}
constructor(init?: HeadersInit) {
if (init instanceof BaseHeaders) {
const rawHeaders = init.raw()
for (const headerName of Object.keys(rawHeaders)) {
for (const value of rawHeaders[headerName]) {
this.append(headerName, value)
}
}
} else if (isIterable(init)) {
const pairs = []
for (const pair of init) {
if (!isIterable(pair)) {
throw new TypeError('Each header pair must be iterable')
}
pairs.push(Array.from(pair))
}
for (const pair of pairs) {
if (pair.length !== 2) {
throw new TypeError('Each header pair must be a name/value tuple')
}
this.append(pair[0], pair[1])
}
} else if (typeof init === 'object') {
for (const key of Object.keys(init)) {
this.append(key, init[key])
}
} else if (init) {
throw new TypeError('Provided initializer must be an object')
}
}
get(name: string) {
const _name = `${name}`
validateName(_name)
const key = find(this[MAP], _name)
if (key === undefined) {
return null
}
return this[MAP][key].join(', ')
}
forEach(
callback: (value: string, name: string, parent: BaseHeaders) => void,
thisArg: any = undefined
): void {
let pairs = getHeaders(this)
let i = 0
while (i < pairs.length) {
const [name, value] = pairs[i]
callback.call(thisArg, value, name, this)
pairs = getHeaders(this)
i++
}
}
set(name: string, value: string) {
name = `${name}`
value = `${value}`
validateName(name)
validateValue(value)
const key = find(this[MAP], name)
this[MAP][key !== undefined ? key : name] = [value]
}
append(name: string, value: string) {
name = `${name}`
value = `${value}`
validateName(name)
validateValue(value)
const key = find(this[MAP], name)
if (key !== undefined) {
this[MAP][key].push(value)
} else {
this[MAP][name] = [value]
}
}
has(name: string) {
name = `${name}`
validateName(name)
return find(this[MAP], name) !== undefined
}
delete(name: string) {
name = `${name}`
validateName(name)
const key = find(this[MAP], name)
if (key !== undefined) {
delete this[MAP][key]
}
}
raw() {
return this[MAP]
}
keys() {
return createHeadersIterator(this, 'key')
}
values() {
return createHeadersIterator(this, 'value')
}
entries() {
return createHeadersIterator(this, 'key+value')
}
[Symbol.iterator]() {
return createHeadersIterator(this, 'key+value')
}
}
function createHeadersIterator(
target: BaseHeaders,
kind: 'key' | 'value' | 'key+value'
) {
const iterator = Object.create(HeadersIteratorPrototype)
iterator[INTERNAL] = {
target,
kind,
index: 0,
}
return iterator
}
function validateName(name: string) {
name = `${name}`
if (INVALID_TOKEN_REGEX.test(name)) {
throw new TypeError(`${name} is not a legal HTTP header name`)
}
}
function validateValue(value: string) {
value = `${value}`
if (INVALID_HEADER_CHAR_REGEX.test(value)) {
throw new TypeError(`${value} is not a legal HTTP header value`)
}
}
function find(
map: { [k: string]: string[] },
name: string
): string | undefined {
name = name.toLowerCase()
for (const key in map) {
if (key.toLowerCase() === name) {
return key
}
}
return undefined
}
Object.defineProperty(BaseHeaders.prototype, Symbol.toStringTag, {
value: 'Headers',
writable: false,
enumerable: false,
configurable: true,
})
Object.defineProperties(BaseHeaders.prototype, {
append: { enumerable: true },
delete: { enumerable: true },
entries: { enumerable: true },
forEach: { enumerable: true },
get: { enumerable: true },
has: { enumerable: true },
keys: { enumerable: true },
raw: { enumerable: false },
set: { enumerable: true },
values: { enumerable: true },
})
function getHeaders(
headers: BaseHeaders,
kind: 'key' | 'value' | 'key+value' = 'key+value'
) {
const fn =
kind === 'key'
? (key: string) => key.toLowerCase()
: kind === 'value'
? (key: string) => headers[MAP][key].join(', ')
: (key: string) => [key.toLowerCase(), headers[MAP][key].join(', ')]
return Object.keys(headers[MAP])
.sort()
.map((key) => fn(key))
}
const HeadersIteratorPrototype = Object.setPrototypeOf(
{
next() {
if (!this || Object.getPrototypeOf(this) !== HeadersIteratorPrototype) {
throw new TypeError('Value of `this` is not a HeadersIterator')
}
const { target, kind, index } = this[INTERNAL]
const values = getHeaders(target, kind)
const len = values.length
if (index >= len) {
return {
value: undefined,
done: true,
}
}
this[INTERNAL].index = index + 1
return {
value: values[index],
done: false,
}
},
},
Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))
)
Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, {
value: 'HeadersIterator',
writable: false,
enumerable: false,
configurable: true,
})
export { BaseHeaders as Headers }