diff --git a/modules/@angular/http/src/backends/xhr_backend.ts b/modules/@angular/http/src/backends/xhr_backend.ts index 27882598f0..0fde263c85 100644 --- a/modules/@angular/http/src/backends/xhr_backend.ts +++ b/modules/@angular/http/src/backends/xhr_backend.ts @@ -152,18 +152,18 @@ export class XHRConnection implements Connection { case ContentType.NONE: break; case ContentType.JSON: - _xhr.setRequestHeader('Content-Type', 'application/json'); + _xhr.setRequestHeader('content-type', 'application/json'); break; case ContentType.FORM: - _xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8'); + _xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded;charset=UTF-8'); break; case ContentType.TEXT: - _xhr.setRequestHeader('Content-Type', 'text/plain'); + _xhr.setRequestHeader('content-type', 'text/plain'); break; case ContentType.BLOB: var blob = req.blob(); if (blob.type) { - _xhr.setRequestHeader('Content-Type', blob.type); + _xhr.setRequestHeader('content-type', blob.type); } break; } diff --git a/modules/@angular/http/src/headers.ts b/modules/@angular/http/src/headers.ts index efe752851a..dc49ddc644 100644 --- a/modules/@angular/http/src/headers.ts +++ b/modules/@angular/http/src/headers.ts @@ -56,7 +56,7 @@ export class Headers { // headers instanceof StringMap StringMapWrapper.forEach(headers, (v: any, k: string) => { - this._headersMap.set(k, isListLikeIterable(v) ? v : [v]); + this._headersMap.set(normalize(k), isListLikeIterable(v) ? v : [v]); }); } @@ -68,13 +68,16 @@ export class Headers { .split('\n') .map(val => val.split(':')) .map(([key, ...parts]) => ([key.trim(), parts.join(':').trim()])) - .reduce((headers, [key, value]) => !headers.set(key, value) && headers, new Headers()); + .reduce( + (headers, [key, value]) => !headers.set(normalize(key), value) && headers, + new 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); @@ -84,7 +87,7 @@ export class Headers { /** * Deletes all header values for the given name. */ - delete (name: string): void { this._headersMap.delete(name); } + delete (name: string): void { this._headersMap.delete(normalize(name)); } forEach(fn: (values: string[], name: string, headers: Map) => void): void { this._headersMap.forEach(fn); @@ -93,12 +96,12 @@ export class Headers { /** * Returns first header that matches given name. */ - get(header: string): string { return ListWrapper.first(this._headersMap.get(header)); } + get(header: string): string { return ListWrapper.first(this._headersMap.get(normalize(header))); } /** * Check for existence of header by given name. */ - has(header: string): boolean { return this._headersMap.has(header); } + has(header: string): boolean { return this._headersMap.has(normalize(header)); } /** * Provides names of set headers @@ -118,7 +121,7 @@ export class Headers { list.push(value); } - this._headersMap.set(header, list); + this._headersMap.set(normalize(header), list); } /** @@ -137,7 +140,7 @@ export class Headers { iterateListLike( values, (val: any /** TODO #9100 */) => list = ListWrapper.concat(list, val.split(','))); - (serializableHeaders as any /** TODO #9100 */)[name] = list; + (serializableHeaders as any /** TODO #9100 */)[normalize(name)] = list; }); return serializableHeaders; } @@ -146,7 +149,7 @@ export class Headers { * Returns list of header values for a given name. */ getAll(header: string): string[] { - var headers = this._headersMap.get(header); + var headers = this._headersMap.get(normalize(header)); return isListLikeIterable(headers) ? headers : []; } @@ -155,3 +158,11 @@ export class Headers { */ entries() { throw new BaseException('"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(); +} diff --git a/modules/@angular/http/test/backends/xhr_backend_spec.ts b/modules/@angular/http/test/backends/xhr_backend_spec.ts index 4bd5394240..0d214e83bf 100644 --- a/modules/@angular/http/test/backends/xhr_backend_spec.ts +++ b/modules/@angular/http/test/backends/xhr_backend_spec.ts @@ -236,9 +236,9 @@ export function main() { var connection = new XHRConnection( new Request(base.merge(new RequestOptions({headers: headers}))), new MockBrowserXHR()); connection.response.subscribe(); - expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'text/xml'); - expect(setRequestHeaderSpy).toHaveBeenCalledWith('Breaking-Bad', '<3'); - expect(setRequestHeaderSpy).toHaveBeenCalledWith('X-Multi', 'a,b'); + expect(setRequestHeaderSpy).toHaveBeenCalledWith('content-type', 'text/xml'); + expect(setRequestHeaderSpy).toHaveBeenCalledWith('breaking-bad', '<3'); + expect(setRequestHeaderSpy).toHaveBeenCalledWith('x-multi', 'a,b'); }); it('should skip content type detection if custom content type header is set', () => { @@ -249,8 +249,8 @@ export function main() { new Request(base.merge(new RequestOptions({body: body, headers: headers}))), new MockBrowserXHR()); connection.response.subscribe(); - expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'text/plain'); - expect(setRequestHeaderSpy).not.toHaveBeenCalledWith('Content-Type', 'application/json'); + expect(setRequestHeaderSpy).toHaveBeenCalledWith('content-type', 'text/plain'); + expect(setRequestHeaderSpy).not.toHaveBeenCalledWith('content-type', 'application/json'); }); it('should use object body and detect content type header to the request', () => { @@ -260,7 +260,7 @@ export function main() { new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR()); connection.response.subscribe(); expect(sendSpy).toHaveBeenCalledWith(Json.stringify(body)); - expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'application/json'); + expect(setRequestHeaderSpy).toHaveBeenCalledWith('content-type', 'application/json'); }); it('should use number body and detect content type header to the request', () => { @@ -270,7 +270,7 @@ export function main() { new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR()); connection.response.subscribe(); expect(sendSpy).toHaveBeenCalledWith('23'); - expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'text/plain'); + expect(setRequestHeaderSpy).toHaveBeenCalledWith('content-type', 'text/plain'); }); it('should use string body and detect content type header to the request', () => { @@ -280,7 +280,7 @@ export function main() { new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR()); connection.response.subscribe(); expect(sendSpy).toHaveBeenCalledWith(body); - expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'text/plain'); + expect(setRequestHeaderSpy).toHaveBeenCalledWith('content-type', 'text/plain'); }); it('should use URLSearchParams body and detect content type header to the request', () => { @@ -294,7 +294,7 @@ export function main() { expect(sendSpy).toHaveBeenCalledWith('test1=val1&test2=val2'); expect(setRequestHeaderSpy) .toHaveBeenCalledWith( - 'Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8'); + 'content-type', 'application/x-www-form-urlencoded;charset=UTF-8'); }); if ((global as any /** TODO #9100 */)['Blob']) { @@ -335,7 +335,7 @@ export function main() { new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR()); connection.response.subscribe(); expect(sendSpy).toHaveBeenCalledWith(body); - expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'text/css'); + expect(setRequestHeaderSpy).toHaveBeenCalledWith('content-type', 'text/css'); }); it('should use blob body without type to the request', () => { @@ -358,7 +358,7 @@ export function main() { new MockBrowserXHR()); connection.response.subscribe(); expect(sendSpy).toHaveBeenCalledWith(body); - expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'text/css'); + expect(setRequestHeaderSpy).toHaveBeenCalledWith('content-type', 'text/css'); }); it('should use array buffer body to the request', () => { @@ -389,7 +389,7 @@ export function main() { new MockBrowserXHR()); connection.response.subscribe(); expect(sendSpy).toHaveBeenCalledWith(body); - expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'text/css'); + expect(setRequestHeaderSpy).toHaveBeenCalledWith('content-type', 'text/css'); }); } diff --git a/modules/@angular/http/test/headers_spec.ts b/modules/@angular/http/test/headers_spec.ts index 5645911793..cc522651a2 100644 --- a/modules/@angular/http/test/headers_spec.ts +++ b/modules/@angular/http/test/headers_spec.ts @@ -20,6 +20,10 @@ export function main() { 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'); var httpHeaders = StringMapWrapper.create(); StringMapWrapper.set(httpHeaders, 'Content-Type', 'image/jpeg'); StringMapWrapper.set(httpHeaders, 'Accept-Charset', 'utf-8'); @@ -72,7 +76,7 @@ export function main() { beforeEach(() => { headers = new Headers(); inputArr = ['application/jeisen', 'application/jason', 'application/patrickjs']; - obj = {'Accept': inputArr}; + obj = {'accept': inputArr}; headers.set('Accept', inputArr); });