/
headers.ts
288 lines (263 loc) Β· 8.41 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
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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
interface Update {
name: string;
value?: string | string[];
op: 'a' | 's' | 'd';
}
/**
* Represents the header configuration options for an HTTP request.
* Instances are immutable. Modifying methods return a cloned
* instance with the change. The original object is never changed.
*
* @publicApi
*/
export class HttpHeaders {
/**
* Internal map of lowercase header names to values.
*/
// TODO(issue/24571): remove '!'.
private headers!: Map<string, string[]>;
/**
* Internal map of lowercased header names to the normalized
* form of the name (the form seen first).
*/
private normalizedNames: Map<string, string> = new Map();
/**
* Complete the lazy initialization of this object (needed before reading).
*/
private lazyInit!: HttpHeaders | Function | null;
/**
* Queued updates to be materialized the next initialization.
*/
private lazyUpdate: Update[] | null = null;
/** Constructs a new HTTP header object with the given values.*/
constructor(
headers?: string | {[name: string]: string | number | (string | number)[]} | Headers,
) {
if (!headers) {
this.headers = new Map<string, string[]>();
} else if (typeof headers === 'string') {
this.lazyInit = () => {
this.headers = new Map<string, string[]>();
headers.split('\n').forEach((line) => {
const index = line.indexOf(':');
if (index > 0) {
const name = line.slice(0, index);
const key = name.toLowerCase();
const value = line.slice(index + 1).trim();
this.maybeSetNormalizedName(name, key);
if (this.headers.has(key)) {
this.headers.get(key)!.push(value);
} else {
this.headers.set(key, [value]);
}
}
});
};
} else if (typeof Headers !== 'undefined' && headers instanceof Headers) {
this.headers = new Map<string, string[]>();
headers.forEach((values: string, name: string) => {
this.setHeaderEntries(name, values);
});
} else {
this.lazyInit = () => {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
assertValidHeaders(headers);
}
this.headers = new Map<string, string[]>();
Object.entries(headers).forEach(([name, values]) => {
this.setHeaderEntries(name, values);
});
};
}
}
/**
* Checks for existence of a given header.
*
* @param name The header name to check for existence.
*
* @returns True if the header exists, false otherwise.
*/
has(name: string): boolean {
this.init();
return this.headers.has(name.toLowerCase());
}
/**
* Retrieves the first value of a given header.
*
* @param name The header name.
*
* @returns The value string if the header exists, null otherwise
*/
get(name: string): string | null {
this.init();
const values = this.headers.get(name.toLowerCase());
return values && values.length > 0 ? values[0] : null;
}
/**
* Retrieves the names of the headers.
*
* @returns A list of header names.
*/
keys(): string[] {
this.init();
return Array.from(this.normalizedNames.values());
}
/**
* Retrieves a list of values for a given header.
*
* @param name The header name from which to retrieve values.
*
* @returns A string of values if the header exists, null otherwise.
*/
getAll(name: string): string[] | null {
this.init();
return this.headers.get(name.toLowerCase()) || null;
}
/**
* Appends a new value to the existing set of values for a header
* and returns them in a clone of the original instance.
*
* @param name The header name for which to append the values.
* @param value The value to append.
*
* @returns A clone of the HTTP headers object with the value appended to the given header.
*/
append(name: string, value: string | string[]): HttpHeaders {
return this.clone({name, value, op: 'a'});
}
/**
* Sets or modifies a value for a given header in a clone of the original instance.
* If the header already exists, its value is replaced with the given value
* in the returned object.
*
* @param name The header name.
* @param value The value or values to set or override for the given header.
*
* @returns A clone of the HTTP headers object with the newly set header value.
*/
set(name: string, value: string | string[]): HttpHeaders {
return this.clone({name, value, op: 's'});
}
/**
* Deletes values for a given header in a clone of the original instance.
*
* @param name The header name.
* @param value The value or values to delete for the given header.
*
* @returns A clone of the HTTP headers object with the given value deleted.
*/
delete(name: string, value?: string | string[]): HttpHeaders {
return this.clone({name, value, op: 'd'});
}
private maybeSetNormalizedName(name: string, lcName: string): void {
if (!this.normalizedNames.has(lcName)) {
this.normalizedNames.set(lcName, name);
}
}
private init(): void {
if (!!this.lazyInit) {
if (this.lazyInit instanceof HttpHeaders) {
this.copyFrom(this.lazyInit);
} else {
this.lazyInit();
}
this.lazyInit = null;
if (!!this.lazyUpdate) {
this.lazyUpdate.forEach((update) => this.applyUpdate(update));
this.lazyUpdate = null;
}
}
}
private copyFrom(other: HttpHeaders) {
other.init();
Array.from(other.headers.keys()).forEach((key) => {
this.headers.set(key, other.headers.get(key)!);
this.normalizedNames.set(key, other.normalizedNames.get(key)!);
});
}
private clone(update: Update): HttpHeaders {
const clone = new HttpHeaders();
clone.lazyInit = !!this.lazyInit && this.lazyInit instanceof HttpHeaders ? this.lazyInit : this;
clone.lazyUpdate = (this.lazyUpdate || []).concat([update]);
return clone;
}
private applyUpdate(update: Update): void {
const key = update.name.toLowerCase();
switch (update.op) {
case 'a':
case 's':
let value = update.value!;
if (typeof value === 'string') {
value = [value];
}
if (value.length === 0) {
return;
}
this.maybeSetNormalizedName(update.name, key);
const base = (update.op === 'a' ? this.headers.get(key) : undefined) || [];
base.push(...value);
this.headers.set(key, base);
break;
case 'd':
const toDelete = update.value as string | undefined;
if (!toDelete) {
this.headers.delete(key);
this.normalizedNames.delete(key);
} else {
let existing = this.headers.get(key);
if (!existing) {
return;
}
existing = existing.filter((value) => toDelete.indexOf(value) === -1);
if (existing.length === 0) {
this.headers.delete(key);
this.normalizedNames.delete(key);
} else {
this.headers.set(key, existing);
}
}
break;
}
}
private setHeaderEntries(name: string, values: any) {
const headerValues = (Array.isArray(values) ? values : [values]).map((value) =>
value.toString(),
);
const key = name.toLowerCase();
this.headers.set(key, headerValues);
this.maybeSetNormalizedName(name, key);
}
/**
* @internal
*/
forEach(fn: (name: string, values: string[]) => void) {
this.init();
Array.from(this.normalizedNames.keys()).forEach((key) =>
fn(this.normalizedNames.get(key)!, this.headers.get(key)!),
);
}
}
/**
* Verifies that the headers object has the right shape: the values
* must be either strings, numbers or arrays. Throws an error if an invalid
* header value is present.
*/
function assertValidHeaders(
headers: Record<string, unknown> | Headers,
): asserts headers is Record<string, string | string[] | number | number[]> {
for (const [key, value] of Object.entries(headers)) {
if (!(typeof value === 'string' || typeof value === 'number') && !Array.isArray(value)) {
throw new Error(
`Unexpected value of the \`${key}\` header provided. ` +
`Expecting either a string, a number or an array, but got: \`${value}\`.`,
);
}
}
}