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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import { DomIterableMixin } from "./dom_iterable.ts";
import { requiredArguments } from "./util.ts";
import { customInspect } from "./console.ts";
// From node-fetch
// Copyright (c) 2016 David Frank. MIT License.
const invalidTokenRegex = /[^\^_`a-zA-Z\-0-9!#$%&'*+.|~]/;
const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isHeaders(value: any): value is Headers {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return value instanceof Headers;
}
const headersData = Symbol("headers data");
// TODO: headerGuard? Investigate if it is needed
// node-fetch did not implement this but it is in the spec
function normalizeParams(name: string, value?: string): string[] {
name = String(name).toLowerCase();
value = String(value).trim();
return [name, value];
}
// The following name/value validations are copied from
// https://github.com/bitinn/node-fetch/blob/master/src/headers.js
// Copyright (c) 2016 David Frank. MIT License.
function validateName(name: string): void {
if (invalidTokenRegex.test(name) || name === "") {
throw new TypeError(`${name} is not a legal HTTP header name`);
}
}
function validateValue(value: string): void {
if (invalidHeaderCharRegex.test(value)) {
throw new TypeError(`${value} is not a legal HTTP header value`);
}
}
/** Appends a key and value to the header list.
*
* The spec indicates that when a key already exists, the append adds the new
* value onto the end of the existing value. The behaviour of this though
* varies when the key is `set-cookie`. In this case, if the key of the cookie
* already exists, the value is replaced, but if the key of the cookie does not
* exist, and additional `set-cookie` header is added.
*
* The browser specification of `Headers` is written for clients, and not
* servers, and Deno is a server, meaning that it needs to follow the patterns
* expected for servers, of which a `set-cookie` header is expected for each
* unique cookie key, but duplicate cookie keys should not exist. */
function dataAppend(
data: Array<[string, string]>,
key: string,
value: string
): void {
for (let i = 0; i < data.length; i++) {
const [dataKey] = data[i];
if (key === "set-cookie" && dataKey === "set-cookie") {
const [, dataValue] = data[i];
const [dataCookieKey] = dataValue.split("=");
const [cookieKey] = value.split("=");
if (dataCookieKey === cookieKey) {
data[i][1] = value;
return;
}
} else {
if (dataKey === key) {
data[i][1] += `, ${value}`;
return;
}
}
}
data.push([key, value]);
}
/** Gets a value of a key in the headers list.
*
* This varies slightly from spec behaviour in that when the key is `set-cookie`
* the value returned will look like a concatenated value, when in fact, if the
* headers were iterated over, each individual `set-cookie` value is a unique
* entry in the headers list. */
function dataGet(
data: Array<[string, string]>,
key: string
): string | undefined {
const setCookieValues = [];
for (const [dataKey, value] of data) {
if (dataKey === key) {
if (key === "set-cookie") {
setCookieValues.push(value);
} else {
return value;
}
}
}
if (setCookieValues.length) {
return setCookieValues.join(", ");
}
return undefined;
}
/** Sets a value of a key in the headers list.
*
* The spec indicates that the value should be replaced if the key already
* exists. The behaviour here varies, where if the key is `set-cookie` the key
* of the cookie is inspected, and if the key of the cookie already exists,
* then the value is replaced. If the key of the cookie is not found, then
* the value of the `set-cookie` is added to the list of headers.
*
* The browser specification of `Headers` is written for clients, and not
* servers, and Deno is a server, meaning that it needs to follow the patterns
* expected for servers, of which a `set-cookie` header is expected for each
* unique cookie key, but duplicate cookie keys should not exist. */
function dataSet(
data: Array<[string, string]>,
key: string,
value: string
): void {
for (let i = 0; i < data.length; i++) {
const [dataKey] = data[i];
if (dataKey === key) {
// there could be multiple set-cookie headers, but all others are unique
if (key === "set-cookie") {
const [, dataValue] = data[i];
const [dataCookieKey] = dataValue.split("=");
const [cookieKey] = value.split("=");
if (cookieKey === dataCookieKey) {
data[i][1] = value;
return;
}
} else {
data[i][1] = value;
return;
}
}
}
data.push([key, value]);
}
function dataDelete(data: Array<[string, string]>, key: string): void {
let i = 0;
while (i < data.length) {
const [dataKey] = data[i];
if (dataKey === key) {
data.splice(i, 1);
} else {
i++;
}
}
}
function dataHas(data: Array<[string, string]>, key: string): boolean {
for (const [dataKey] of data) {
if (dataKey === key) {
return true;
}
}
return false;
}
// ref: https://fetch.spec.whatwg.org/#dom-headers
class HeadersBase {
[headersData]: Array<[string, string]>;
constructor(init?: HeadersInit) {
if (init === null) {
throw new TypeError(
"Failed to construct 'Headers'; The provided value was not valid"
);
} else if (isHeaders(init)) {
this[headersData] = [...init];
} else {
this[headersData] = [];
if (Array.isArray(init)) {
for (const tuple of init) {
// If header does not contain exactly two items,
// then throw a TypeError.
// ref: https://fetch.spec.whatwg.org/#concept-headers-fill
requiredArguments(
"Headers.constructor tuple array argument",
tuple.length,
2
);
this.append(tuple[0], tuple[1]);
}
} else if (init) {
for (const [rawName, rawValue] of Object.entries(init)) {
this.append(rawName, rawValue);
}
}
}
}
[customInspect](): string {
let length = this[headersData].length;
let output = "";
for (const [key, value] of this[headersData]) {
const prefix = length === this[headersData].length ? " " : "";
const postfix = length === 1 ? " " : ", ";
output = output + `${prefix}${key}: ${value}${postfix}`;
length--;
}
return `Headers {${output}}`;
}
// ref: https://fetch.spec.whatwg.org/#concept-headers-append
append(name: string, value: string): void {
requiredArguments("Headers.append", arguments.length, 2);
const [newname, newvalue] = normalizeParams(name, value);
validateName(newname);
validateValue(newvalue);
dataAppend(this[headersData], newname, newvalue);
}
delete(name: string): void {
requiredArguments("Headers.delete", arguments.length, 1);
const [newname] = normalizeParams(name);
validateName(newname);
dataDelete(this[headersData], newname);
}
get(name: string): string | null {
requiredArguments("Headers.get", arguments.length, 1);
const [newname] = normalizeParams(name);
validateName(newname);
return dataGet(this[headersData], newname) ?? null;
}
has(name: string): boolean {
requiredArguments("Headers.has", arguments.length, 1);
const [newname] = normalizeParams(name);
validateName(newname);
return dataHas(this[headersData], newname);
}
set(name: string, value: string): void {
requiredArguments("Headers.set", arguments.length, 2);
const [newname, newvalue] = normalizeParams(name, value);
validateName(newname);
validateValue(newvalue);
dataSet(this[headersData], newname, newvalue);
}
get [Symbol.toStringTag](): string {
return "Headers";
}
}
// @internal
export class HeadersImpl extends DomIterableMixin<
string,
string,
typeof HeadersBase
>(HeadersBase, headersData) {}
Object.defineProperty(HeadersImpl, "name", {
value: "Headers",
configurable: true,
});
|