diff --git a/aio/content/guide/http.md b/aio/content/guide/http.md index 55d6f25343..3c3a51885f 100644 --- a/aio/content/guide/http.md +++ b/aio/content/guide/http.md @@ -1,829 +1,614 @@ -# HTTP +# HttpClient -[HTTP](https://tools.ietf.org/html/rfc2616) is the primary protocol for browser/server communication. +Most front-end applications communicate with backend services over the HTTP protocol. Modern browsers support two different APIs for making HTTP requests: the `XMLHttpRequest` interface and the `fetch()` API. + +With `HttpClient`, `@angular/common/http` provides a simplified API for HTTP functionality for use with Angular applications, building on top of the `XMLHttpRequest` interface exposed by browsers. +Additional benefits of `HttpClient` include testability support, strong typing of request and response objects, request and response interceptor support, and better error handling via apis based on Observables. -
+## Setup: installing the module - The [`WebSocket`](https://tools.ietf.org/html/rfc6455) protocol is another important communication technology; - it isn't covered in this page. +Before you can use the `HttpClient`, you need to install the `HttpClientModule` which provides it. This can be done in your application module, and is only necessary once. -
+```javascript +// app.module.ts: -Modern browsers support two HTTP-based APIs: -[XMLHttpRequest (XHR)](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) and -[JSONP](https://en.wikipedia.org/wiki/JSONP). A few browsers also support -[Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). +import {NgModule} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; -The Angular HTTP library simplifies application programming with the **XHR** and **JSONP** APIs. +// Import HttpClientModule from @angular/common/http +import {HttpClientModule} from '@angular/common/http'; -A live example illustrates these topics. +@NgModule({ + imports: [ + BrowserModule, + // Include it under 'imports' in your application module + // after BrowserModule. + HttpClientModule, + ], +}) +export class MyAppModule {} +``` -{@a demos} +Once you import `HttpClientModule` into your app module, you can inject `HttpClient` +into your components and services. -## Demos +## Making a request for JSON data -This page describes server communication with the help of the following demos: +The most common type of request applications make to a backend is to request JSON data. For example, suppose you have an API endpoint that lists items, `/api/items`, which returns a JSON object of the form: -* [The Tour of Heroes *HTTP* client demo](guide/http#http-client). -* [Fall back to Promises](guide/http#promises). -* [Cross-Origin Requests: Wikipedia example](guide/http#cors). -* [More fun with Observables](guide/http#more-observables). +```json +{ + "results": [ + "Item 1", + "Item 2", + ] +} +``` -The root `AppComponent` orchestrates these demos: +The `get()` method on `HttpClient` makes accessing this data straightforward. - -{@a http-providers} +```javascript +@Component(...) +export class MyComponent implements NgOnInit { + // Inject HttpClient into your component or service. + constructor(private http: HttpClient) {} + + ngOnInit(): void { + // Make the HTTP request: + this.http.get('/api/items').subscribe(data => { + // Read the result field from the JSON response. + this.results = data['results']; + }); + } +} +``` -## Providing HTTP services -First, configure the application to use server communication facilities. +### Typechecking the response -The Angular Http client communicates with the server using a familiar HTTP request/response protocol. -The `Http` client is one of a family of services in the Angular HTTP library. +In the above example, the `data['results']` field access stands out because you use bracket notation to access the results field. If you tried to write `data.results`, TypeScript would correctly complain that the `Object` coming back from HTTP does not have a `results` property. That's because while `HttpClient` parsed the JSON response into an `Object`, it doesn't know what shape that object is. +You can, however, tell `HttpClient` what type the response will be, which is recommended. +To do so, first you define an interface with the correct shape: -
+```javascript +interface DevelopersResponse { + results: string[]; +} +``` - When importing from the `@angular/http` module, SystemJS knows how to load services from - the Angular HTTP library - because the `systemjs.config.js` file maps to that module name. +Then, when you make the `HttpClient.get` call, pass a type parameter: -
+```javascript +http.get('/api/items').subscribe(data => { + // data is now an instance of type ItemsResponse, so you can do this: + this.results = data.results; +}); +``` +### Reading the full response -Before you can use the `Http` client, you need to register it as a service provider with the dependency injection system. +The response body doesn't return all the data you may need. Sometimes servers return special headers or status codes to indicate certain conditions, and inspecting those can be necessary. To do this, you can tell `HttpClient` you want the full response instead of just the body with the `observe` option: +```javascript +http + .get('/data.json', {observe: 'response'}) + .subscribe(resp => { + // Here, resp is of type HttpResponse. + // You can inspect its headers: + console.log(resp.headers.get('X-Custom-Header')); + // And access the body directly, which is typed as MyJsonData as requested. + console.log(resp.body.someField); + }); +``` -
+As you can see, the resulting object has a `body` property of the correct type. - Read about providers in the [Dependency Injection](guide/dependency-injection) page. -
+### Error handling -Register providers by importing other NgModules to the root NgModule in `app.module.ts`. +What happens if the request fails on the server, or if a poor network connection prevents it from even reaching the server? `HttpClient` will return an _error_ instead of a successful response. - +To handle it, add an error handler to your `.subscribe()` call: + +```javascript +http + .get('/api/items') + .subscribe( + // Successful responses call the first callback. + data => {...}, + // Errors will call this callback instead: + err => { + console.log('Something went wrong!'); + } + }); +``` -Begin by importing the necessary members. -The newcomers are the `HttpModule` and the `JsonpModule` from the Angular HTTP library. For more information about imports and related terminology, see the [MDN reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) on the `import` statement. +#### Getting error details -To add these modules to the application, pass them to the `imports` array in the root `@NgModule`. +Detecting that an error occurred is one thing, but it's more useful to know what error actually occurred. The `err` parameter to the callback above is of type `HttpErrorResponse`, and contains useful information on what went wrong. +There are two types of errors that can occur. If the backend returns an unsuccessful response code (404, 500, etc.), it gets returned as an error. Also, if something goes wrong client-side, such as an exception gets thrown in an RxJS operator, or if a network error prevents the request from completing successfully, an actual `Error` will be thrown. -
+In both cases, you can look at the `HttpErrorResponse` to figure out what happened. - The `HttpModule` is necessary for making HTTP calls. - Though the `JsonpModule` isn't necessary for plain HTTP, - there is a JSONP demo later in this page. - Loading its module now saves time. +```javascript +http + .get('/api/items') + .subscribe( + data => {...}, + (err: HttpErrorResponse) => { + if (err.error instanceof Error) { + // A client-side or network error occurred. Handle it accordingly. + console.log('An error occurred:', err.error.message); + } else { + // The backend returned an unsuccessful response code. + // The response body may contain clues as to what went wrong, + console.log(`Backend returned code ${err.status}, body was: ${err.error}`); + } + } + }); +``` -
+#### `.retry()` +One way to deal with errors is to simply retry the request. This strategy can be useful when the errors are transient and unlikely to repeat. -{@a http-client} +RxJS has a useful operator called `.retry()`, which automatically resubscribes to an Observable, thus reissuing the request, upon encountering an error. -## The Tour of Heroes HTTP client demo +First, import it: -The first demo is a mini-version of the [tutorial](tutorial)'s "Tour of Heroes" (ToH) application. -This version gets some heroes from the server, displays them in a list, lets the user add new heroes, and saves them to the server. -The app uses the Angular Http client to communicate via **XMLHttpRequest (XHR)**. +```js +import 'rxjs/add/operator/retry'; +``` -It works like this: +Then, you can use it with HTTP Observables like this: -
- ToH mini app -
+```javascript +http + .get('/api/items') + // Retry this request up to 3 times. + .retry(3) + // Any errors after the 3rd retry will fall through to the app. + .subscribe(...); +``` -This demo has a single component, the `HeroListComponent`. Here's its template: +### Requesting non-JSON data - +Not all APIs return JSON data. Suppose you want to read a text file on the server. You have to tell `HttpClient` that you expect a textual response: -It presents the list of heroes with an `ngFor`. -Below the list is an input box and an *Add Hero* button where you can enter the names of new heroes -and add them to the database. -A [template reference variable](guide/template-syntax#ref-vars), `newHeroName`, accesses the -value of the input box in the `(click)` event binding. -When the user clicks the button, that value is passed to the component's `addHero` method and then -the event binding clears it to make it ready for a new hero name. +```javascript +http + .get('/textfile.txt', {responseType: 'text'}) + // The Observable returned by get() is of type Observable + // because a text response was specified. There's no need to pass + // a type parameter to get(). + .subscribe(data => console.log(data)); +``` -Below the button is an area for an error message. +## Sending data to the server -{@a oninit} -{@a HeroListComponent} +In addition to fetching data from the server, `HttpClient` supports mutating requests, that is, sending data to the server in various forms. -### The *HeroListComponent* class +### Making a POST request -Here's the component class: - - - -Angular [injects](guide/dependency-injection) a `HeroService` into the constructor -and the component calls that service to fetch and save data. - -The component **does not talk directly to the Angular Http client**. -The component doesn't know or care how it gets the data. -It delegates to the `HeroService`. - -This is a golden rule: **always delegate data access to a supporting service class**. - -Although _at runtime_ the component requests heroes immediately after creation, -you **don't** call the service's `get` method in the component's constructor. -Instead, call it inside the `ngOnInit` [lifecycle hook](guide/lifecycle-hooks) -and rely on Angular to call `ngOnInit` when it instantiates this component. - - -
- - This is a *best practice*. - Components are easier to test and debug when their constructors are simple, and all real work - (especially calling a remote server) is handled in a separate method. - -
- - -The service's `getHeroes()` and `create()` methods return an `Observable` of hero data that the Angular Http client fetched from the server. - -Think of an `Observable` as a stream of events published by some source. -To listen for events in this stream, ***subscribe*** to the `Observable`. -These subscriptions specify the actions to take when the web request -produces a success event (with the hero data in the event payload) or a fail event (with the error in the payload). - -With a basic understanding of the component, you're ready to look inside the `HeroService`. - -{@a HeroService} -{@a fetch-data} - -## Fetch data with _http.get()_ - -In many of the previous samples the app faked the interaction with the server by -returning mock heroes in a service like this one: - - - -You can revise that `HeroService` to get the heroes from the server using the Angular Http client service: - - - -Notice that the Angular Http client service is -[injected](guide/dependency-injection) into the `HeroService` constructor. - - - -Look closely at how to call `http.get`: - - - -You pass the resource URL to `get` and it calls the server which returns heroes. - - -
- - The server returns heroes once you've set up the [in-memory web api](guide/http#in-mem-web-api) - described in the appendix below. - Alternatively, you can temporarily target a JSON file by changing the endpoint URL: - - - -
- - -{@a rxjs} -If you are familiar with asynchronous methods in modern JavaScript, you might expect the `get` method to return a -promise. -You'd expect to chain a call to `then()` and extract the heroes. -Instead you're calling a `map()` method. -Clearly this is not a promise. - -In fact, the `http.get` method returns an **Observable** of HTTP Responses (`Observable`) from the RxJS library -and `map()` is one of the RxJS *operators*. - -{@a rxjs-library} - -## RxJS library - -RxJS -is a third party library, endorsed by Angular, that implements the -asynchronous Observable pattern. - -All of the Developer Guide samples have installed the RxJS npm package -because Observables are used widely in Angular applications. -_This_ app needs it when working with the HTTP client. -But you must take a critical extra step to make RxJS Observables usable: -_you must import the RxJS operators individually_. - -### Enable RxJS operators - -The RxJS library is large. -Size matters when building a production application and deploying it to mobile devices. -You should include only necessary features. - -Each code file should add the operators it needs by importing from an RxJS library. -The `getHeroes()` method needs the `map()` and `catch()` operators so it imports them like this. - - - -{@a extract-data} - -## Process the response object - -Remember that the `getHeroes()` method used an `extractData()` helper method to map the `http.get` response object to heroes: - - - -The `response` object doesn't hold the data in a form the app can use directly. -You must parse the response data into a JSON object. - -{@a parse-to-json} - -### Parse to JSON - -The response data are in JSON string form. -The app must parse that string into JavaScript objects by calling `response.json()`. - - -
- - This is not Angular's own design. - The Angular HTTP client follows the Fetch specification for the - [response object](https://fetch.spec.whatwg.org/#response-class) returned by the `Fetch` function. - That spec defines a `json()` method that parses the response body into a JavaScript object. - -
- - -
- - Don't expect the decoded JSON to be the heroes array directly. - This server always wraps JSON results in an object with a `data` - property. You have to unwrap it to get the heroes. - This is conventional web API behavior, driven by - [security concerns](https://www.owasp.org/index.php/OWASP_AJAX_Security_Guidelines#Always_return_JSON_with_an_Object_on_the_outside). - -
+One common operation is to POST data to a server; for example when submitting a form. The code for +sending a POST request is very similar to the code for GET: +```javascript +const body = {name: 'Brad'}; +http + .post('/api/developers/add', body) + // See below - subscribe() is still necessary when using post(). + .subscribe(...); +```
- Make no assumptions about the server API. - Not all servers return an object with a `data` property. +*Note the `subscribe()` method.* All Observables returned from `HttpClient` are _cold_, which is to say that they are _blueprints_ for making requests. Nothing will happen until you call `subscribe()`, and every such call will make a separate request. For example, this code sends a POST request with the same data twice: +```javascript +const req = http.post('/api/items/add', body); +// 0 requests made - .subscribe() not called. +req.subscribe(); +// 1 request made. +req.subscribe(); +// 2 requests made. +```
+### Configuring other parts of the request -{@a no-return-response-object} +Besides the URL and a possible request body, there are other aspects of an outgoing request which you may wish to configure. All of these are available via an options object, which you pass to the request. -### Do not return the response object +#### Headers -The `getHeroes()` method _could_ have returned the HTTP response but this wouldn't -follow best practices. -The point of a data service is to hide the server interaction details from consumers. -The component that calls the `HeroService` only wants heroes and is kept separate -from getting them, the code dealing with where they come from, and the response object. +One common task is adding an `Authorization` header to outgoing requests. Here's how you do that: -
-
HTTP GET is delayed
+```javascript +http + .post('/api/items/add', body, { + headers: new HttpHeaders().set('Authorization', 'my-auth-token'), + }) + .subscribe(); +``` - The `http.get` does **not send the request just yet.** This Observable is - [*cold*](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/creating.md#cold-vs-hot-observables), - which means that the request won't go out until something *subscribes* to the Observable. - That *something* is the [HeroListComponent](guide/http#subscribe). +The `HttpHeaders` class is immutable, so every `set()` returns a new instance and applies the changes. -
+#### URL Parameters +Adding URL parameters works in the same way. To send a request with the `id` parameter set to `3`, you would do: -{@a error-handling} +```javascript +http + .post('/api/items/add', body, { + params: new HttpParams().set('id', '3'), + }) + .subscribe(); +``` -### Always handle errors +In this way, you send the POST request to the URL `/api/items/add?id=3`. -An important part of dealing with I/O is anticipating errors by preparing to catch them -and do something with them. One way to handle errors is to pass an error message -back to the component for presentation to the user, -but only if it says something that the user can understand and act upon. +## Advanced usage -This simple app conveys that idea, albeit imperfectly, in the way it handles a `getHeroes` error. +The above sections detail how to use the basic HTTP functionality in `@angular/common/http`, but sometimes you need to do more than just make requests and get data back. - -The `catch()` operator passes the error object from `http` to the `handleError()` method. -The `handleError` method transforms the error into a developer-friendly message, -logs it to the console, and returns the message in a new, failed Observable via `Observable.throw`. +### Intercepting all requests or responses +A major feature of `@angular/common/http` is _interception_, the ability to declare interceptors which sit in between your application and the backend. When your application makes a request, interceptors transform it +before sending it to the server, and the interceptors can transform the response on its way back before your application sees it. This is useful for everything from authentication to logging. -{@a subscribe} -{@a hero-list-component} +#### Writing an interceptor -### **HeroListComponent** error handling +To implement an interceptor, you declare a class that implements `HttpInterceptor`, which +has a single `intercept()` method. Here is a simple interceptor which does nothing but forward the request through without altering it: -Back in the `HeroListComponent`, in `heroService.getHeroes()`, -the `subscribe` function has a second function parameter to handle the error message. -It sets an `errorMessage` variable that's bound conditionally in the `HeroListComponent` template. +```javascript +import {Injectable} from '@angular/core'; +import {HttpEvent, HttpInterceptor, HttpHandler, HttpRequest) from '@angular/common/http'; - +@Injectable() +export class NoopInterceptor implements HttpInterceptor { + intercept(req: HttpRequest, next: HttpHandler): Observable> { + return next.handle(Req); + } +} +``` +`intercept` is a method which transforms a request into an Observable that eventually returns the response. In this sense, each interceptor is entirely responsible for handling the request by itself. -
+Most of the time, though, interceptors will make some minor change to the request and forward it to the rest of the chain. That's where the `next` parameter comes in. `next` is an `HttpHandler`, an interface that, similar to `intercept`, transforms a request into an Observable for the response. In an interceptor, `next` always represents the next interceptor in the chain, if any, or the final backend if there are no more interceptors. So most interceptors will end by calling `next` on the request they transformed. - Want to see it fail? In the `HeroService`, reset the api endpoint to a bad value. Afterward, remember to restore it. +Our do-nothing handler simply calls `next.handle` on the original request, forwarding it without mutating it at all. -
+This pattern is similar to those in middleware frameworks such as Express.js. -{@a create} -{@a update} -{@a post} +##### Providing your interceptor -## Send data to the server +Simply declaring the `NoopInterceptor` above doesn't cause your app to use it. You need to wire it up in your app module by providing it as an interceptor, as follows: -So far you've seen how to retrieve data from a remote location using an HTTP service. -Now you'll add the ability to create new heroes and save them in the backend. +```javascript +import {NgModule} from '@angular/core'; +import {HTTP_INTERCEPTORS} from '@angular/common/http'; -You'll write a method for the `HeroListComponent` to call, a `create()` method, that takes -just the name of a new hero and returns an `Observable` of `Hero`. It begins like this: +@NgModule({ + providers: [{ + provide: HTTP_INTERCEPTORS, + useClass: NoopInterceptor, + multi: true, + }], +}) +export class AppModule {} +``` - +Note the `multi: true` option. This is required and tells Angular that `HTTP_INTERCEPTORS` is an array of values, rather than a single value. -To implement it, you must know the server's API for creating heroes. -[This sample's data server](guide/http#in-mem-web-api) follows typical REST guidelines. -It expects a [`POST`](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5) request -at the same endpoint as `GET` heroes. -It expects the new hero data to arrive in the body of the request, -structured like a `Hero` entity but without the `id` property. -The body of the request should look like this: +##### Events - - { "name": "Windstorm" } - +You may have also noticed that the Observable returned by `intercept` and `HttpHandler.handle` is not an `Observable>` but an `Observable>`. That's because interceptors work at a lower level than the `HttpClient` interface. A single request can generate multiple events, including upload and download progress events. The `HttpResponse` class is actually an event itself, with a `type` of `HttpEventType.HttpResponseEvent`. -The server generates the `id` and returns the entire `JSON` representation -of the new hero including its generated id. The hero arrives tucked inside a response object -with its own `data` property. +An interceptor must pass through all events that it does not understand or intend to modify. It must not filter out events it didn't expect to process. Many interceptors are only concerned with the outgoing request, though, and will simply return the event stream from `next` without modifying it. -Now that you know how the API works, implement `create()` as follows: - +##### Ordering - +When you provide multiple interceptors in an application, Angular applies them in the order that you +provided them. -{@a headers} +##### Immutability -### Headers +Interceptors exist to examine and mutate outgoing requests and incoming responses. However, it may be surprising to learn that the `HttpRequest` and `HttpResponse` classes are largely immutable. -In the `headers` object, the `Content-Type` specifies that the body represents JSON. +This is for a reason: because the app may retry requests, the interceptor chain may process an individual request multiple times. If requests were mutable, a retried request would be different than the original request. Immutability ensures the interceptors see the same request for each try. -Next, the `headers` object is used to configure the `options` object. The `options` -object is a new instance of `RequestOptions`, a class that allows you to specify -certain settings when instantiating a request. In this way, [headers](api/http/Headers) is one of the [RequestOptions](api/http/RequestOptions). +There is one case where type safety cannot protect you when writing interceptors—the request body. It is invalid to mutate a request body within an interceptor, but this is not checked by the type system. -In the `return` statement, `options` is the *third* argument of the `post()` method, as shown above. +If you have a need to mutate the request body, you need to copy the request body, mutate the copy, and then use `clone()` to copy the request and set the new body. +Since requests are immutable, they cannot be modified directly. To mutate them, use `clone()`: -{@a json-results} +```javascript +intercept(req: HttpRequest, next: HttpHandler): Observable> { + // This is a duplicate. It is exactly the same as the original. + const dupReq = req.clone(); + + // Change the URL and replace 'http://' with 'https://' + const secureReq = req.clone({url: req.url.replace('http://', 'https://')}); +} +``` -### JSON results +As you can see, the hash accepted by `clone()` allows you to mutate specific properties of the request while copying the others. -As with `getHeroes()`, use the `extractData()` helper to [extract the data](guide/http#extract-data) -from the response. +#### Setting new headers -Back in the `HeroListComponent`, its `addHero()` method subscribes to the Observable returned by the service's `create()` method. -When the data arrive it pushes the new hero object into its `heroes` array for presentation to the user. +A common use of interceptors is to set default headers on outgoing responses. For example, assuming you have an injectable `AuthService` which can provide an authentication token, here is how you would write an interceptor which adds it to all outgoing requests: - +```javascript +import {Injectable} from '@angular/core'; +import {HttpEvent, HttpInterceptor, HttpHandler, HttpRequest) from '@angular/common/http'; -{@a promises} +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + constructor(private auth: AuthService) {} + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + // Get the auth header from the service. + const authHeader: this.auth.getAuthorizationHeader(); + // Clone the request to add the new header. + const authReq = req.clone({headers: req.headers.set('Authorization', authHeader)}); + // Pass on the cloned request instead of the original request. + return next.handle(authReq); + } +} +``` + +The practice of cloning a request to set new headers is so common that there's actually a shortcut for it: + +```javascript +const authReq = req.clone({setHeaders: {Authorization: authHeader}}); +``` + +An interceptor that alters headers can be used for a number of different operations, including: + +* Authentication/authorization +* Caching behavior; for example, If-Modified-Since +* XSRF protection + +#### Logging + +Because interceptors can process the request and response _together_, they can do things like log or time requests. Consider this interceptor which uses `console.log` to show how long each request takes: + +```javascript +import 'rxjs/add/operator/do'; + +export class TimingInterceptor implements HttpInterceptor { + constructor(private auth: AuthService) {} + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + const elapsed = Date.now(); + return next + .handle(req) + .do(event => { + if (event instanceof HttpResponse) { + const time = Date.now() - started; + console.log(`Request for ${req.urlWithParams} took ${elapsed} ms.`); + } + }); + } +} +``` +Notice the RxJS `do()` operator—it adds a side effect to an Observable without affecting the values on the stream. Here, it detects the `HttpResponse` event and logs the time the request took. + +#### Caching + +You can also use interceptors to implement caching. For this example, assume that you've written an HTTP cache with a simple interface: + +```javascript +abstract class HttpCache { + /** + * Returns a cached response, if any, or null if not present. + */ + abstract get(req: HttpRequest): HttpResponse|null; + + /** + * Adds or updates the response in the cache. + */ + abstract put(req: HttpRequest, resp: HttpResponse): void; +} +``` + +An interceptor can apply this cache to outgoing requests. + +```javascript +@Injectable() +export class CachingInterceptor implements HttpInterceptor { + constructor(private cache: HttpCache) {} + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + // Before doing anything, it's important to only cache GET requests. + // Skip this interceptor if the request method isn't GET. + if (req.method !== 'GET') { + return next.handle(req); + } + + // First, check the cache to see if this request exists. + const cachedResponse = this.cache.get(req); + if (cachedResponse) { + // A cached response exists. Serve it instead of forwarding + // the request to the next handler. + return Observable.of(cachedResponse); + } + + // No cached response exists. Go to the network, and cache + // the response when it arrives. + return next.handle(req).do(event => { + // Remember, there may be other events besides just the response. + if (event instanceof HttpResponse) { + // Update the cache. + this.cache.put(req, event); + } + }); + } +} +``` + +Obviously this example glosses over request matching, cache invalidation, etc., but it's easy to see that interceptors have a lot of power beyond just transforming requests. If desired, they can be used to completely take over the request flow. + +To really demonstrate their flexibility, you can change the above example to return _two_ response events if the request exists in cache—the cached response first, and an updated network response later. + +```javascript +intercept(req: HttpRequest, next: HttpHandler): Observable> { + // Still skip non-GET requests. + if (req.method !== 'GET') { + return next.handle(req); + } + + // This will be an Observable of the cached value if there is one, + // or an empty Observable otherwise. It starts out empty. + let maybeCachedResponse: Observable> = Observable.empty(); + + // Check the cache. + const cachedResponse = this.cache.get(req); + if (cachedResponse) { + maybeCachedResponse = Observable.of(cachedResponse); + } + + // Create an Observable (but don't subscribe) that represents making + // the network request and caching the value. + const networkResponse = next.handle(req).do(event => { + // Just like before, check for the HttpResponse event and cache it. + if (event instanceof HttpResponse) { + this.cache.put(req, event); + } + }); + + // Now, combine the two and send the cached response first (if there is + // one), and the network response second. + return Observable.concat(maybeCachedResponse, networkResponse); +} +``` + +Now anyone doing `http.get(url)` will receive _two_ responses if that URL has been cached before. + +### Listening to progress events + +Sometimes applications need to transfer large amounts of data, and those transfers can take time. It's a good user experience practice to provide feedback on the progress of such transfers; for example, uploading files—and `@angular/common/http` supports this. + +To make a request with progress events enabled, first create an instance of `HttpRequest` with the special `reportProgress` option set: + +```javascript +const req = new HttpRequest('POST', '/upload/file', file, { + reportProgress: true, +}); +``` + +This option enables tracking of progress events. Remember, every progress event triggers +change detection, so only turn them on if you intend to actually update the UI on each event. + +Next, make the request through the `request()` method of `HttpClient`. The result will be an Observable of events, just like with interceptors: + +```javascript +http.request(req).subscribe(event => { + // Via this API, you get access to the raw event stream. + // Look for upload progress events. + if (event.type === HttpEventType.UploadProgress) { + // This is an upload progress event. Compute and show the % done: + const percentDone = Math.round(100 * event.loaded / event.total); + console.log(`File is ${percentDone}% uploaded.`); + } else if (event instanceof HttpResponse) { + console.log('File is completely uploaded!'); + } +}); +``` + +## Testing HTTP requests + +Like any external dependency, the HTTP backend needs to be mocked as part of good testing practice. `@angular/common/http` provides a testing library `@angular/common/http/testing` that makes setting up such mocking straightforward. + +### Mocking philosophy + +Angular's HTTP testing library is designed for a pattern of testing where the app executes code and makes requests first. After that, tests expect that certain requests have or have not been made, perform assertions against those requests, and finally provide responses by "flushing" each expected request, which may trigger more new requests, etc. At the end, tests can optionally verify that the app has made no unexpected requests. + +### Setup + +To begin testing requests made through `HttpClient`, import `HttpClientTestingModule` and add it to your `TestBed` setup, like so: + +```javascript + +import {HttpClientTestingModule} from '@angular/common/http/testing'; + +beforeEach(() => { + TestBed.configureTestingModule({ + ..., + imports: [ + HttpClientTestingModule, + ], + }) +}); +``` + +That's it. Now requests made in the course of your tests will hit the testing backend instead of the normal backend. + +### Expecting and answering requests + +With the mock installed via the module, you can write a test that expects a GET Request to occur and provides a mock response. The following example does this by injecting both the `HttpClient` into the test and a class called `HttpTestingController` + +```javascript +it('expects a GET request', inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => { + // Make an HTTP GET request, and expect that it return an object + // of the form {name: 'Test Data'}. + http + .get('/data') + .subscribe(data => expect(data['name']).toEqual('Test Data')); + + // At this point, the request is pending, and no response has been + // sent. The next step is to expect that the request happened. + const req = httpMock.expectOne('/test'); + + // If no request with that URL was made, or if multiple requests match, + // expectOne() would throw. However this test makes only one request to + // this URL, so it will match and return a mock request. The mock request + // can be used to deliver a response or make assertions against the + // request. In this case, the test asserts that the request is a GET. + expect(req.request.method).toEqual('GET'); + + // Next, fulfill the request by transmitting a response. + req.flush({name: 'Test Data'}); + + // Finally, assert that there are no outstanding requests. + mockHttp.verify(); +})); +``` + +The last step, verifying that no requests remain outstanding, is common enough for you to move it into an `afterEach()` step: + +```javascript +afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { + mockHttp.verify(); +})); +``` -## Fall back to promises +#### Custom request expectations -Although the Angular `http` client API returns an `Observable` you can turn it into a -[`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). -It's easy to do, and in simple cases, a Promise-based version looks much -like the Observable-based version. - - -
- - While Promises may be more familiar, Observables have many advantages. - -
- -First, make sure to import the `toPromise` operator of the RxJS library. - - - -Here is a comparison of the `HeroService` using Promises versus Observables, -highlighting just the parts that are different. - - - - - - -You can follow the Promise `then(this.extractData).catch(this.handleError)` pattern as in -this example. - -Alternatively, you can call `toPromise(success, fail)`. The Observable's `map` callback moves to the -first *success* parameter and its `catch` callback to the second *fail* parameter -in this pattern: `.toPromise(this.extractData, this.handleError)`. - -The `errorHandler` forwards an error message as a failed `Promise` instead of a failed `Observable`. - -The diagnostic *log to console* is just one more `then()` in the Promise chain. - -You have to adjust the calling component to expect a `Promise` instead of an `Observable`: - - - - - - -The only obvious difference is that you call `then()` on the returned Promise instead of `subscribe`. -Both methods take the same functional arguments. - - -
- - The less obvious but critical difference is that these two methods return very different results. - - The Promise-based `then()` returns another Promise. You can keep chaining - more `then()` and `catch()` calls, getting a new promise each time. - - The `subscribe()` method returns a `Subscription`. A `Subscription` is not another `Observable`. - It's the end of the line for Observables. You can't call `map()` on it or call `subscribe()` again. - The `Subscription` object has a different purpose, signified by its primary method, `unsubscribe`. - - To understand the implications and consequences of subscriptions, - watch [Ben Lesh's talk on Observables](https://www.youtube.com/watch?v=3LKMwkuK0ZE) - or his video course on [egghead.io](https://egghead.io/lessons/rxjs-rxjs-observables-vs-promises). - -
- - -{@a cors} - -## Cross-Origin Requests: Wikipedia example - -You just learned how to make `XMLHttpRequests` using the Angular Http service. -This is the most common approach to server communication, but it doesn't work in all scenarios. - -For security reasons, web browsers block `XHR` calls to a remote server whose origin is different from the origin of the web page. -The *origin* is the combination of URI scheme, hostname, and port number. -This is called the [same-origin policy](https://en.wikipedia.org/wiki/Same-origin_policy). - - -
- - Modern browsers do allow `XHR` requests to servers from a different origin if the server supports the - [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) protocol. - If the server requires user credentials, enable them in the [request headers](guide/http#headers). - -
- - -Some servers do not support CORS but do support an older, read-only alternative called [JSONP](https://en.wikipedia.org/wiki/JSONP). -Wikipedia is one such server. - - -
- - This [Stack Overflow answer](http://stackoverflow.com/questions/2067472/what-is-jsonp-all-about/2067584#2067584) covers many details of JSONP. - -
- - -{@a search-wikipedia} - -### Search Wikipedia - -Here is a simple search that shows suggestions from Wikipedia as the user -types in a text box: - -
- Wikipedia search app (v.1) -
- -Wikipedia offers a modern `CORS` API and a legacy `JSONP` search API. This example uses the latter. -The Angular `Jsonp` service both extends the `Http` service for JSONP and restricts you to `GET` requests. -All other HTTP methods throw an error because `JSONP` is a read-only facility. - -As always, wrap the interaction with an Angular data access client service inside a dedicated service, here called `WikipediaService`. - - - -The constructor expects Angular to inject its `Jsonp` service, which -is available because `JsonpModule` is in the root `@NgModule` `imports` array -in `app.module.ts`. - -{@a query-parameters} - -### Search parameters - -The [Wikipedia "opensearch" API](https://www.mediawiki.org/wiki/API:Opensearch) -expects four parameters (key/value pairs) to arrive in the request URL's query string. -The keys are `search`, `action`, `format`, and `callback`. -The value of the `search` key is the user-supplied search term to find in Wikipedia. -The other three are the fixed values "opensearch", "json", and "JSONP_CALLBACK" respectively. - - -
- - The `JSONP` technique requires that you pass a callback function name to the server in the query string: `callback=JSONP_CALLBACK`. - The server uses that name to build a JavaScript wrapper function in its response, which Angular ultimately calls to extract the data. - All of this happens under the hood. - -
- - -If you're looking for articles with the word "Angular", you could construct the query string by hand and call `jsonp` like this: - - - -In more parameterized examples you could build the query string with the Angular `URLSearchParams` helper: - - - -This time you call `jsonp` with *two* arguments: the `wikiUrl` and an options object whose `search` property is the `params` object. - - - -`Jsonp` flattens the `params` object into the same query string you saw earlier, sending the request -to the server. - -{@a wikicomponent} - -### The WikiComponent - -Now that you have a service that can query the Wikipedia API, -turn your attention to the component (template and class) that takes user input and displays search results. - - - -The template presents an `` element *search box* to gather search terms from the user, -and calls a `search(term)` method after each `keyup` event. - -The component's `search(term)` method delegates to the `WikipediaService`, which returns an -Observable array of string results (`Observable`). -Instead of subscribing to the Observable inside the component, as in the `HeroListComponent`, -the app forwards the Observable result to the template (via `items`) where the `async` pipe -in the `ngFor` handles the subscription. Read more about [async pipes](guide/pipes#async-pipe) -in the [Pipes](guide/pipes) page. - - -
- - The [async pipe](guide/pipes#async-pipe) is a good choice in read-only components - where the component has no need to interact with the data. - - `HeroListComponent` can't use the pipe because `addHero()` pushes newly created heroes into the list. - -
- - -{@a wasteful-app} - -## A wasteful app - -The Wikipedia search makes too many calls to the server. -It is inefficient and potentially expensive on mobile devices with limited data plans. - -### 1. Wait for the user to stop typing - -Presently, the code calls the server after every keystroke. -It should only make requests when the user *stops typing*. -Here's how it will work after refactoring: - -
- Wikipedia search app (v.2) -
- -### 2. Search when the search term changes - -Suppose a user enters the word *angular* in the search box and pauses for a while. -The application issues a search request for *angular*. - -Then the user backspaces over the last three letters, *lar*, and immediately re-types *lar* before pausing once more. -The search term is still _angular_. The app shouldn't make another request. - -### 3. Cope with out-of-order responses - -The user enters *angular*, pauses, clears the search box, and enters *http*. -The application issues two search requests, one for *angular* and one for *http*. - -Which response arrives first? It's unpredictable. -When there are multiple requests in-flight, the app should present the responses -in the original request order. -In this example, the app must always display the results for the *http* search -no matter which response arrives first. - -{@a more-observables} - -## More fun with Observables - -You could make changes to the `WikipediaService`, but for a better -user experience, create a copy of the `WikiComponent` instead and make it smarter, -with the help of some nifty Observable operators. - -Here's the `WikiSmartComponent`, shown next to the original `WikiComponent`: - - - - - - -While the templates are virtually identical, -there's a lot more RxJS in the "smart" version, -starting with `debounceTime`, `distinctUntilChanged`, and `switchMap` operators, -imported as [described above](guide/http#rxjs-library). - -{@a create-stream} - -### Create a stream of search terms - -The `WikiComponent` passes a new search term directly to the `WikipediaService` after every keystroke. - -The `WikiSmartComponent` class turns the user's keystrokes into an Observable _stream of search terms_ -with the help of a `Subject`, which you import from RxJS: - - - -The component creates a `searchTermStream` as a `Subject` of type `string`. -The `search()` method adds each new search box value to that stream via the subject's `next()` method. - - - -{@a listen-for-search-terms} - -### Listen for search terms - -The `WikiSmartComponent` listens to the *stream of search terms* and -processes that stream _before_ calling the service. - - - -* debounceTime -waits for the user to stop typing for at least 300 milliseconds. - -* distinctUntilChanged -ensures that the service is called only when the new search term is different from the previous search term. - -* The switchMap -calls the `WikipediaService` with a fresh, debounced search term and coordinates the stream(s) of service response. - -The role of `switchMap` is particularly important. -The `WikipediaService` returns a separate Observable of string arrays (`Observable`) for each search request. -The user could issue multiple requests before a slow server has had time to reply, -which means a backlog of response Observables could arrive at the client, at any moment, in any order. - -The `switchMap` returns its own Observable that _combines_ all `WikipediaService` response Observables, -re-arranges them in their original request order, -and delivers to subscribers only the most recent search results. - -{@a xsrf} - -## Guarding against Cross-Site Request Forgery - -In a cross-site request forgery (CSRF or XSRF), an attacker tricks the user into visiting -a different web page with malignant code that secretly sends a malicious request to your application's web server. - -The server and client application must work together to thwart this attack. -Angular's `Http` client does its part by applying a default `CookieXSRFStrategy` automatically to all requests. - -The `CookieXSRFStrategy` supports a common anti-XSRF technique in which the server sends a randomly -generated authentication token in a cookie named `XSRF-TOKEN`. -The HTTP client adds an `X-XSRF-TOKEN` header with that token value to subsequent requests. -The server receives both the cookie and the header, compares them, and processes the request only if the cookie and header match. - -See the [XSRF topic on the Security page](guide/security#xsrf) for more information about XSRF and Angular's `XSRFStrategy` counter measures. - -{@a override-default-request-options} - -## Override default request headers (and other request options) - -Request options (such as headers) are merged into the -[default _RequestOptions_](/api/http/BaseRequestOptions "API: BaseRequestOptions") -before the request is processed. -The `HttpModule` provides these default options via the `RequestOptions` token. - -You can override these defaults to suit your application needs -by creating a custom sub-class of `RequestOptions` -that sets the default options for the application. - -This sample creates a class that sets the default `Content-Type` header to JSON. -It exports a constant with the necessary `RequestOptions` provider to simplify registration in `AppModule`. - - - -Then it registers the provider in the root `AppModule`. - - - - -
- - Remember to include this provider during setup when unit testing the app's HTTP services. - -
- - -After this change, the `header` option setting in `HeroService.create()` is no longer necessary, - - - -You can confirm that `DefaultRequestOptions` is working by examing HTTP requests in the browser developer tools' network tab. -If you're short-circuiting the server call with something like the [_in-memory web api_](guide/http#in-mem-web-api), -try commenting-out the `create` header option, -set a breakpoint on the POST call, and step through the request processing -to verify the header is there. - -Individual requests options, like this one, take precedence over the default `RequestOptions`. -It might be wise to keep the `create` request header setting for extra safety. - -{@a in-mem-web-api} - -## Appendix: Tour of Heroes _in-memory web api_ - -If the app only needed to retrieve data, you could get the heroes from a `heroes.json` file: - - -
- - You wrap the heroes array in an object with a `data` property for the same reason that a data server does: - to mitigate the [security risk](http://stackoverflow.com/questions/3503102/what-are-top-level-json-arrays-and-why-are-they-a-security-risk) - posed by top-level JSON arrays. - -
- -You'd set the endpoint to the JSON file like this: - - - -The *get heroes* scenario would work, -but since the app can't save changes to a JSON file, it needs a web API server. -Because there isn't a real server for this demo, -it substitutes the Angular _in-memory web api_ simulator for the actual XHR backend service. - - -
- - The in-memory web api is not part of Angular _proper_. - It's an optional service in its own - angular-in-memory-web-api - library installed with npm (see `package.json`). - - See the - README file - for configuration options, default behaviors, and limitations. - -
- - -The in-memory web API gets its data from a custom application class with a `createDb()` -method that returns a map whose keys are collection names and whose values -are arrays of objects in those collections. - -Here's the class for this sample, based on the JSON data: - - - -Ensure that the `HeroService` endpoint refers to the web API: - - - -Finally, redirect client HTTP requests to the in-memory web API by -adding the `InMemoryWebApiModule` to the `AppModule.imports` list. -At the same time, call its `forRoot()` configuration method with the `HeroData` class. - - - -### How it works - -Angular's `http` service delegates the client/server communication tasks -to a helper service called the `XHRBackend`. - -Using standard Angular provider registration techniques, the `InMemoryWebApiModule` -replaces the default `XHRBackend` service with its own in-memory alternative. -At the same time, the `forRoot` method initializes the in-memory web API with the *seed data* from the mock hero dataset. - - -
- - The `forRoot()` method name is a strong reminder that you should only call the `InMemoryWebApiModule` _once_, - while setting the metadata for the root `AppModule`. Don't call it again. - -
- - -Here is the final, revised version of src/app/app.module.ts, demonstrating these steps. - - - - -
- - Import the `InMemoryWebApiModule` _after_ the `HttpModule` to ensure that - the `XHRBackend` provider of the `InMemoryWebApiModule` supersedes all others. - -
- - -See the full source code in the . +If matching by URL isn't sufficient, it's possible to implement your own matching function. For example, you could look for an outgoing request that has an Authorization header: + +```javascript +const req = mockHttp.expectOne((req) => req.headers.has('Authorization')); +``` + +Just as with the `expectOne()` by URL in the test above, if 0 or 2+ requests match this expectation, it will throw. + +#### Handling more than one request + +If you need to respond to duplicate requests in your test, use the `match()` API instead of `expectOne()`, which takes the same arguments but returns an array of matching requests. Once returned, these requests are removed from future matching and are your responsibility to verify and flush. + +```javascript +// Expect that 5 pings have been made and flush them. +const reqs = mockHttp.match('/ping'); +expect(reqs.length).toBe(5); +reqs.forEach(req => req.flush()); +``` diff --git a/aio/content/navigation.json b/aio/content/navigation.json index a424bbec70..786c318b91 100644 --- a/aio/content/navigation.json +++ b/aio/content/navigation.json @@ -252,7 +252,7 @@ { "url": "guide/http", - "title": "Server Communication", + "title": "HttpClient", "tooltip": "Use HTTP to talk to a remote server." }, {