diff --git a/modules/angular2/src/http/backends/jsonp_backend.ts b/modules/angular2/src/http/backends/jsonp_backend.ts index 12bad4c0ebe1..4059ce7c4a4f 100644 --- a/modules/angular2/src/http/backends/jsonp_backend.ts +++ b/modules/angular2/src/http/backends/jsonp_backend.ts @@ -57,7 +57,7 @@ export class JSONPConnection_ extends JSONPConnection { _dom.cleanup(script); if (!this._finished) { let responseOptions = - new ResponseOptions({body: JSONP_ERR_NO_CALLBACK, type: ResponseTypes.Error}); + new ResponseOptions({body: JSONP_ERR_NO_CALLBACK, type: ResponseTypes.Error, url}); if (isPresent(baseResponseOptions)) { responseOptions = baseResponseOptions.merge(responseOptions); } @@ -65,7 +65,7 @@ export class JSONPConnection_ extends JSONPConnection { return; } - let responseOptions = new ResponseOptions({body: this._responseData}); + let responseOptions = new ResponseOptions({body: this._responseData, url}); if (isPresent(this.baseResponseOptions)) { responseOptions = this.baseResponseOptions.merge(responseOptions); } diff --git a/modules/angular2/src/http/backends/xhr_backend.ts b/modules/angular2/src/http/backends/xhr_backend.ts index 7de04161a8e4..a234e07ac876 100644 --- a/modules/angular2/src/http/backends/xhr_backend.ts +++ b/modules/angular2/src/http/backends/xhr_backend.ts @@ -2,11 +2,13 @@ import {ConnectionBackend, Connection} from '../interfaces'; import {ReadyStates, RequestMethods, ResponseTypes} from '../enums'; import {Request} from '../static_request'; import {Response} from '../static_response'; +import {Headers} from '../headers'; import {ResponseOptions, BaseResponseOptions} from '../base_response_options'; import {Injectable} from 'angular2/angular2'; import {BrowserXhr} from './browser_xhr'; import {isPresent} from 'angular2/src/facade/lang'; import {Observable} from 'angular2/angular2'; +import {isSuccess, getResponseURL} from '../http_utils'; /** * Creates connections using `XMLHttpRequest`. Given a fully-qualified * request, an `XHRConnection` will immediately create an `XMLHttpRequest` object and send the @@ -33,24 +35,33 @@ export class XHRConnection implements Connection { // responseText is the old-school way of retrieving response (supported by IE8 & 9) // response/responseType properties were introduced in XHR Level2 spec (supported by // IE10) - let response = isPresent(_xhr.response) ? _xhr.response : _xhr.responseText; + let body = isPresent(_xhr.response) ? _xhr.response : _xhr.responseText; + + let headers = Headers.fromResponseHeaderString(_xhr.getAllResponseHeaders()); + + let url = getResponseURL(_xhr); // normalize IE9 bug (http://bugs.jquery.com/ticket/1450) - let status = _xhr.status === 1223 ? 204 : _xhr.status; + let status: number = _xhr.status === 1223 ? 204 : _xhr.status; // fix status code when it is 0 (0 status is undocumented). // Occurs when accessing file resources or on Android 4.1 stock browser // while retrieving files from application cache. if (status === 0) { - status = response ? 200 : 0; + status = body ? 200 : 0; } - var responseOptions = new ResponseOptions({body: response, status: status}); + var responseOptions = new ResponseOptions({body, status, headers, url}); if (isPresent(baseResponseOptions)) { responseOptions = baseResponseOptions.merge(responseOptions); } - responseObserver.next(new Response(responseOptions)); - // TODO(gdi2290): defer complete if array buffer until done - responseObserver.complete(); + let response = new Response(responseOptions); + if (isSuccess(status)) { + responseObserver.next(response); + // TODO(gdi2290): defer complete if array buffer until done + responseObserver.complete(); + return; + } + responseObserver.error(response); }; // error event handler let onError = (err) => { diff --git a/modules/angular2/src/http/headers.ts b/modules/angular2/src/http/headers.ts index 03c45aae6ece..b6740e642b80 100644 --- a/modules/angular2/src/http/headers.ts +++ b/modules/angular2/src/http/headers.ts @@ -54,6 +54,17 @@ export class Headers { headers, (v, k) => { this._headersMap.set(k, isListLikeIterable(v) ? v : [v]); }); } + /** + * Returns a new Headers instance from the given DOMString of Response Headers + */ + static fromResponseHeaderString(headersString: string): Headers { + return headersString.trim() + .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()); + } + /** * Appends a header to existing list of header values for a given header name. */ diff --git a/modules/angular2/src/http/http.ts b/modules/angular2/src/http/http.ts index bba1d73e3d85..09f48159997d 100644 --- a/modules/angular2/src/http/http.ts +++ b/modules/angular2/src/http/http.ts @@ -16,9 +16,9 @@ function mergeOptions(defaultOpts, providedOpts, method, url): RequestOptions { var newOptions = defaultOpts; if (isPresent(providedOpts)) { // Hack so Dart can used named parameters - newOptions = newOptions.merge(new RequestOptions({ - method: providedOpts.method, - url: providedOpts.url, + return newOptions.merge(new RequestOptions({ + method: providedOpts.method || method, + url: providedOpts.url || url, search: providedOpts.search, headers: providedOpts.headers, body: providedOpts.body diff --git a/modules/angular2/src/http/http_utils.ts b/modules/angular2/src/http/http_utils.ts index 77bb16978cbe..e4c2051e0bc9 100644 --- a/modules/angular2/src/http/http_utils.ts +++ b/modules/angular2/src/http/http_utils.ts @@ -1,6 +1,7 @@ import {isString} from 'angular2/src/facade/lang'; import {RequestMethods} from './enums'; import {makeTypeError} from 'angular2/src/facade/exceptions'; +import {Response} from './static_response'; export function normalizeMethodName(method): RequestMethods { if (isString(method)) { @@ -14,4 +15,16 @@ export function normalizeMethodName(method): RequestMethods { return method; } +export const isSuccess = (status: number): boolean => (status >= 200 && status < 300); + +export function getResponseURL(xhr: any): string { + if ('responseURL' in xhr) { + return xhr.responseURL; + } + if (/^X-Request-URL:/m.test(xhr.getAllResponseHeaders())) { + return xhr.getResponseHeader('X-Request-URL'); + } + return; +} + export {isJsObject} from 'angular2/src/facade/lang'; diff --git a/modules/angular2/test/http/backends/xhr_backend_spec.ts b/modules/angular2/test/http/backends/xhr_backend_spec.ts index f1db60ad847d..865eb8e4c1fc 100644 --- a/modules/angular2/test/http/backends/xhr_backend_spec.ts +++ b/modules/angular2/test/http/backends/xhr_backend_spec.ts @@ -40,6 +40,8 @@ class MockBrowserXHR extends BrowserXhr { setRequestHeader: any; callbacks = new Map(); status: number; + responseHeaders: string; + responseURL: string; constructor() { super(); var spy = new SpyObject(); @@ -55,6 +57,14 @@ class MockBrowserXHR extends BrowserXhr { setResponseText(value) { this.responseText = value; } + setResponseURL(value) { this.responseURL = value; } + + setResponseHeaders(value) { this.responseHeaders = value; } + + getAllResponseHeaders() { return this.responseHeaders || ''; } + + getResponseHeader(key) { return Headers.fromResponseHeaderString(this.responseHeaders).get(key); } + addEventListener(type: string, cb: Function) { this.callbacks.set(type, cb); } removeEventListener(type: string, cb: Function) { this.callbacks.delete(type); } @@ -99,6 +109,7 @@ export function main() { expect(res.type).toBe(ResponseTypes.Error); async.done(); }); + existingXHRs[0].setStatusCode(200); existingXHRs[0].dispatchEvent('load'); })); @@ -107,7 +118,7 @@ export function main() { new ResponseOptions({type: ResponseTypes.Error})); connection.response.subscribe(res => { expect(res.type).toBe(ResponseTypes.Error); }, null, () => { async.done(); }); - + existingXHRs[0].setStatusCode(200); existingXHRs[0].dispatchEvent('load'); })); @@ -164,15 +175,57 @@ export function main() { var connection = new XHRConnection(sampleRequest, new MockBrowserXHR(), new ResponseOptions({status: statusCode})); - connection.response.subscribe(res => { - expect(res.status).toBe(statusCode); - async.done(); - }); + connection.response.subscribe( + res => { + + }, + errRes => { + expect(errRes.status).toBe(statusCode); + async.done(); + }); existingXHRs[0].setStatusCode(statusCode); existingXHRs[0].dispatchEvent('load'); })); + it('should call next and complete on 200 codes', inject([AsyncTestCompleter], async => { + var nextCalled = false; + var errorCalled = false; + var statusCode = 200; + var connection = new XHRConnection(sampleRequest, new MockBrowserXHR(), + new ResponseOptions({status: statusCode})); + + connection.response.subscribe( + res => { + nextCalled = true; + expect(res.status).toBe(statusCode); + }, + errRes => { errorCalled = true; }, () => { + expect(nextCalled).toBe(true); + expect(errorCalled).toBe(false); + async.done(); + }); + + existingXHRs[0].setStatusCode(statusCode); + existingXHRs[0].dispatchEvent('load'); + })); + + it('should call error and not complete on 300+ codes', inject([AsyncTestCompleter], async => { + var nextCalled = false; + var errorCalled = false; + var statusCode = 301; + var connection = new XHRConnection(sampleRequest, new MockBrowserXHR(), + new ResponseOptions({status: statusCode})); + + connection.response.subscribe(res => { nextCalled = true; }, errRes => { + expect(errRes.status).toBe(statusCode); + expect(nextCalled).toBe(false); + async.done(); + }, () => { throw 'should not be called'; }); + + existingXHRs[0].setStatusCode(statusCode); + existingXHRs[0].dispatchEvent('load'); + })); it('should normalize IE\'s 1223 status code into 204', inject([AsyncTestCompleter], async => { var statusCode = 1223; var normalizedCode = 204; @@ -204,14 +257,72 @@ export function main() { expect(ress.text()).toBe(responseBody); async.done(); }); + existingXHRs[1].setStatusCode(200); existingXHRs[1].setResponse(responseBody); existingXHRs[1].dispatchEvent('load'); }); - + existingXHRs[0].setStatusCode(200); existingXHRs[0].setResponseText(responseBody); existingXHRs[0].dispatchEvent('load'); })); + it('should parse response headers and add them to the response', + inject([AsyncTestCompleter], async => { + var statusCode = 200; + var connection = new XHRConnection(sampleRequest, new MockBrowserXHR(), + new ResponseOptions({status: statusCode})); + + let responseHeaderString = + `Date: Fri, 20 Nov 2015 01:45:26 GMT + Content-Type: application/json; charset=utf-8 + Transfer-Encoding: chunked + Connection: keep-alive` + + connection.response.subscribe(res => { + expect(res.headers.get('Date')).toEqual('Fri, 20 Nov 2015 01:45:26 GMT'); + expect(res.headers.get('Content-Type')).toEqual('application/json; charset=utf-8'); + expect(res.headers.get('Transfer-Encoding')).toEqual('chunked'); + expect(res.headers.get('Connection')).toEqual('keep-alive'); + async.done(); + }); + + existingXHRs[0].setResponseHeaders(responseHeaderString); + existingXHRs[0].setStatusCode(statusCode); + existingXHRs[0].dispatchEvent('load'); + })); + + it('should add the responseURL to the response', inject([AsyncTestCompleter], async => { + var statusCode = 200; + var connection = new XHRConnection(sampleRequest, new MockBrowserXHR(), + new ResponseOptions({status: statusCode})); + + connection.response.subscribe(res => { + expect(res.url).toEqual('http://google.com'); + async.done(); + }); + + existingXHRs[0].setResponseURL('http://google.com'); + existingXHRs[0].setStatusCode(statusCode); + existingXHRs[0].dispatchEvent('load'); + })); + + it('should add use the X-Request-URL in CORS situations', + inject([AsyncTestCompleter], async => { + var statusCode = 200; + var connection = new XHRConnection(sampleRequest, new MockBrowserXHR(), + new ResponseOptions({status: statusCode})); + var responseHeaders = `X-Request-URL: http://somedomain.com + Foo: Bar` + + connection.response.subscribe(res => { + expect(res.url).toEqual('http://somedomain.com'); + async.done(); + }); + + existingXHRs[0].setResponseHeaders(responseHeaders); + existingXHRs[0].setStatusCode(statusCode); + existingXHRs[0].dispatchEvent('load'); + })); }); }); } diff --git a/modules/angular2/test/http/headers_spec.ts b/modules/angular2/test/http/headers_spec.ts index 156ab65b3062..0102164f4ac9 100644 --- a/modules/angular2/test/http/headers_spec.ts +++ b/modules/angular2/test/http/headers_spec.ts @@ -63,4 +63,23 @@ 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'); + + }); + }); } diff --git a/modules/angular2/test/http/http_spec.ts b/modules/angular2/test/http/http_spec.ts index 1c5fd71f5741..f67bc03f09ca 100644 --- a/modules/angular2/test/http/http_spec.ts +++ b/modules/angular2/test/http/http_spec.ts @@ -151,6 +151,19 @@ export function main() { .subscribe((res) => {}); })); + it('should accept a fully-qualified request as its only parameter', + inject([AsyncTestCompleter], (async) => { + backend.connections.subscribe(c => { + expect(c.request.url).toBe('https://google.com'); + expect(c.request.method).toBe(RequestMethods.Post); + c.mockRespond(new Response(new ResponseOptions({body: 'Thank you'}))); + async.done(); + }); + http.request(new Request(new RequestOptions( + {url: 'https://google.com', method: RequestMethods.Post}))) + .subscribe((res) => {}); + })); + it('should perform a get request for given url if only passed a string', inject([AsyncTestCompleter], (async) => { @@ -162,6 +175,34 @@ export function main() { }); })); + it('should perform a post request for given url if options include a method', + inject([AsyncTestCompleter], (async) => { + backend.connections.subscribe(c => { + expect(c.request.method).toEqual(RequestMethods.Post); + c.mockRespond(baseResponse); + }); + let requestOptions = new RequestOptions({method: RequestMethods.Post}); + http.request('http://basic.connection', requestOptions) + .subscribe(res => { + expect(res.text()).toBe('base response'); + async.done(); + }); + })); + + it('should perform a post request for given url if options include a method', + inject([AsyncTestCompleter], (async) => { + backend.connections.subscribe(c => { + expect(c.request.method).toEqual(RequestMethods.Post); + c.mockRespond(baseResponse); + }); + let requestOptions = {method: RequestMethods.Post}; + http.request('http://basic.connection', requestOptions) + .subscribe(res => { + expect(res.text()).toBe('base response'); + async.done(); + }); + })); + it('should perform a get request and complete the response', inject([AsyncTestCompleter], (async) => { backend.connections.subscribe(c => c.mockRespond(baseResponse)); @@ -180,18 +221,6 @@ export function main() { .subscribe(res => { expect(res.text()).toBe('base response'); }, null, () => { async.done(); }); })); - // TODO: make dart not complain about "argument type 'Map' cannot be assigned to the - // parameter type 'IRequestOptions'" - // xit('should perform a get request for given url if passed a dictionary', - // inject([AsyncTestCompleter], async => { - // ObservableWrapper.subscribe(backend.connections, c => c.mockRespond(baseResponse)); - // ObservableWrapper.subscribe(http.request(url, {method: RequestMethods.GET}), res => - // { - // expect(res.text()).toBe('base response'); - // async.done(); - // }); - // })); - it('should throw if url is not a string or Request', () => { var req = {};