HTTP Client

2.2

Send feedback

HTTP is the primary protocol for browser/server communication.

The WebSocket protocol is another important communication technology; we won't cover it in this chapter.

Modern browsers support two HTTP-based APIs: XMLHttpRequest (XHR) and JSONP. A few browsers also support Fetch.

The Dart http library simplifies application programming of the XHR and JSONP APIs as we'll learn in this chapter covering:

We illustrate these topics with code that you can run live.

Demos

This chapter describes server communication with the help of the following demos

These demos are orchestrated by the root AppComponent

lib/app_component.dart

import 'package:angular2/core.dart'; import 'toh/hero_list_component.dart'; import 'wiki/wiki_component.dart'; import 'wiki/wiki_smart_component.dart'; @Component( selector: 'my-app', template: ''' <hero-list></hero-list> <my-wiki></my-wiki> <my-wiki-smart></my-wiki-smart> ''', directives: const [ HeroListComponent, WikiComponent, WikiSmartComponent ]) class AppComponent {}

First, we have to configure our application to use server communication facilities.

Providing HTTP Services

We use the Dart BrowserClient client to communicate with a server using a familiar HTTP request/response protocol. The BrowserClient client is one of a family of services in the Dart http library.

Before we can use the BrowserClient client , we'll have to register it as a service provider with the Dependency Injection system.

Learn about providers in the Dependency Injection chapter.

In this demo, we register providers in the bootstrap() method of web/main.dart.

web/main.dart (v1)

import 'package:angular2/core.dart'; import 'package:angular2/platform/browser.dart'; import 'package:http/browser_client.dart'; import 'package:server_communication/app_component.dart'; void main() { bootstrap(AppComponent, [ provide(BrowserClient, useFactory: () => new BrowserClient(), deps: []) ]); }

Actually, it is unnecessary to include BrowserClient in the list of providers. But as is mentioned in the Angular Dart Transformer wiki page, the template compiler generates dependency injection code, hence all the identifiers used in DI have to be collected by the Angular transformer so that the libraries containing these identifiers can be transformed.

Unless special steps are taken, Dart libraries like http are not transformed. To ensure that the BrowserClient identifier is available for DI, we must add a resolved_identifiers parameter to the angular2 transformer in pubspec.yaml:

pubspec.yaml (transformers)

transformers: - angular2: platform_directives: - 'package:angular2/common.dart#COMMON_DIRECTIVES' platform_pipes: - 'package:angular2/common.dart#COMMON_PIPES' entry_points: web/main.dart resolved_identifiers: BrowserClient: 'package:http/browser_client.dart' - dart_to_js_script_rewriter

The Tour of Heroes HTTP Client Demo

Our first demo is a mini-version of the tutorial's "Tour of Heroes" (ToH) application. This version gets some heroes from the server, displays them in a list, lets us add new heroes, and saves them to the server. We use the Dart BrowserClient client to communicate via XMLHttpRequest (XHR).

It works like this.

ToH mini app

This demo has a single component, the HeroListComponent. Here's its template:

lib/toh/hero_list_component.html (template)

<h1>Tour of Heroes</h1> <h3>Heroes:</h3> <ul> <li *ngFor="let hero of heroes"> {{hero.name}} </li> </ul> New hero name: <input #newHeroName /> <button (click)="addHero(newHeroName.value); newHeroName.value=''"> Add Hero </button> <div class="error" *ngIf="errorMessage != null">{{errorMessage}}</div>

It presents the list of heroes with an ngFor. Below the list is an input box and an Add Hero button where we can enter the names of new heroes and add them to the database. We use a template reference variable, newHeroName, to access the value of the input box in the (click) event binding. When the user clicks the button, we pass that value to the component's addHero method and then clear it to make it ready for a new hero name.

Below the button is an area for an error message.

The HeroListComponent class

Here's the component class:

lib/toh/hero_list_component.dart (class)

class HeroListComponent implements OnInit { final HeroService _heroService; String errorMessage; List<Hero> heroes = []; HeroListComponent(this._heroService); Future<Null> ngOnInit() => getHeroes(); Future<Null> getHeroes() async { try { heroes = await _heroService.getHeroes(); } catch (e) { errorMessage = e.toString(); } } Future<Null> addHero(String name) async { name = name.trim(); if (name.isEmpty) return; try { heroes.add(await _heroService.addHero(name)); } catch (e) { errorMessage = e.toString(); } } }

Angular injects a HeroService into the constructor and the component calls that service to fetch and save data.

The component does not talk directly to the Dart BrowserClient client! The component doesn't know or care how we get 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, we do not call the service's get method in the component's constructor. We call it inside the ngOnInit lifecycle hook instead and count 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 hero service getHeroes() and addHero() asynchronous methods return the Future values of the current hero list and the newly added hero, respectively. The hero list component methods of the same name specifying the actions to be taken when the asynchronous method calls succeed or fail.

For more information about Futures, consult any one of the articles on asynchronous programming in Dart, or the tutorial on Asynchronous Programming: Futures.

With our basic intuitions about the component squared away, we're ready to look inside the HeroService.

Fetch data with the HeroService

In many of our previous samples we faked the interaction with the server by returning mock heroes in a service like this one:

import 'dart:async'; import 'package:angular2/core.dart'; import 'hero.dart'; import 'mock_heroes.dart'; @Injectable() class HeroService { Future<List<Hero>> getHeroes() async => mockHeroes; }

In this chapter, we revise that HeroService to get the heroes from the server using the Dart BrowserClient client service:

lib/toh/hero_service.dart (revised)

import 'dart:async'; import 'dart:convert'; import 'hero.dart'; import 'package:angular2/core.dart'; import 'package:http/browser_client.dart'; import 'package:http/http.dart'; @Injectable() class HeroService { static const _heroesUrl = 'app/heroes'; // URL to web API final BrowserClient _http; HeroService(this._http); Future<List<Hero>> getHeroes() async { try { final response = await _http.get(_heroesUrl); final heroes = _extractData(response) .map((value) => new Hero.fromJson(value)) .toList(); return heroes; } catch (e) { throw _handleError(e); } } Future<Hero> addHero(String name) async { try { final response = await _http.post(_heroesUrl, headers: {'Content-Type': 'application/json'}, body: JSON.encode({'name': name})); return new Hero.fromJson(_extractData(response)); } catch (e) { throw _handleError(e); } }

Notice that the Dart BrowserClient client service is injected into the HeroService constructor.

HeroService(this._http);

Look closely at how we call _http.get

lib/toh/hero_service.dart (getHeroes)

static const _heroesUrl = 'app/heroes'; // URL to web API Future<List<Hero>> getHeroes() async { try { final response = await _http.get(_heroesUrl); final heroes = _extractData(response) .map((value) => new Hero.fromJson(value)) .toList(); return heroes; } catch (e) { throw _handleError(e); } }

We pass the resource URL to get and it calls the server which should return heroes.

It will return heroes once we've set up the in-memory web api described in the appendix below. Alternatively, we can (temporarily) target a JSON file by changing the endpoint URL:

static const _heroesUrl = 'heroes.json'; // URL to JSON file

Process the response object

Remember that our getHeroes() method mapped the _http.get response object to heroes with an _extractData helper method:

lib/toh/hero_service.dart (excerpt)

dynamic _extractData(Response res) { var body = JSON.decode(res.body); return body['data']; }

The response object does not hold our data in a form we can use directly. To make it useful in our application we must parse the response data into a JSON object

Parse to JSON

The response data are in JSON string form. We must parse that string into Objects which we do by calling the JSON.decode() method from the dart:convert library.

We shouldn't expect the decoded JSON to be the heroes list directly. The server we're calling always wraps JSON results in an object with a data property. We have to unwrap it to get the heroes. This is conventional web api behavior, driven by security concerns.

Make no assumptions about the server API. Not all servers return an object with a data property.

Do not return the response object

Our getHeroes() could have returned the HTTP response. Bad idea! The point of a data service is to hide the server interaction details from consumers. The component that calls the HeroService wants heroes. It has no interest in what we do to get them. It doesn't care where they come from. And it certainly doesn't want to deal with a response object.

Always handle errors

Whenever we deal with I/O we must be prepared for something to go wrong as it surely will. We should catch errors in the HeroService and do something with them. We may also pass an error message back to the component for presentation to the user but only if we can say something the user can understand and act upon.

In this simple app we provide rudimentary error handling in both the service and the component.

lib/toh/hero_service.dart (excerpt)

Future<List<Hero>> getHeroes() async { try { final response = await _http.get(_heroesUrl); final heroes = _extractData(response) .map((value) => new Hero.fromJson(value)) .toList(); return heroes; } catch (e) { throw _handleError(e); } } Exception _handleError(dynamic e) { // In a real world app, we might use a remote logging infrastructure // We'd also dig deeper into the error to get a better message print(e); // log to console instead return new Exception('Server error; cause: $e'); }

HeroListComponent error handling

Back in the HeroListComponent, we wrapped our call to _heroService.getHeroes() in a try clause. When an exception is caught, the errorMessage variable — which we've bound conditionally in the template — gets assigned to.

lib/toh/hero_list_component.dart (getHeroes)

Future<Null> getHeroes() async { try { heroes = await _heroService.getHeroes(); } catch (e) { errorMessage = e.toString(); } }

Want to see it fail? Reset the api endpoint in the HeroService to a bad value. Remember to restore it!

Send data to the server

So far we've seen how to retrieve data from a remote location using an HTTP service. Let's add the ability to create new heroes and save them in the backend.

We'll create an easy method for the HeroListComponent to call, an addHero() method that takes just the name of a new hero:

Future<Hero> addHero(String name) async {

To implement it, we need to know some details about the server's api for creating heroes.

Our data server follows typical REST guidelines. It expects a POST request at the same endpoint where we 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:

{ "name": "Windstorm" }

The server will generate the id and return 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.

Now that we know how the API works, we implement addHero()like this:

lib/toh/hero_service.dart (addHero)

Future<Hero> addHero(String name) async { try { final response = await _http.post(_heroesUrl, headers: {'Content-Type': 'application/json'}, body: JSON.encode({'name': name})); return new Hero.fromJson(_extractData(response)); } catch (e) { throw _handleError(e); } }

Headers

The Content-Type header allows us to inform the server that the body will represent JSON.

Body

Despite the content type being specified as JSON, the POST body must actually be a string. Hence, we explicitly encode the JSON hero content before passing it in as the body argument.

JSON results

As with getHeroes(), we extract the data from the response using the _extractData() helper.

Back in the HeroListComponent, we see that its addHero() awaits for the service's asynchronous addHero() to return, and when it does, the new hero is added to the heroes list for presentation to the user.

lib/toh/hero_list_component.dart (addHero)

Future<Null> addHero(String name) async { name = name.trim(); if (name.isEmpty) return; try { heroes.add(await _heroService.addHero(name)); } catch (e) { errorMessage = e.toString(); } }

Cross-origin requests: Wikipedia example

We just learned how to make XMLHttpRequests using the Dart BrowserClient service. This is the most common approach for server communication. 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.

Modern browsers do allow XHR requests to servers from a different origin if the server supports the CORS protocol. If the server requires user credentials, we'll enable them in the request headers.

Some servers do not support CORS but do support an older, read-only alternative called JSONP. Wikipedia is one such server.

This StackOverflow answer covers many details of JSONP.

Search wikipedia

Let's build a simple search that shows suggestions from wikipedia as we type in a text box.

Wikipedia search app (v.1)

Wikipedia offers a modern CORS API and a legacy JSONP search API.

The remaining content of this section is coming soon. In the meantime, consult the example sources to see how to access Wikipedia via its JSONP API.

Appendix: Tour of Heroes in-memory server

If the app only needed to retrieve data, you could get the heroes from a heroes.json file:

web/heroes.json

{ "data": [ { "id": "1", "name": "Windstorm" }, { "id": "2", "name": "Bombasto" }, { "id": "3", "name": "Magneta" }, { "id": "4", "name": "Tornado" } ] }

We 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 posed by top-level JSON arrays.

We'd set the endpoint to the JSON file like this:

static const _heroesUrl = 'heroes.json'; // URL to JSON file

The get heroes scenario would work. But we want to save data too. We can't save changes to a JSON file. We need a web API server. We didn't want the hassle of setting up and maintaining a real server for this chapter. So we turned to an in-memory web API simulator instead.

The in-memory web api is not part of the Angular core. It's an optional service in its own angular-in-memory-web-api library that we installed with npm (see package.json) and registered for module loading by SystemJS (see systemjs.config.js)

The in-memory web API gets its data from a createDb() method that returns a map whose keys are collection names and whose values are lists of objects in those collections.

Here's the class we created for this sample based on the JSON data:

lib/hero_data.dart

import 'package:http/browser_client.dart'; import 'package:http_in_memory_web_api/http_in_memory_web_api.dart'; CreateDb _createDb = () => { 'heroes': [ {"id": "1", "name": "Windstorm"}, {"id": "2", "name": "Bombasto"}, {"id": "3", "name": "Magneta"}, {"id": "4", "name": "Tornado"} ] }; BrowserClient HttpClientBackendServiceFactory() => new HttpClientInMemoryBackendService(_createDb);

Ensure that the HeroService endpoint refers to the web API:

static const _heroesUrl = 'app/heroes'; // URL to web API

Finally, we need to redirect client HTTP requests to the in-memory web API.

To achieve this, we have Angular inject an in-memory web API server instance as a provider for the BrowserClient. This is possible because the in-memory web API server class extends BrowserClient.

Here is the revised (and final) version of web/main.dart demonstrating these steps.

web/main.dart (final)

import 'package:angular2/core.dart'; import 'package:angular2/platform/browser.dart'; import 'package:http/browser_client.dart'; import 'package:server_communication/app_component.dart'; import "package:server_communication/hero_data.dart"; void main() { bootstrap(AppComponent, const [ // in-memory web api provider const Provider(BrowserClient, useFactory: HttpClientBackendServiceFactory, deps: const []) ]); }

See the full source code in the .