diff --git a/modules/@angular/http/src/headers.ts b/modules/@angular/http/src/headers.ts index 9eb16ac31a..b656f315a9 100644 --- a/modules/@angular/http/src/headers.ts +++ b/modules/@angular/http/src/headers.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ListWrapper, MapWrapper, StringMapWrapper, isListLikeIterable, iterateListLike} from '../src/facade/collection'; +import {MapWrapper} from '../src/facade/collection'; /** * Polyfill for [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers/Headers), as @@ -15,7 +15,7 @@ import {ListWrapper, MapWrapper, StringMapWrapper, isListLikeIterable, iterateLi * The only known difference between this `Headers` implementation and the spec is the * lack of an `entries` method. * - * ### Example ([live demo](http://plnkr.co/edit/MTdwT6?p=preview)) + * ### Example * * ``` * import {Headers} from '@angular/http'; @@ -37,23 +37,31 @@ import {ListWrapper, MapWrapper, StringMapWrapper, isListLikeIterable, iterateLi * @experimental */ export class Headers { - /** @internal */ - _headersMap: Map; - constructor(headers?: Headers|{[key: string]: any}) { - if (headers instanceof Headers) { - this._headersMap = new Map((headers)._headersMap); - return; - } - - this._headersMap = new Map(); + /** @internal header names are lower case */ + _headers: Map = new Map(); + /** @internal map lower case names to actual names */ + _normalizedNames: Map = new Map(); + // TODO(vicb): any -> string|string[] + constructor(headers?: Headers|{[name: string]: any}) { if (!headers) { return; } - // headers instanceof StringMap - StringMapWrapper.forEach(headers, (v: any, k: string) => { - this._headersMap.set(normalize(k), isListLikeIterable(v) ? v : [v]); + if (headers instanceof Headers) { + headers._headers.forEach((value: string[], name: string) => { + const lcName = name.toLowerCase(); + this._headers.set(lcName, value); + this.mayBeSetNormalizedName(name); + }); + return; + } + + Object.keys(headers).forEach((name: string) => { + const value = headers[name]; + const lcName = name.toLowerCase(); + this._headers.set(lcName, Array.isArray(value) ? value : [value]); + this.mayBeSetNormalizedName(name); }); } @@ -61,14 +69,14 @@ export class Headers { * Returns a new Headers instance from the given DOMString of Response Headers */ static fromResponseHeaderString(headersString: string): Headers { - let headers = new Headers(); + const headers = new Headers(); headersString.split('\n').forEach(line => { const index = line.indexOf(':'); if (index > 0) { - const key = line.substring(0, index); - const value = line.substring(index + 1).trim(); - headers.set(key, value); + const name = line.slice(0, index); + const value = line.slice(index + 1).trim(); + headers.set(name, value); } }); @@ -79,92 +87,94 @@ export class Headers { * Appends a header to existing list of header values for a given header name. */ append(name: string, value: string): void { - name = normalize(name); - var mapName = this._headersMap.get(name); - var list = isListLikeIterable(mapName) ? mapName : []; - list.push(value); - this._headersMap.set(name, list); + const values = this.getAll(name); + this.set(name, values === null ? [value] : [...values, value]); } /** * Deletes all header values for the given name. */ - delete (name: string): void { this._headersMap.delete(normalize(name)); } + delete (name: string): void { + const lcName = name.toLowerCase(); + this._normalizedNames.delete(lcName); + this._headers.delete(lcName); + } forEach(fn: (values: string[], name: string, headers: Map) => void): void { - this._headersMap.forEach(fn); + this._headers.forEach( + (values, lcName) => fn(values, this._normalizedNames.get(lcName), this._headers)); } /** * Returns first header that matches given name. */ - get(header: string): string { return ListWrapper.first(this._headersMap.get(normalize(header))); } + get(name: string): string { + const values = this.getAll(name); + + if (values === null) { + return null; + } + + return values.length > 0 ? values[0] : null; + } /** - * Check for existence of header by given name. + * Checks for existence of header by given name. */ - has(header: string): boolean { return this._headersMap.has(normalize(header)); } + has(name: string): boolean { return this._headers.has(name.toLowerCase()); } /** - * Provides names of set headers + * Returns the names of the headers */ - keys(): string[] { return MapWrapper.keys(this._headersMap); } + keys(): string[] { return MapWrapper.values(this._normalizedNames); } /** * Sets or overrides header value for given name. */ - set(header: string, value: string|string[]): void { - var list: string[] = []; - - if (isListLikeIterable(value)) { - var pushValue = (value).join(','); - list.push(pushValue); - } else { - list.push(value); - } - - this._headersMap.set(normalize(header), list); + set(name: string, value: string|string[]): void { + const strValue = Array.isArray(value) ? value.join(',') : value; + this._headers.set(name.toLowerCase(), [strValue]); + this.mayBeSetNormalizedName(name); } /** * Returns values of all headers. */ - values(): string[][] { return MapWrapper.values(this._headersMap); } + values(): string[][] { return MapWrapper.values(this._headers); } /** * Returns string of all headers. */ - toJSON(): {[key: string]: any} { - let serializableHeaders = {}; - this._headersMap.forEach((values: string[], name: string) => { - let list: any[] /** TODO #9100 */ = []; + // TODO(vicb): returns {[name: string]: string[]} + toJSON(): {[name: string]: any} { + const serialized: {[name: string]: string[]} = {}; - iterateListLike( - values, (val: any /** TODO #9100 */) => list = ListWrapper.concat(list, val.split(','))); - - (serializableHeaders as any /** TODO #9100 */)[normalize(name)] = list; + this._headers.forEach((values: string[], name: string) => { + const split: string[] = []; + values.forEach(v => split.push(...v.split(','))); + serialized[this._normalizedNames.get(name)] = split; }); - return serializableHeaders; + + return serialized; } /** * Returns list of header values for a given name. */ - getAll(header: string): string[] { - var headers = this._headersMap.get(normalize(header)); - return isListLikeIterable(headers) ? headers : []; + getAll(name: string): string[] { + return this.has(name) ? this._headers.get(name.toLowerCase()) : null; } /** * This method is not implemented. */ entries() { throw new Error('"entries" method is not implemented on Headers class'); } -} -// "HTTP character sets are identified by case-insensitive tokens" -// Spec at https://tools.ietf.org/html/rfc2616 -// This implementation is same as NodeJS. -// see https://nodejs.org/dist/latest-v6.x/docs/api/http.html#http_message_headers -function normalize(name: string): string { - return name.toLowerCase(); + private mayBeSetNormalizedName(name: string): void { + const lcName = name.toLowerCase(); + + if (!this._normalizedNames.has(lcName)) { + this._normalizedNames.set(lcName, name); + } + } } diff --git a/modules/@angular/http/test/headers_spec.ts b/modules/@angular/http/test/headers_spec.ts index c31c9111f4..3744f6c10c 100644 --- a/modules/@angular/http/test/headers_spec.ts +++ b/modules/@angular/http/test/headers_spec.ts @@ -6,46 +6,36 @@ * found in the LICENSE file at https://angular.io/license */ -import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal'; -import {Json} from '../src/facade/lang'; import {Headers} from '../src/headers'; export function main() { describe('Headers', () => { - it('should conform to spec', () => { - // Examples borrowed from https://developer.mozilla.org/en-US/docs/Web/API/Headers/Headers - // Spec at https://fetch.spec.whatwg.org/#dom-headers - var firstHeaders = new Headers(); // Currently empty - firstHeaders.append('Content-Type', 'image/jpeg'); - expect(firstHeaders.get('Content-Type')).toBe('image/jpeg'); - // "HTTP character sets are identified by case-insensitive tokens" - // Spec at https://tools.ietf.org/html/rfc2616 - expect(firstHeaders.get('content-type')).toBe('image/jpeg'); - expect(firstHeaders.get('content-Type')).toBe('image/jpeg'); - const httpHeaders = { - 'Content-Type': 'image/jpeg', - 'Accept-Charset': 'utf-8', - 'X-My-Custom-Header': 'Zeke are cool', - }; - const secondHeaders = new Headers(httpHeaders); - const secondHeadersObj = new Headers(secondHeaders); - expect(secondHeadersObj.get('Content-Type')).toBe('image/jpeg'); - }); - describe('initialization', () => { + it('should conform to spec', () => { + const httpHeaders = { + 'Content-Type': 'image/jpeg', + 'Accept-Charset': 'utf-8', + 'X-My-Custom-Header': 'Zeke are cool', + }; + const secondHeaders = new Headers(httpHeaders); + const secondHeadersObj = new Headers(secondHeaders); + expect(secondHeadersObj.get('Content-Type')).toEqual('image/jpeg'); + }); + it('should merge values in provided dictionary', () => { const headers = new Headers({'foo': 'bar'}); - expect(headers.get('foo')).toBe('bar'); + expect(headers.get('foo')).toEqual('bar'); expect(headers.getAll('foo')).toEqual(['bar']); }); + it('should not alter the values of a provided header template', () => { // Spec at https://fetch.spec.whatwg.org/#concept-headers-fill // test for https://github.com/angular/angular/issues/6845 const firstHeaders = new Headers(); const secondHeaders = new Headers(firstHeaders); secondHeaders.append('Content-Type', 'image/jpeg'); - expect(firstHeaders.has('Content-Type')).toBeFalsy(); + expect(firstHeaders.has('Content-Type')).toEqual(false); }); }); @@ -53,56 +43,104 @@ export function main() { describe('.set()', () => { it('should clear all values and re-set for the provided key', () => { const headers = new Headers({'foo': 'bar'}); - expect(headers.get('foo')).toBe('bar'); - expect(headers.getAll('foo')).toEqual(['bar']); + expect(headers.get('foo')).toEqual('bar'); + headers.set('foo', 'baz'); - expect(headers.get('foo')).toBe('baz'); - expect(headers.getAll('foo')).toEqual(['baz']); + expect(headers.get('foo')).toEqual('baz'); + + headers.set('fOO', 'bat'); + expect(headers.get('foo')).toEqual('bat'); }); + it('should preserve the case of the first call', () => { + const headers = new Headers(); + headers.set('fOo', 'baz'); + headers.set('foo', 'bat'); + expect(JSON.stringify(headers)).toEqual('{"fOo":["bat"]}'); + }); it('should convert input array to string', () => { - var headers = new Headers(); - var inputArr = ['bar', 'baz']; - headers.set('foo', inputArr); - expect(/bar, ?baz/g.test(headers.get('foo'))).toBe(true); - expect(/bar, ?baz/g.test(headers.getAll('foo')[0])).toBe(true); + const headers = new Headers(); + headers.set('foo', ['bar', 'baz']); + expect(headers.get('foo')).toEqual('bar,baz'); + expect(headers.getAll('foo')).toEqual(['bar,baz']); }); }); + describe('.get()', () => { + it('should be case insensitive', () => { + const headers = new Headers(); + headers.set('foo', 'baz'); + expect(headers.get('foo')).toEqual('baz'); + expect(headers.get('FOO')).toEqual('baz'); + }); + + it('should return null if the header is not present', () => { + const headers = new Headers({bar: []}); + expect(headers.get('bar')).toEqual(null); + expect(headers.get('foo')).toEqual(null); + }); + }); + + describe('.getAll()', () => { + it('should be case insensitive', () => { + const headers = new Headers({foo: ['bar', 'baz']}); + expect(headers.getAll('foo')).toEqual(['bar', 'baz']); + expect(headers.getAll('FOO')).toEqual(['bar', 'baz']); + }); + + it('should return null if the header is not present', () => { + const headers = new Headers(); + expect(headers.getAll('foo')).toEqual(null); + }); + }); + + describe('.delete', () => { + it('should be case insensitive', () => { + const headers = new Headers(); + + headers.set('foo', 'baz'); + expect(headers.has('foo')).toEqual(true); + headers.delete('foo'); + expect(headers.has('foo')).toEqual(false); + + headers.set('foo', 'baz'); + expect(headers.has('foo')).toEqual(true); + headers.delete('FOO'); + expect(headers.has('foo')).toEqual(false); + }); + }); + + describe('.append', () => { + it('should preserve the case of the first call', () => { + const headers = new Headers(); + + headers.append('FOO', 'bar'); + headers.append('foo', 'baz'); + expect(JSON.stringify(headers)).toEqual('{"FOO":["bar","baz"]}'); + }); + }); describe('.toJSON()', () => { - let headers: any /** TODO #9100 */ = null; - let inputArr: any /** TODO #9100 */ = null; - let obj: any /** TODO #9100 */ = null; + let headers: Headers; + let values: string[]; + let ref: {[name: string]: string[]}; beforeEach(() => { + values = ['application/jeisen', 'application/jason', 'application/patrickjs']; headers = new Headers(); - inputArr = ['application/jeisen', 'application/jason', 'application/patrickjs']; - obj = {'accept': inputArr}; - headers.set('Accept', inputArr); + headers.set('Accept', values); + ref = {'Accept': values}; }); - it('should be serializable with toJSON', () => { - let stringifed = Json.stringify(obj); - let serializedHeaders = Json.stringify(headers); - expect(serializedHeaders).toEqual(stringifed); - }); - - - it('should be able to parse serialized header', () => { - let stringifed = Json.stringify(obj); - let serializedHeaders = Json.stringify(headers); - expect(Json.parse(serializedHeaders)).toEqual(Json.parse(stringifed)); - }); - + it('should be serializable with toJSON', + () => { expect(JSON.stringify(headers)).toEqual(JSON.stringify(ref)); }); it('should be able to recreate serializedHeaders', () => { - let serializedHeaders = Json.stringify(headers); - let parsedHeaders = Json.parse(serializedHeaders); - let recreatedHeaders = new Headers(parsedHeaders); - expect(Json.stringify(parsedHeaders)).toEqual(Json.stringify(recreatedHeaders)); + const parsedHeaders = JSON.parse(JSON.stringify(headers)); + const recreatedHeaders = new Headers(parsedHeaders); + expect(JSON.stringify(parsedHeaders)).toEqual(JSON.stringify(recreatedHeaders)); }); }); }); @@ -110,19 +148,15 @@ export function main() { describe('.fromResponseHeaderString()', () => { it('should parse a response header string', () => { - - let responseHeaderString = `Date: Fri, 20 Nov 2015 01:45:26 GMT -Content-Type: application/json; charset=utf-8 -Transfer-Encoding: chunked -Connection: keep-alive`; - - let responseHeaders = Headers.fromResponseHeaderString(responseHeaderString); - - expect(responseHeaders.get('Date')).toEqual('Fri, 20 Nov 2015 01:45:26 GMT'); - expect(responseHeaders.get('Content-Type')).toEqual('application/json; charset=utf-8'); - expect(responseHeaders.get('Transfer-Encoding')).toEqual('chunked'); - expect(responseHeaders.get('Connection')).toEqual('keep-alive'); - + const response = `Date: Fri, 20 Nov 2015 01:45:26 GMT\n` + + `Content-Type: application/json; charset=utf-8\n` + + `Transfer-Encoding: chunked\n` + + `Connection: keep-alive`; + const headers = Headers.fromResponseHeaderString(response); + expect(headers.get('Date')).toEqual('Fri, 20 Nov 2015 01:45:26 GMT'); + expect(headers.get('Content-Type')).toEqual('application/json; charset=utf-8'); + expect(headers.get('Transfer-Encoding')).toEqual('chunked'); + expect(headers.get('Connection')).toEqual('keep-alive'); }); }); } diff --git a/tools/public_api_guard/http/index.d.ts b/tools/public_api_guard/http/index.d.ts index bc45296ac1..568a34d9c8 100644 --- a/tools/public_api_guard/http/index.d.ts +++ b/tools/public_api_guard/http/index.d.ts @@ -35,19 +35,19 @@ export declare class CookieXSRFStrategy implements XSRFStrategy { /** @experimental */ export declare class Headers { constructor(headers?: Headers | { - [key: string]: any; + [name: string]: any; }); append(name: string, value: string): void; delete(name: string): void; entries(): void; forEach(fn: (values: string[], name: string, headers: Map) => void): void; - get(header: string): string; - getAll(header: string): string[]; - has(header: string): boolean; + get(name: string): string; + getAll(name: string): string[]; + has(name: string): boolean; keys(): string[]; - set(header: string, value: string | string[]): void; + set(name: string, value: string | string[]): void; toJSON(): { - [key: string]: any; + [name: string]: any; }; values(): string[][]; static fromResponseHeaderString(headersString: string): Headers;