/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/common/http/src/headers.ts
294 строки
9 KB
SkyZeroZx
fix(http): match header values exactly when deleting
29 июл 2026, 18:39
29 июл 2026, 18:39
f33ee95
Код
Авторство
О чём код?
/** * @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.dev/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. * * @see [Setting request headers](guide/http/making-requests#setting-request-headers) * * @publicApi */ export class HttpHeaders { /** * Internal map of lowercase header names to values. */ 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 value = line.slice(index + 1).trim(); this.addHeaderEntry(name, value); } }); }; } else if (typeof Headers !== 'undefined' && headers instanceof Headers) { this.headers = new Map<string, string[]>(); headers.forEach((value: string, name: string) => { this.addHeaderEntry(name, value); }); } 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(); for (const [key, values] of other.headers.entries()) { this.headers.set(key, values); 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) || []).slice() : []; base.push(...value); this.headers.set(key, base); break; case 'd': const toDelete = update.value; if (toDelete === undefined) { this.headers.delete(key); this.normalizedNames.delete(key); } else { const valuesToDelete = Array.isArray(toDelete) ? toDelete : [toDelete]; let existing = this.headers.get(key); if (!existing) { return; } existing = existing.filter((value) => valuesToDelete.indexOf(value) === -1); if (existing.length === 0) { this.headers.delete(key); this.normalizedNames.delete(key); } else { this.headers.set(key, existing); } } break; } } private addHeaderEntry(name: string, value: string) { const key = name.toLowerCase(); this.maybeSetNormalizedName(name, key); if (this.headers.has(key)) { this.headers.get(key)!.push(value); } else { this.headers.set(key, [value]); } } 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}\`.`, ); } } }