-
Notifications
You must be signed in to change notification settings - Fork 26k
/
headers.ts
231 lines (192 loc) · 7.15 KB
/
headers.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
import type { IncomingHttpHeaders } from 'http'
import { ReflectAdapter } from './reflect'
/**
* @internal
*/
export class ReadonlyHeadersError extends Error {
constructor() {
super(
'Headers cannot be modified. Read more: https://nextjs.org/docs/api-reference/headers'
)
}
public static callable() {
throw new ReadonlyHeadersError()
}
}
export type ReadonlyHeaders = Headers & {
/** @deprecated Method unavailable on `ReadonlyHeaders`. Read more: https://nextjs.org/docs/api-reference/headers */
append(...args: any[]): void
/** @deprecated Method unavailable on `ReadonlyHeaders`. Read more: https://nextjs.org/docs/api-reference/headers */
set(...args: any[]): void
/** @deprecated Method unavailable on `ReadonlyHeaders`. Read more: https://nextjs.org/docs/api-reference/headers */
delete(...args: any[]): void
}
export class HeadersAdapter extends Headers {
private readonly headers: IncomingHttpHeaders
constructor(headers: IncomingHttpHeaders) {
// We've already overridden the methods that would be called, so we're just
// calling the super constructor to ensure that the instanceof check works.
super()
this.headers = new Proxy(headers, {
get(target, prop, receiver) {
// Because this is just an object, we expect that all "get" operations
// are for properties. If it's a "get" for a symbol, we'll just return
// the symbol.
if (typeof prop === 'symbol') {
return ReflectAdapter.get(target, prop, receiver)
}
const lowercased = prop.toLowerCase()
// Let's find the original casing of the key. This assumes that there is
// no mixed case keys (e.g. "Content-Type" and "content-type") in the
// headers object.
const original = Object.keys(headers).find(
(o) => o.toLowerCase() === lowercased
)
// If the original casing doesn't exist, return undefined.
if (typeof original === 'undefined') return
// If the original casing exists, return the value.
return ReflectAdapter.get(target, original, receiver)
},
set(target, prop, value, receiver) {
if (typeof prop === 'symbol') {
return ReflectAdapter.set(target, prop, value, receiver)
}
const lowercased = prop.toLowerCase()
// Let's find the original casing of the key. This assumes that there is
// no mixed case keys (e.g. "Content-Type" and "content-type") in the
// headers object.
const original = Object.keys(headers).find(
(o) => o.toLowerCase() === lowercased
)
// If the original casing doesn't exist, use the prop as the key.
return ReflectAdapter.set(target, original ?? prop, value, receiver)
},
has(target, prop) {
if (typeof prop === 'symbol') return ReflectAdapter.has(target, prop)
const lowercased = prop.toLowerCase()
// Let's find the original casing of the key. This assumes that there is
// no mixed case keys (e.g. "Content-Type" and "content-type") in the
// headers object.
const original = Object.keys(headers).find(
(o) => o.toLowerCase() === lowercased
)
// If the original casing doesn't exist, return false.
if (typeof original === 'undefined') return false
// If the original casing exists, return true.
return ReflectAdapter.has(target, original)
},
deleteProperty(target, prop) {
if (typeof prop === 'symbol')
return ReflectAdapter.deleteProperty(target, prop)
const lowercased = prop.toLowerCase()
// Let's find the original casing of the key. This assumes that there is
// no mixed case keys (e.g. "Content-Type" and "content-type") in the
// headers object.
const original = Object.keys(headers).find(
(o) => o.toLowerCase() === lowercased
)
// If the original casing doesn't exist, return true.
if (typeof original === 'undefined') return true
// If the original casing exists, delete the property.
return ReflectAdapter.deleteProperty(target, original)
},
})
}
/**
* Seals a Headers instance to prevent modification by throwing an error when
* any mutating method is called.
*/
public static seal(headers: Headers): ReadonlyHeaders {
return new Proxy<ReadonlyHeaders>(headers, {
get(target, prop, receiver) {
switch (prop) {
case 'append':
case 'delete':
case 'set':
return ReadonlyHeadersError.callable
default:
return ReflectAdapter.get(target, prop, receiver)
}
},
})
}
/**
* Merges a header value into a string. This stores multiple values as an
* array, so we need to merge them into a string.
*
* @param value a header value
* @returns a merged header value (a string)
*/
private merge(value: string | string[]): string {
if (Array.isArray(value)) return value.join(', ')
return value
}
/**
* Creates a Headers instance from a plain object or a Headers instance.
*
* @param headers a plain object or a Headers instance
* @returns a headers instance
*/
public static from(headers: IncomingHttpHeaders | Headers): Headers {
if (headers instanceof Headers) return headers
return new HeadersAdapter(headers)
}
public append(name: string, value: string): void {
const existing = this.headers[name]
if (typeof existing === 'string') {
this.headers[name] = [existing, value]
} else if (Array.isArray(existing)) {
existing.push(value)
} else {
this.headers[name] = value
}
}
public delete(name: string): void {
delete this.headers[name]
}
public get(name: string): string | null {
const value = this.headers[name]
if (typeof value !== 'undefined') return this.merge(value)
return null
}
public has(name: string): boolean {
return typeof this.headers[name] !== 'undefined'
}
public set(name: string, value: string): void {
this.headers[name] = value
}
public forEach(
callbackfn: (value: string, name: string, parent: Headers) => void,
thisArg?: any
): void {
for (const [name, value] of this.entries()) {
callbackfn.call(thisArg, value, name, this)
}
}
public *entries(): IterableIterator<[string, string]> {
for (const key of Object.keys(this.headers)) {
const name = key.toLowerCase()
// We assert here that this is a string because we got it from the
// Object.keys() call above.
const value = this.get(name) as string
yield [name, value] as [string, string]
}
}
public *keys(): IterableIterator<string> {
for (const key of Object.keys(this.headers)) {
const name = key.toLowerCase()
yield name
}
}
public *values(): IterableIterator<string> {
for (const key of Object.keys(this.headers)) {
// We assert here that this is a string because we got it from the
// Object.keys() call above.
const value = this.get(key) as string
yield value
}
}
public [Symbol.iterator](): IterableIterator<[string, string]> {
return this.entries()
}
}