Unwrapping HTTP Responses Using RxJS Observables In Angular 2 Beta 6
Now that Angular 2 has ditched Promises in favor of RxJS Observable streams, I've been actively trying to translate my Promise-based mental model into a Stream-based mental model. And, one of the things that I always did with Promises, in an AJAX-based application, was "unwrap" the HTTP payloads so that the calling context didn't have to know anything about the HTTP transport. In Angular 2, the HTTP service now responds with streams rather than promises. So, this post is an exploration of how to unwrap an RxJS Observable-based HTTP payload.
Run this demo in my JavaScript Demos project on GitHub.
Normally, when I'm building an API, the API accepts JSON (JavaScript Object Notation) payloads and responds with JSON payloads. This is true even if the response is an error - the error, itself, is expressed in some sort of a JSON payload. As such, on the client, I often need to unwrap and parse JSON for both 200-level HTTP responses as well as for non-200-level HTTP responses.
And, of course, we may also have to respond to errors that are not application errors exactly. Meaning, an HTTP request failed for a reason outside of the application's control such as a networking error or a gateway timeout error (when the application is temporarily unavailable). This brand of error won't come back as a JSON payload which means that our error handling on the client has to account for both expected JSON errors as well as unexpected HTTP errors.
With Promises, this kind of HTTP unwrapping can be easily done in a single .then() operator; or, a .then() operator followed by a .catch() operator. With RxJS Observable streams, the most similar pattern seems to be a .map() operator followed by a .catch() operator.
CAUTION: I'm on like day-5 of learning RxJS, so there may very well be a better way to do this.
To explore this map/catch combination, I've put together a small demo in which I can toggle between two lists: friends and enemies. Each of these lists is represented by a remote JSON data file; but, only the friends' version actually exists - the enemies' version will throw a 404. In either case, however, the concept of the HTTP transport is fully encapsulated within the PeopleService, which unwraps and extracts the data payloads before passing them on as the "next" value in the observable stream.
Since I am just learning about RxJS Observable streams, this demo also contains some other little features not entirely relevant to the concept of unwrapping HTTP payloads. For example, I'm injecting some simulated network latency into the HTTP request. And, I'm also making one of the streams a "hot stream" so as to compare and contrast what happens when you cancel the RxJS stream (which you can see in the video).
That said, let's take a look at the code:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
Unwrapping HTTP Responses Using RxJS Observables In Angular 2 Beta 6
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>
<h1>
Unwrapping HTTP Responses Using RxJS Observables In Angular 2 Beta 6
</h1>
<my-app>
Loading...
</my-app>
<!-- Load demo scripts. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/es6-shim.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/Rx.umd.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/angular2-polyfills.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/angular2-all.umd.js"></script>
<!-- AlmondJS - minimal implementation of RequireJS. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/almond.js"></script>
<script type="text/javascript">
// Defer bootstrapping until all of the components have been declared.
// --
// NOTE: Not all components have to be required here since they will be
// implicitly required by other components.
requirejs(
[ "AppComponent", "PeopleService" ],
function run( AppComponent, PeopleService ) {
ng.platform.browser.bootstrap(
AppComponent,
[
ng.http.HTTP_PROVIDERS,
PeopleService
]
);
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide the root App component.
define(
"AppComponent",
function registerAppComponent() {
var PeopleService = require( "PeopleService" );
// Configure the App component definition.
ng.core
.Component({
selector: "my-app",
template:
`
<p>
<a (click)="showFriends()">Friends</a>
|
<a (click)="showEnemies()">Enemies</a>
</p>
<p *ngIf="isLoading">
<em>Loading...</em>
</p>
<div *ngIf="! isLoading">
<h2 [ngSwitch]="listType">
<span *ngSwitchWhen=" 'friends' ">Friends</span>
<span *ngSwitchWhen=" 'enemies' ">Enemies</span>
</h2>
<ul>
<li *ngFor="#person of people">
{{ person.name }}
</li>
</ul>
</div>
`
})
.Class({
constructor: AppController,
// Register the component life-cycle methods on the prototype
// so that they're picked up at runtime.
ngOnInit: function noop() {}
})
;
AppController.parameters = [ new ng.core.Inject( PeopleService ) ];
return( AppController );
// I control the App component.
function AppController( peopleService ) {
var vm = this;
// I hold the most recent subscription to a people request.
var recentRequest = null;
// I determine if the local data is currently being loaded from the
// remote repository.
vm.isLoading = true;
// I determine which list of people is being rendered.
vm.listType = null;
// I hold the collection of people to be rendered.
vm.people = null;
// Expose the public methods.
vm.ngOnInit = ngOnInit;
vm.showEnemies = showEnemies;
vm.showFriends = showFriends;
// ---
// PUBLIC METHODS.
// ---
// I get called once after the component has been initialized and the
// inputs have been bound.
function ngOnInit() {
showFriends();
}
// I switch over to rendering the enemies list.
function showEnemies() {
vm.isLoading = true;
vm.listType = "enemies";
handlePeopleStream( peopleService.getEnemies() );
}
// I switch over to rendering the friends list.
function showFriends() {
vm.isLoading = true;
vm.listType = "friends";
handlePeopleStream( peopleService.getFriends() );
}
// ---
// PRIVATE METHODS.
// ---
// I cancel the subscription to the most recent people request so
// that any data piped back over the stream will be ignored.
function cancelRecentRequest() {
if ( recentRequest ) {
console.info( "Canceling recent request." );
// CAUTION: While this will stop our local value and error
// handlers from responding to the stream, if the underlying
// stream is HOT, it won't actually cancel the stream - that
// ship has sailed, those wheels have been set in motion,
// things have been done that cannot be undone. Mwww ha ha!
recentRequest.unsubscribe();
}
}
// Since requests to Friends and Enemies are both people streams, I
// can handle them uniformly.
function handlePeopleStream( stream ) {
// Cancel the subscription to any pending request - this way, we
// don't respond to the data once it comes back (if it has not
// yet returned to the client).
cancelRecentRequest();
// Subscribe to the people stream and keep track of the
// subscription so that we can cancel it if we need to.
recentRequest = stream.subscribe(
function handleValue( people ) {
vm.isLoading = false;
vm.people = people;
},
function handleError( error ) {
console.warn( "People request failed." );
console.log( error );
}
);
}
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide access to the people repository. People collections are returned
// as RxJS Observable streams.
define(
"PeopleService",
function registerPeopleService() {
PeopleService.parameters = [ new ng.core.Inject( ng.http.Http ) ];
return( PeopleService );
// I provide access to the people repository.
function PeopleService( http ) {
// Return the public API.
return({
getEnemies: getEnemies,
getFriends: getFriends
});
// ---
// PUBLIC METHODS.
// ---
// I return the collection of enemies (as a stream).
function getEnemies() {
var stream = Rx.Observable
// Since GitHub Pages (where this demo is hosted) are crazy
// fast, we're injecting a 2,000 millisecond simulated network
// delay before the HTTP request actually goes out.
// --
// NOTE: This actually makes the demo way more interesting
// because we can see how unsubscribing from the resulting
// stream affects the HTTP request.
.timer( 2 * 1000 )
// Once the timer is complete, kick off the AJAX request.
.flatMap(
function addNetworkLatency() {
return( http.get( "./enemies.json" ) );
}
)
// We don't want the calling context to know anything about
// the HTTP transport being used. As such, we want to unwrap
// the successful HTTP response body to be just the naked JSON.
.map(
function unwrapValue( value ) {
return( value.json() );
}
)
// The same is true for failed HTTP responses. We still don't
// want to return an HTTP response to the calling context; so,
// we're going to try and normalize the body, assuming it was
// JSON sent from the server.
.catch(
function unwrapError( error ) {
try {
var response = error.json();
// If the body wasn't JSON, something else went wrong -
// provide a normalized default error body.
} catch ( jsonError ) {
var response = {
code: -1,
message: "Something went horribly wrong."
};
}
return( Rx.Observable.throw( response ) );
}
)
.finally(
function handleFinally() {
console.debug( "Finally called for Enemies request." );
}
)
;
// CAUTION: This next little bit really has NOTHING to do with
// the core concept of the demo - this is just Me trying to
// understand streams a bit better.
// --
// To make this demo a little more interesting, we going to make
// the simulated network timer a "hot stream" so that it is
// initiated before the calling context even has a chance to
// subscribe to the published stream. This will create a proxy
// to the timer that will unify the event source.
var hotStream = stream.publish()
// Since we are creating a "hot stream," we have to tell the
// publisher to connect to the underlying source (the simulated
// network latency timer) in order to kick-off the timer stream.
hotStream.connect();
return( hotStream );
}
// I return the collection of friends (as a stream).
function getFriends() {
var stream = Rx.Observable
// Since GitHub Pages (where this demo is hosted) are crazy
// fast, we're injecting a 2,000 millisecond simulated network
// delay before the HTTP request actually goes out.
// --
// NOTE: This actually makes the demo way more interesting
// because we can see how unsubscribing from the resulting
// stream affects the HTTP request.
.timer( 2 * 1000 )
// Once the timer is complete, kick off the AJAX request.
.flatMap(
function addNetworkLatency() {
return( http.get( "./friends.json" ) );
}
)
// We don't want the calling context to know anything about
// the HTTP transport being used. As such, we want to unwrap
// the successful HTTP response body to be just the naked JSON.
.map(
function unwrapValue( value ) {
return( value.json() );
}
)
// The same is true for failed HTTP responses. We still don't
// want to return an HTTP response to the calling context; so,
// we're going to try and normalize the body, assuming it was
// JSON sent from the server.
.catch(
function unwrapError( error ) {
try {
var response = error.json();
// If the body wasn't JSON, something else went wrong -
// provide a normalized default error body.
} catch ( jsonError ) {
var response = {
code: -1,
message: "Something went horribly wrong."
};
}
return( Rx.Observable.throw( response ) );
}
)
.finally(
function handleFinally() {
console.debug( "Finally called for Friends request." );
}
)
;
// CAUTION: We are purposefully NOT CREATING A HOT STREAM for
// the friends request. This way, we can see some diversity in
// the RxJS observable behaviors.
return( stream );
}
}
}
);
</script>
</body>
</html>
As you can see, in the PeopleService, when the HTTP response comes back (ie, provides a "next" value in the Observable stream), I'm mapping it to the underlying JSON payload. This way, successful HTTP responses will show up as parsed JSON objects downstream. For non-2xx responses, I'm also mapping the HTTP object to the underlying JSON payload; however, if the error response doesn't have a JSON payload, I'm normalizing the error in order to adhere to a "known" error response structure. In all cases, the HTTP response is guaranteed to show up as a parsed JSON object downstream.
If we run this page, toggle over to the Enemies list and then back to the Friends list, we get the following page output:
As you can see, the successful HTTP responses were unwrapped to provide the Friends JSON payload. And, the unsuccessful HTTP requests were unwrapped and (in this case) normalized to present a consistent error structure for the calling context.
RxJS observable streams seem to be able to do all the things that Promises can do; it's just that the RxJS API landscape is an order of magnitude more complex. It looks like the key to success here is really just finding the right operators needed to do the thing that you want. In this case, unwrapping HTTP response payloads with RxJS' .map() and .catch() methods is delightfully similar to the way it would work with Promises.
Want to use code from this post? Check out the license.
Reader Comments
Thanks for the post. One nitpick dough - don't rely on "response" variable hoisting in the .catch handler. Just define it before "try-catch" block.
@John,
I am not sure there is any reason to think that wouldn't work? Can you give an example?
This is very nice example of Observables in action. There are 2 more ways (that I know) to make an Observable hot: .publish().refCount() or .share(). I prefer the later because it's more descriptive.
Another suggestion: maybe consider using Plunker and TypeScript for your examples. That makes it easier for other people to fork it and play with the code.
Cheers,
Sekib
@Sekib,
Thanks for the pointers - I have not seen refCount() or share() before. I'll have to read up on what they do. To be honest, I don't find the RxJS documentation all that easy to navigate.
Re: Plunker, I've not used it before. I don't think I would know how to get all the appropriate libraries in place. Right now, I just keep all my samples in a GitHub repo that you could fork and start playing with locally. Might not be as easy as forking something online, but I think it's much more flexible.
Thanks for this well written post, i'll follow up for more updates if you keep posting them.
http://jobremind.com/
http://www.jobremind.com/ibps-probationary-officer-notification-2016-17-cwe-vi-ibps-in-online-apply/
How were you able to use finally? I cannot see this in current RXJS Observable definition?
thanks for sharing this information. it is very useful for me.
http://apnajob.co.in/2017/03/maharashtra-police-constable-hall-ticket-2017-download/
http://apnajob.co.in