From 37797e2b4e1ddc6560ae6c8364ea678ffbaa3de1 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Wed, 22 Mar 2017 17:13:24 -0700 Subject: [PATCH] feat(common): new HttpClient API HttpClient is an evolution of the existing Angular HTTP API, which exists alongside of it in a separate package, @angular/common/http. This structure ensures that existing codebases can slowly migrate to the new API. The new API improves significantly on the ergonomics and features of the legacy API. A partial list of new features includes: * Typed, synchronous response body access, including support for JSON body types * JSON is an assumed default and no longer needs to be explicitly parsed * Interceptors allow middleware logic to be inserted into the pipeline * Immutable request/response objects * Progress events for both request upload and response download * Post-request verification & flush based testing framework --- npm-shrinkwrap.clean.json | 2 +- npm-shrinkwrap.json | 6 +- package.json | 2 +- packages/common/BUILD | 1 + packages/common/http/index.ts | 14 + packages/common/http/package.json | 7 + packages/common/http/public_api.ts | 18 + packages/common/http/rollup.config.js | 21 + packages/common/http/src/backend.ts | 25 + packages/common/http/src/client.ts | 891 ++++++++++++++++ packages/common/http/src/headers.ts | 214 ++++ packages/common/http/src/interceptor.ts | 66 ++ packages/common/http/src/jsonp.ts | 224 ++++ packages/common/http/src/module.ts | 93 ++ packages/common/http/src/request.ts | 325 ++++++ packages/common/http/src/response.ts | 329 ++++++ packages/common/http/src/url_encoded_body.ts | 214 ++++ packages/common/http/src/xhr.ts | 327 ++++++ packages/common/http/test/client_spec.ts | 114 ++ packages/common/http/test/headers_spec.ts | 144 +++ packages/common/http/test/jsonp_mock.ts | 44 + packages/common/http/test/jsonp_spec.ts | 75 ++ packages/common/http/test/module_spec.ts | 88 ++ packages/common/http/test/request_spec.ts | 136 +++ packages/common/http/test/response_spec.ts | 78 ++ .../common/http/test/url_encoded_body_spec.ts | 76 ++ packages/common/http/test/xhr_mock.ts | 119 +++ packages/common/http/test/xhr_spec.ts | 306 ++++++ packages/common/http/testing/index.ts | 9 + packages/common/http/testing/package.json | 7 + packages/common/http/testing/public_api.ts | 11 + packages/common/http/testing/rollup.config.js | 29 + packages/common/http/testing/src/api.ts | 49 + packages/common/http/testing/src/backend.ts | 124 +++ packages/common/http/testing/src/module.ts | 34 + packages/common/http/testing/src/request.ts | 200 ++++ .../common/http/testing/tsconfig-build.json | 32 + packages/common/http/tsconfig-build.json | 30 + .../platform-browser/src/dom/dom_tokens.ts | 4 +- packages/platform-server/src/http.ts | 101 +- packages/platform-server/src/server.ts | 3 +- packages/platform-server/tsconfig-build.json | 1 + test-main.js | 2 + tools/gulp-tasks/public-api.js | 1 + tools/public_api_guard/common/common.d.ts | 3 + tools/public_api_guard/common/http.d.ts | 991 ++++++++++++++++++ .../public_api_guard/common/http/testing.d.ts | 47 + .../platform-browser/platform-browser.d.ts | 2 +- 48 files changed, 5599 insertions(+), 40 deletions(-) create mode 100644 packages/common/http/index.ts create mode 100644 packages/common/http/package.json create mode 100644 packages/common/http/public_api.ts create mode 100644 packages/common/http/rollup.config.js create mode 100644 packages/common/http/src/backend.ts create mode 100644 packages/common/http/src/client.ts create mode 100755 packages/common/http/src/headers.ts create mode 100644 packages/common/http/src/interceptor.ts create mode 100644 packages/common/http/src/jsonp.ts create mode 100644 packages/common/http/src/module.ts create mode 100644 packages/common/http/src/request.ts create mode 100644 packages/common/http/src/response.ts create mode 100755 packages/common/http/src/url_encoded_body.ts create mode 100644 packages/common/http/src/xhr.ts create mode 100644 packages/common/http/test/client_spec.ts create mode 100644 packages/common/http/test/headers_spec.ts create mode 100644 packages/common/http/test/jsonp_mock.ts create mode 100644 packages/common/http/test/jsonp_spec.ts create mode 100644 packages/common/http/test/module_spec.ts create mode 100644 packages/common/http/test/request_spec.ts create mode 100644 packages/common/http/test/response_spec.ts create mode 100644 packages/common/http/test/url_encoded_body_spec.ts create mode 100644 packages/common/http/test/xhr_mock.ts create mode 100644 packages/common/http/test/xhr_spec.ts create mode 100644 packages/common/http/testing/index.ts create mode 100644 packages/common/http/testing/package.json create mode 100644 packages/common/http/testing/public_api.ts create mode 100644 packages/common/http/testing/rollup.config.js create mode 100644 packages/common/http/testing/src/api.ts create mode 100644 packages/common/http/testing/src/backend.ts create mode 100644 packages/common/http/testing/src/module.ts create mode 100644 packages/common/http/testing/src/request.ts create mode 100644 packages/common/http/testing/tsconfig-build.json create mode 100644 packages/common/http/tsconfig-build.json create mode 100644 tools/public_api_guard/common/http.d.ts create mode 100644 tools/public_api_guard/common/http/testing.d.ts diff --git a/npm-shrinkwrap.clean.json b/npm-shrinkwrap.clean.json index d0a2ee7c4d..56ca6f5f50 100644 --- a/npm-shrinkwrap.clean.json +++ b/npm-shrinkwrap.clean.json @@ -4807,7 +4807,7 @@ "version": "1.0.1" }, "ts-api-guardian": { - "version": "0.2.1", + "version": "0.2.2", "dependencies": { "diff": { "version": "2.2.3" diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index d5e2345ffa..fdfda54fdb 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -7679,9 +7679,9 @@ "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz" }, "ts-api-guardian": { - "version": "0.2.1", - "from": "ts-api-guardian@0.2.1", - "resolved": "https://registry.npmjs.org/ts-api-guardian/-/ts-api-guardian-0.2.1.tgz", + "version": "0.2.2", + "from": "ts-api-guardian@0.2.2", + "resolved": "https://registry.npmjs.org/ts-api-guardian/-/ts-api-guardian-0.2.2.tgz", "dependencies": { "diff": { "version": "2.2.3", diff --git a/package.json b/package.json index a9b93ca781..c75bea11cf 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "source-map": "^0.5.6", "source-map-support": "^0.4.2", "systemjs": "0.18.10", - "ts-api-guardian": "^0.2.1", + "ts-api-guardian": "^0.2.2", "tsickle": "^0.21.1", "tslint": "^4.1.1", "tslint-eslint-rules": "^3.1.0", diff --git a/packages/common/BUILD b/packages/common/BUILD index 857abc0b1e..fb99e72d1a 100644 --- a/packages/common/BUILD +++ b/packages/common/BUILD @@ -4,6 +4,7 @@ load("@io_bazel_rules_typescript//:defs.bzl", "ts_library") ts_library( name = "common", srcs = glob(["**/*.ts"], exclude=[ + "http/**", "test/**", "testing/**", ]), diff --git a/packages/common/http/index.ts b/packages/common/http/index.ts new file mode 100644 index 0000000000..ca39d26dcd --- /dev/null +++ b/packages/common/http/index.ts @@ -0,0 +1,14 @@ +/** + * @license + * Copyright Google Inc. 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 + */ + +// This file is not used to build this module. It is only used during editing +// by the TypeScript language service and during build for verification. `ngc` +// replaces this file with production index.ts when it rewrites private symbol +// names. + +export * from './public_api'; \ No newline at end of file diff --git a/packages/common/http/package.json b/packages/common/http/package.json new file mode 100644 index 0000000000..31a1d9572d --- /dev/null +++ b/packages/common/http/package.json @@ -0,0 +1,7 @@ +{ + "name": "@angular/common/http", + "typings": "../http.d.ts", + "main": "../bundles/common-http.umd.js", + "module": "../@angular/common/http.es5.js", + "es2015": "../@angular/common/http.js" +} diff --git a/packages/common/http/public_api.ts b/packages/common/http/public_api.ts new file mode 100644 index 0000000000..7766d768d2 --- /dev/null +++ b/packages/common/http/public_api.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright Google Inc. 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 + */ + +export {HttpBackend, HttpHandler} from './src/backend'; +export {HttpClient} from './src/client'; +export {HttpHeaders} from './src/headers'; +export {HTTP_INTERCEPTORS, HttpInterceptor} from './src/interceptor'; +export {JsonpClientBackend, JsonpInterceptor} from './src/jsonp'; +export {HttpClientJsonpModule, HttpClientModule, interceptingHandler as ɵinterceptingHandler} from './src/module'; +export {HttpRequest} from './src/request'; +export {HttpDownloadProgressEvent, HttpErrorResponse, HttpEvent, HttpEventType, HttpHeaderResponse, HttpProgressEvent, HttpResponse, HttpResponseBase, HttpSentEvent, HttpUserEvent} from './src/response'; +export {HttpStandardUrlParameterCodec, HttpUrlEncodedBody, HttpUrlParameterCodec} from './src/url_encoded_body'; +export {HttpXhrBackend, XhrFactory} from './src/xhr'; \ No newline at end of file diff --git a/packages/common/http/rollup.config.js b/packages/common/http/rollup.config.js new file mode 100644 index 0000000000..dfde0927f1 --- /dev/null +++ b/packages/common/http/rollup.config.js @@ -0,0 +1,21 @@ +/** + * @license + * Copyright Google Inc. 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 + */ + +export default { + entry: '../../../dist/packages-dist/common/@angular/common/http.es5.js', + dest: '../../../dist/packages-dist/common/bundles/common-http.umd.js', + format: 'umd', + exports: 'named', + moduleName: 'ng.commmon.http', + globals: { + '@angular/core': 'ng.core', + '@angular/platform-browser': 'ng.platformBrowser', + 'rxjs/Observable': 'Rx', + 'rxjs/Subject': 'Rx' + } +}; diff --git a/packages/common/http/src/backend.ts b/packages/common/http/src/backend.ts new file mode 100644 index 0000000000..f7c9afd1f7 --- /dev/null +++ b/packages/common/http/src/backend.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright Google Inc. 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 + */ + +import {Observable} from 'rxjs/Observable'; +import {HttpRequest} from './request'; +import {HttpEvent} from './response'; + +/** + * @experimental + */ +export abstract class HttpHandler { + abstract handle(req: HttpRequest): Observable>; +} + +/** + * @experimental + */ +export abstract class HttpBackend implements HttpHandler { + abstract handle(req: HttpRequest): Observable>; +} diff --git a/packages/common/http/src/client.ts b/packages/common/http/src/client.ts new file mode 100644 index 0000000000..7ea0c10cac --- /dev/null +++ b/packages/common/http/src/client.ts @@ -0,0 +1,891 @@ +/** + * @license + * Copyright Google Inc. 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 + */ + +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs/Observable'; +import {of } from 'rxjs/observable/of'; +import {concatMap} from 'rxjs/operator/concatMap'; +import {filter} from 'rxjs/operator/filter'; +import {map} from 'rxjs/operator/map'; + +import {HttpHandler} from './backend'; +import {HttpHeaders} from './headers'; +import {HttpRequest} from './request'; +import {HttpEvent, HttpEventType, HttpResponse} from './response'; + + +/** + * Construct an instance of `HttpRequestOptions` from a source `HttpMethodOptions` and + * the given `body`. Basically, this clones the object and adds the body. + */ +function addBody( + options: { + headers?: HttpHeaders, + observe?: HttpObserve, + responseType?: 'arraybuffer' | 'blob' | 'json' | 'text', + withCredentials?: boolean, + }, + body: T | null): any { + return { + body, + headers: options.headers, + observe: options.observe, + responseType: options.responseType, + withCredentials: options.withCredentials, + }; +} + +/** + * @experimental + */ +export type HttpObserve = 'body' | 'events' | 'response'; + +/** + * The main API for making outgoing HTTP requests. + * + * @experimental + */ +@Injectable() +export class HttpClient { + constructor(private handler: HttpHandler) {} + + request(req: HttpRequest): Observable>; + request(method: string, url: string, options: { + body?: any, + headers?: HttpHeaders, + observe?: 'body', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable; + request(method: string, url: string, options: { + body?: any, + headers?: HttpHeaders, + observe?: 'body', + responseType: 'blob', withCredentials?: boolean, + }): Observable; + request(method: string, url: string, options: { + body?: any, + headers?: HttpHeaders, + observe?: 'body', + responseType: 'text', withCredentials?: boolean, + }): Observable; + request(method: string, url: string, options: { + body?: any, + headers?: HttpHeaders, + observe: 'events', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable>; + request(method: string, url: string, options: { + body?: any, + headers?: HttpHeaders, + observe: 'events', + responseType: 'blob', withCredentials?: boolean, + }): Observable>; + request(method: string, url: string, options: { + body?: any, + headers?: HttpHeaders, + observe: 'events', + responseType: 'text', withCredentials?: boolean, + }): Observable>; + request(method: string, url: string, options: { + body?: any, + headers?: HttpHeaders, + observe: 'events', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + request(method: string, url: string, options: { + body?: any, + headers?: HttpHeaders, + observe: 'response', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable>; + request(method: string, url: string, options: { + body?: any, + headers?: HttpHeaders, + observe: 'response', + responseType: 'blob', withCredentials?: boolean, + }): Observable>; + request(method: string, url: string, options: { + body?: any, + headers?: HttpHeaders, + observe: 'response', + responseType: 'text', withCredentials?: boolean, + }): Observable>; + request(method: string, url: string, options: { + body?: any, + headers?: HttpHeaders, + observe: 'response', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + request(method: string, url: string, options?: { + body?: any, + headers?: HttpHeaders, + observe?: 'body', + responseType?: 'json', + withCredentials?: boolean, + }): Observable; + request(method: string, url: string, options?: { + body?: any, + headers?: HttpHeaders, + observe?: 'body', + responseType?: 'json', + withCredentials?: boolean, + }): Observable; + request(method: string, url: string, options?: { + body?: any, + headers?: HttpHeaders, + observe?: HttpObserve, + responseType?: 'arraybuffer'|'blob'|'json'|'text', + withCredentials?: boolean, + }): Observable; + /** + * Constructs an `Observable` for a particular HTTP request that, when subscribed, + * fires the request through the chain of registered interceptors and on to the + * server. + * + * This method can be called in one of two ways. Either an `HttpRequest` + * instance can be passed directly as the only parameter, or a method can be + * passed as the first parameter, a string URL as the second, and an + * options hash as the third. + * + * If a `HttpRequest` object is passed directly, an `Observable` of the + * raw `HttpEvent` stream will be returned. + * + * If a request is instead built by providing a URL, the options object + * determines the return type of `request()`. In addition to configuring + * request parameters such as the outgoing headers and/or the body, the options + * hash specifies two key pieces of information about the request: the + * `responseType` and what to `observe`. + * + * The `responseType` value determines how a successful response body will be + * parsed. If `responseType` is the default `json`, a type interface for the + * resulting object may be passed as a type parameter to `request()`. + * + * The `observe` value determines the return type of `request()`, based on what + * the consumer is interested in observing. A value of `events` will return an + * `Observable` representing the raw `HttpEvent` stream, + * including progress events by default. A value of `response` will return an + * `Observable>` where the `T` parameter of `HttpResponse` + * depends on the `responseType` and any optionally provided type parameter. + * A value of `body` will return an `Observable` with the same `T` body type. + */ + request(first: string|HttpRequest, url?: string, options: { + body?: any, + headers?: HttpHeaders, + observe?: HttpObserve, + responseType?: 'arraybuffer'|'blob'|'json'|'text', + withCredentials?: boolean, + } = {}): Observable { + let req: HttpRequest; + // Firstly, check whether the primary argument is an instance of `HttpRequest`. + if (first instanceof HttpRequest) { + // It is. The other arguments must be undefined (per the signatures) and can be + // ignored. + req = first as HttpRequest; + } else { + // It's a string, so it represents a URL. Construct a request based on it, + // and incorporate the remaining arguments (assuming GET unless a method is + // provided. + req = new HttpRequest(first, url !, options.body || null, { + headers: options.headers, + // By default, JSON is assumed to be returned for all calls. + responseType: options.responseType || 'json', + withCredentials: options.withCredentials, + }); + } + + // Start with an Observable.of() the initial request, and run the handler (which + // includes all interceptors) inside a concatMap(). This way, the handler runs + // inside an Observable chain, which causes interceptors to be re-run on every + // subscription (this also makes retries re-run the handler, including interceptors). + const events$: Observable> = + concatMap.call(of (req), (req: HttpRequest) => this.handler.handle(req)); + + // If coming via the API signature which accepts a previously constructed HttpRequest, + // the only option is to get the event stream. Otherwise, return the event stream if + // that is what was requested. + if (first instanceof HttpRequest || options.observe === 'events') { + return events$; + } + + // The requested stream contains either the full response or the body. In either + // case, the first step is to filter the event stream to extract a stream of + // responses(s). + const res$: Observable> = + filter.call(events$, (event: HttpEvent) => event instanceof HttpResponse); + + // Decide which stream to return. + switch (options.observe || 'body') { + case 'body': + // The requested stream is the body. Map the response stream to the response + // body. This could be done more simply, but a misbehaving interceptor might + // transform the response body into a different format and ignore the requested + // responseType. Guard against this by validating that the response is of the + // requested type. + switch (req.responseType) { + case 'arraybuffer': + return map.call(res$, (res: HttpResponse) => { + // Validate that the body is an ArrayBuffer. + if (res.body !== null && !(res.body instanceof ArrayBuffer)) { + throw new Error('Response is not an ArrayBuffer.'); + } + return res.body; + }); + case 'blob': + return map.call(res$, (res: HttpResponse) => { + // Validate that the body is a Blob. + if (res.body !== null && !(res.body instanceof Blob)) { + throw new Error('Response is not a Blob.'); + } + return res.body; + }); + case 'text': + return map.call(res$, (res: HttpResponse) => { + // Validate that the body is a string. + if (res.body !== null && typeof res.body !== 'string') { + throw new Error('Response is not a string.'); + } + return res.body; + }); + case 'json': + default: + // No validation needed for JSON responses, as they can be of any type. + return map.call(res$, (res: HttpResponse) => res.body); + } + case 'response': + // The response stream was requested directly, so return it. + return res$; + default: + // Guard against new future observe types being added. + throw new Error(`Unreachable: unhandled observe type ${options.observe}}`); + } + } + + delete (url: string, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable; + delete (url: string, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'blob', withCredentials?: boolean, + }): Observable; + delete (url: string, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'text', withCredentials?: boolean, + }): Observable; + delete (url: string, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable>; + delete (url: string, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'blob', withCredentials?: boolean, + }): Observable>; + delete (url: string, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'text', withCredentials?: boolean, + }): Observable>; + delete (url: string, options: { + headers?: HttpHeaders, + observe: 'events', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + delete(url: string, options: { + headers?: HttpHeaders, + observe: 'events', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + delete (url: string, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable>; + delete (url: string, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'blob', withCredentials?: boolean, + }): Observable>; + delete (url: string, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'text', withCredentials?: boolean, + }): Observable>; + delete (url: string, options: { + headers?: HttpHeaders, + observe: 'response', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + delete(url: string, options: { + headers?: HttpHeaders, + observe: 'response', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + delete (url: string, options?: { + headers?: HttpHeaders, + observe?: 'body', + responseType?: 'json', + withCredentials?: boolean, + }): Observable; + delete(url: string, options?: { + headers?: HttpHeaders, + observe?: 'body', + responseType?: 'json', + withCredentials?: boolean, + }): Observable; + /** + * Constructs an `Observable` which, when subscribed, will cause the configured + * DELETE request to be executed on the server. See {@link HttpClient#request} for + * details of `delete()`'s return type based on the provided options. + */ + delete (url: string, options: { + headers?: HttpHeaders, + observe?: HttpObserve, + responseType?: 'arraybuffer'|'blob'|'json'|'text', + withCredentials?: boolean, + } = {}): Observable { + return this.request('DELETE', url, options as any); + } + + get(url: string, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable; + get(url: string, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'blob', withCredentials?: boolean, + }): Observable; + get(url: string, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'text', withCredentials?: boolean, + }): Observable; + get(url: string, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable>; + get(url: string, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'blob', withCredentials?: boolean, + }): Observable>; + get(url: string, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'text', withCredentials?: boolean, + }): Observable>; + get(url: string, options: { + headers?: HttpHeaders, + observe: 'events', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + get(url: string, options: { + headers?: HttpHeaders, + observe: 'events', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + get(url: string, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable>; + get(url: string, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'blob', withCredentials?: boolean, + }): Observable>; + get(url: string, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'text', withCredentials?: boolean, + }): Observable>; + get(url: string, options: { + headers?: HttpHeaders, + observe: 'response', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + get(url: string, options: { + headers?: HttpHeaders, + observe: 'response', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + get(url: string, options?: { + headers?: HttpHeaders, + observe?: 'body', + responseType?: 'json', + withCredentials?: boolean, + }): Observable; + get(url: string, options?: { + headers?: HttpHeaders, + observe?: 'body', + responseType?: 'json', + withCredentials?: boolean, + }): Observable; + /** + * Constructs an `Observable` which, when subscribed, will cause the configured + * GET request to be executed on the server. See {@link HttpClient#request} for + * details of `get()`'s return type based on the provided options. + */ + get(url: string, options: { + headers?: HttpHeaders, + observe?: HttpObserve, + responseType?: 'arraybuffer'|'blob'|'json'|'text', + withCredentials?: boolean, + } = {}): Observable { + return this.request('GET', url, options as any); + } + + head(url: string, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable; + head(url: string, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'blob', withCredentials?: boolean, + }): Observable; + head(url: string, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'text', withCredentials?: boolean, + }): Observable; + head(url: string, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable>; + head(url: string, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'blob', withCredentials?: boolean, + }): Observable>; + head(url: string, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'text', withCredentials?: boolean, + }): Observable>; + head(url: string, options: { + headers?: HttpHeaders, + observe: 'events', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + head(url: string, options: { + headers?: HttpHeaders, + observe: 'events', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + head(url: string, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable>; + head(url: string, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'blob', withCredentials?: boolean, + }): Observable>; + head(url: string, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'text', withCredentials?: boolean, + }): Observable>; + head(url: string, options: { + headers?: HttpHeaders, + observe: 'response', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + head(url: string, options: { + headers?: HttpHeaders, + observe: 'response', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + head(url: string, options?: { + headers?: HttpHeaders, + observe?: 'body', + responseType?: 'json', + withCredentials?: boolean, + }): Observable; + head(url: string, options?: { + headers?: HttpHeaders, + observe?: 'body', + responseType?: 'json', + withCredentials?: boolean, + }): Observable; + /** + * Constructs an `Observable` which, when subscribed, will cause the configured + * HEAD request to be executed on the server. See {@link HttpClient#request} for + * details of `head()`'s return type based on the provided options. + */ + head(url: string, options: { + headers?: HttpHeaders, + observe?: HttpObserve, + responseType?: 'arraybuffer'|'blob'|'json'|'text', + withCredentials?: boolean, + } = {}): Observable { + return this.request('HEAD', url, options as any); + } + + jsonp(url: string): Observable; + jsonp(url: string): Observable; + /** + * Constructs an `Observable` which, when subscribed, will cause a request + * with the special method `JSONP` to be dispatched via the interceptor pipeline. + * + * A suitable interceptor must be installed (e.g. via the `HttpClientJsonpModule`). + * If no such interceptor is reached, then the `JSONP` request will likely be + * rejected by the configured backend. + */ + jsonp(url: string): Observable { + return this.request('JSONP', url, { + observe: 'body', + responseType: 'json', + }); + } + + options(url: string, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable; + options(url: string, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'blob', withCredentials?: boolean, + }): Observable; + options(url: string, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'text', withCredentials?: boolean, + }): Observable; + options(url: string, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable>; + options(url: string, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'blob', withCredentials?: boolean, + }): Observable>; + options(url: string, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'text', withCredentials?: boolean, + }): Observable>; + options(url: string, options: { + headers?: HttpHeaders, + observe: 'events', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + options(url: string, options: { + headers?: HttpHeaders, + observe: 'events', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + options(url: string, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable>; + options(url: string, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'blob', withCredentials?: boolean, + }): Observable>; + options(url: string, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'text', withCredentials?: boolean, + }): Observable>; + options(url: string, options: { + headers?: HttpHeaders, + observe: 'response', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + options(url: string, options: { + headers?: HttpHeaders, + observe: 'response', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + options(url: string, options?: { + headers?: HttpHeaders, + observe?: 'body', + responseType?: 'json', + withCredentials?: boolean, + }): Observable; + options(url: string, options?: { + headers?: HttpHeaders, + observe?: 'body', + responseType?: 'json', + withCredentials?: boolean, + }): Observable; + /** + * Constructs an `Observable` which, when subscribed, will cause the configured + * OPTIONS request to be executed on the server. See {@link HttpClient#request} for + * details of `options()`'s return type based on the provided options. + */ + options(url: string, options: { + headers?: HttpHeaders, + observe?: HttpObserve, + responseType?: 'arraybuffer'|'blob'|'json'|'text', + withCredentials?: boolean, + } = {}): Observable { + return this.request('OPTIONS', url, options as any); + } + + patch(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable; + patch(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'blob', withCredentials?: boolean, + }): Observable; + patch(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'text', withCredentials?: boolean, + }): Observable; + patch(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable>; + patch(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'blob', withCredentials?: boolean, + }): Observable>; + patch(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'text', withCredentials?: boolean, + }): Observable>; + patch(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'events', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + patch(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'events', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + patch(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable>; + patch(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'blob', withCredentials?: boolean, + }): Observable>; + patch(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'text', withCredentials?: boolean, + }): Observable>; + patch(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'response', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + patch(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'response', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + patch(url: string, body: any|null, options?: { + headers?: HttpHeaders, + observe?: 'body', + responseType?: 'json', + withCredentials?: boolean, + }): Observable; + patch(url: string, body: any|null, options?: { + headers?: HttpHeaders, + observe?: 'body', + responseType?: 'json', + withCredentials?: boolean, + }): Observable; + /** + * Constructs an `Observable` which, when subscribed, will cause the configured + * PATCH request to be executed on the server. See {@link HttpClient#request} for + * details of `patch()`'s return type based on the provided options. + */ + patch(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe?: HttpObserve, + responseType?: 'arraybuffer'|'blob'|'json'|'text', + withCredentials?: boolean, + } = {}): Observable { + return this.request('PATCH', url, addBody(options, body)); + } + + post(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable; + post(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'blob', withCredentials?: boolean, + }): Observable; + post(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'text', withCredentials?: boolean, + }): Observable; + post(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable>; + post(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'blob', withCredentials?: boolean, + }): Observable>; + post(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'text', withCredentials?: boolean, + }): Observable>; + post(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'events', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + post(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'events', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + post(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable>; + post(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'blob', withCredentials?: boolean, + }): Observable>; + post(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'text', withCredentials?: boolean, + }): Observable>; + post(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'response', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + post(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'response', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + post(url: string, body: any|null, options?: { + headers?: HttpHeaders, + observe?: 'body', + responseType?: 'json', + withCredentials?: boolean, + }): Observable; + post(url: string, body: any|null, options?: { + headers?: HttpHeaders, + observe?: 'body', + responseType?: 'json', + withCredentials?: boolean, + }): Observable; + /** + * Constructs an `Observable` which, when subscribed, will cause the configured + * POST request to be executed on the server. See {@link HttpClient#request} for + * details of `post()`'s return type based on the provided options. + */ + post(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe?: HttpObserve, + responseType?: 'arraybuffer'|'blob'|'json'|'text', + withCredentials?: boolean, + } = {}): Observable { + return this.request('POST', url, addBody(options, body)); + } + + put(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable; + put(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'blob', withCredentials?: boolean, + }): Observable; + put(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe?: 'body', + responseType: 'text', withCredentials?: boolean, + }): Observable; + put(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable>; + put(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'blob', withCredentials?: boolean, + }): Observable>; + put(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'events', + responseType: 'text', withCredentials?: boolean, + }): Observable>; + put(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'events', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + put(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'events', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + put(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'arraybuffer', withCredentials?: boolean, + }): Observable>; + put(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'blob', withCredentials?: boolean, + }): Observable>; + put(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'response', + responseType: 'text', withCredentials?: boolean, + }): Observable>; + put(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'response', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + put(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe: 'response', responseType?: 'json', withCredentials?: boolean, + }): Observable>; + put(url: string, body: any|null, options?: { + headers?: HttpHeaders, + observe?: 'body', + responseType?: 'json', + withCredentials?: boolean, + }): Observable; + put(url: string, body: any|null, options?: { + headers?: HttpHeaders, + observe?: 'body', + responseType?: 'json', + withCredentials?: boolean, + }): Observable; + /** + * Constructs an `Observable` which, when subscribed, will cause the configured + * POST request to be executed on the server. See {@link HttpClient#request} for + * details of `post()`'s return type based on the provided options. + */ + put(url: string, body: any|null, options: { + headers?: HttpHeaders, + observe?: HttpObserve, + responseType?: 'arraybuffer'|'blob'|'json'|'text', + withCredentials?: boolean, + } = {}): Observable { + return this.request('PUT', url, addBody(options, body)); + } +} diff --git a/packages/common/http/src/headers.ts b/packages/common/http/src/headers.ts new file mode 100755 index 0000000000..40a99438a8 --- /dev/null +++ b/packages/common/http/src/headers.ts @@ -0,0 +1,214 @@ +/** + * @license + * Copyright Google Inc. 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'; +} + +/** + * Immutable set of Http headers, with lazy parsing. + * @experimental + */ +export class HttpHeaders { + /** + * Internal map of lowercase header names to values. + */ + private headers: Map; + + + /** + * Internal map of lowercased header names to the normalized + * form of the name (the form seen first). + */ + private normalizedNames: Map = 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; + + constructor(headers?: string|{[name: string]: string | string[]}) { + if (!headers) { + this.headers = new Map(); + } else if (typeof headers === 'string') { + this.lazyInit = () => { + this.headers = new Map(); + 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 { + this.lazyInit = () => { + this.headers = new Map(); + Object.keys(headers).forEach(name => { + let values: string|string[] = headers[name]; + const key = name.toLowerCase(); + if (typeof values === 'string') { + values = [values]; + } + if (values.length > 0) { + this.headers.set(key, values); + this.maybeSetNormalizedName(name, key); + } + }); + }; + } + } + + /** + * Checks for existence of header by given name. + */ + has(name: string): boolean { + this.init(); + + return this.headers.has(name.toLowerCase()); + } + + /** + * Returns first header that matches given name. + */ + get(name: string): string|null { + this.init(); + + const values = this.headers.get(name.toLowerCase()); + return values && values.length > 0 ? values[0] : null; + } + + /** + * Returns the names of the headers + */ + keys(): string[] { + this.init(); + + return Array.from(this.normalizedNames.values()); + } + + /** + * Returns list of header values for a given name. + */ + getAll(name: string): string[]|null { + this.init(); + + return this.headers.get(name.toLowerCase()) || null; + } + + append(name: string, value: string|string[]): HttpHeaders { + return this.clone({name, value, op: 'a'}); + } + + set(name: string, value: string|string[]): HttpHeaders { + return this.clone({name, value, op: 's'}); + } + + 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; + } + } + + /** + * @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) !)); + } +} diff --git a/packages/common/http/src/interceptor.ts b/packages/common/http/src/interceptor.ts new file mode 100644 index 0000000000..d8ba5e8a9f --- /dev/null +++ b/packages/common/http/src/interceptor.ts @@ -0,0 +1,66 @@ +/** + * @license + * Copyright Google Inc. 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 + */ + +import {InjectionToken} from '@angular/core'; +import {Observable} from 'rxjs/Observable'; + +import {HttpHandler} from './backend'; +import {HttpRequest} from './request'; +import {HttpEvent, HttpResponse} from './response'; + +/** + * Intercepts `HttpRequest` and handles them. + * + * Most interceptors will transform the outgoing request before passing it to the + * next interceptor in the chain, by calling `next.handle(transformedReq)`. + * + * In rare cases, interceptors may wish to completely handle a request themselves, + * and not delegate to the remainder of the chain. This behavior is allowed. + * + * @experimental + */ +export interface HttpInterceptor { + /** + * Intercept an outgoing `HttpRequest` and optionally transform it or the + * response. + * + * Typically an interceptor will transform the outgoing request before returning + * `next.handle(transformedReq)`. An interceptor may choose to transform the + * response event stream as well, by applying additional Rx operators on the stream + * returned by `next.handle()`. + * + * More rarely, an interceptor may choose to completely handle the request itself, + * and compose a new event stream instead of invoking `next.handle()`. This is + * acceptable behavior, but keep in mind further interceptors will be skipped entirely. + * + * It is also rare but valid for an interceptor to return multiple responses on the + * event stream for a single request. + */ + intercept(req: HttpRequest, next: HttpHandler): Observable>; +} + +/** + * `HttpHandler` which applies an `HttpInterceptor` to an `HttpRequest`. + * + * @experimental + */ +export class HttpInterceptorHandler implements HttpHandler { + constructor(private next: HttpHandler, private interceptor: HttpInterceptor) {} + + handle(req: HttpRequest): Observable> { + return this.interceptor.intercept(req, this.next); + } +} + +/** + * A multi-provider token which represents the array of `HttpInterceptor`s that + * are registered. + * + * @experimental + */ +export const HTTP_INTERCEPTORS = new InjectionToken('HTTP_INTERCEPTORS'); diff --git a/packages/common/http/src/jsonp.ts b/packages/common/http/src/jsonp.ts new file mode 100644 index 0000000000..aa1a61c008 --- /dev/null +++ b/packages/common/http/src/jsonp.ts @@ -0,0 +1,224 @@ +/** + * @license + * Copyright Google Inc. 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 + */ + +import {DOCUMENT} from '@angular/common'; +import {Inject, Injectable, InjectionToken} from '@angular/core'; +import {Observable} from 'rxjs/Observable'; +import {Observer} from 'rxjs/Observer'; + +import {HttpBackend, HttpHandler} from './backend'; +import {HttpInterceptor} from './interceptor'; +import {HttpRequest} from './request'; +import {HttpErrorResponse, HttpEvent, HttpEventType, HttpResponse} from './response'; + +// Every request made through JSONP needs a callback name that's unique across the +// whole page. Each request is assigned an id and the callback name is constructed +// from that. The next id to be assigned is tracked in a global variable here that +// is shared among all applications on the page. +let nextRequestId: number = 0; + +// Error text given when a JSONP script is injected, but doesn't invoke the callback +// passed in its URL. +export const JSONP_ERR_NO_CALLBACK = 'JSONP injected script did not invoke callback.'; + +// Error text given when a request is passed to the JsonpClientBackend that doesn't +// have a request method JSONP. +export const JSONP_ERR_WRONG_METHOD = 'JSONP requests must use JSONP request method.'; +export const JSONP_ERR_WRONG_RESPONSE_TYPE = 'JSONP requests must use Json response type.'; + +/** + * DI token/abstract type representing a map of JSONP callbacks. + * + * In the browser, this should always be the `window` object. + * + * @experimental + */ +export abstract class JsonpCallbackContext { [key: string]: (data: any) => void; } + +/** + * `HttpBackend` that only processes `HttpRequest` with the JSONP method, + * by performing JSONP style requests. + * + * @experimental + */ +@Injectable() +export class JsonpClientBackend implements HttpBackend { + constructor(private callbackMap: JsonpCallbackContext, @Inject(DOCUMENT) private document: any) {} + + /** + * Get the name of the next callback method, by incrementing the global `nextRequestId`. + */ + private nextCallback(): string { return `ng_jsonp_callback_${nextRequestId++}`; } + + /** + * Process a JSONP request and return an event stream of the results. + */ + handle(req: HttpRequest): Observable> { + // Firstly, check both the method and response type. If either doesn't match + // then the request was improperly routed here and cannot be handled. + if (req.method !== 'JSONP') { + throw new Error(JSONP_ERR_WRONG_METHOD); + } else if (req.responseType !== 'json') { + throw new Error(JSONP_ERR_WRONG_RESPONSE_TYPE); + } + + // Everything else happens inside the Observable boundary. + return new Observable>((observer: Observer>) => { + // The first step to make a request is to generate the callback name, and replace the + // callback placeholder in the URL with the name. Care has to be taken here to ensure + // a trailing &, if matched, gets inserted back into the URL in the correct place. + const callback = this.nextCallback(); + const url = req.url.replace(/=JSONP_CALLBACK(&|$)/, `=${callback}$1`); + + // Construct the