Angular HTTP Interceptor using multiple RxJS streams (update)

Juri Sinitson
9 min readFeb 4, 2021
Photo by Samuel Sianipar on Unsplash

Introduction

I recently ran into a need of using an interceptor with multiple streams. I hope the solution below helps others dealing with a similar topic.

I assume you are already familiar with Angular, Angular HTTP Interceptor and RxJS. Furthermore to better understand this article it is desirable but not necessary to be familiar with a state management system like NgRx or Akita and an automated testing Framework like Jasmine.

Below are requirements as an example.

To every request except of the login one we have to:

  1. Add a token to be authorized (there are good alternatives how to do authorization in more secure way, but that’s not the topic here)
  2. Add person ID to the request, so backend knows what data is being requested (by a successful authorization with the token above we can request every person)
  3. If the token is not there, show the login dialog
  4. Continue after a successful login

The login-request should be passed through without being handled. Because in this example the login process doesn’t care about previous tokens and doesn’t need any additional parameters from an interceptor.

To satisfy the requirements above, I decided to make multiple streams, which go one in another. Every stream modifies the request by just one requirement. And the end stream handles that modified request.

Implementation

So let’s begin with the first stream.

This must be a token stream with the token coming originally from the login service. For the token stream we have two scenarios: either it’s there or we have to wait for the login. (We also have to wait for the login in case of an 401 Error coming from the backend. But this requirement is considered in the error handling further below). So we cover these two scenarios as the following:

Please note, that the token stream should be a completing one to avoid the intercepting system to endless wait for the completion and thus stalling the app.

For a completing stream by a success login in the login service you may use AsyncSubject like this :

Please note, that we call the method complete to complete the stream and begin emitting. According to the official documentation of RxJS it’s the feature of AsyncSubject, that the stream will not begin emitting until you call complete.

By assigning a new AsyncSubject we also reset the token stream for the next login. If we forget to reset, the token stream stays completed forever and will never emit again and thus the next login won’t work.

Now we pipe the token stream above and create the next stream of the modified request, requestArg is the original unmodified request object here. In this modification we add the token to the request header:

The next stream we create is again the stream of the modified request. In addition to the modification above we add the person id to the request:

Please note, that we have again to take care that the intercepting system doesn’t endless wait for the stream of person id to complete thus stalling the app. I decided for the Operator withLastesFrom. The stream of person id is combined with the modified request stream. The result stream is again the modified request stream containing the person id as parameter in addition to the modifications above.

And now we crate the handling stream:

Catching an error: there are at least two kinds of error, an authentication one (Error with the status 401) and other errors. On an authentication error the app should logout and show the login dialog. On other errors the error is just rethrown an caught e.g. by the login dialog component showing the error to the user.

In this example the method logout does two things: it deletes the cookie and sends a signal to the login dialog component to pop up (some details ot this method follow furhter below, more details follow in my next article about login state management).

Putting all together we get the following interceptor:

The implementation part is now completed.

By the way

When we get the person id from the URL, we may produce the stream of person ids in the person service like this:

Some details to the method logout of the login service:

We have to share the data between the login service and the login dialog component. There are different ways to do that e.g. sharing an RxJS-Subject or sharing a state of the login state management. I personally prefer the latter using Akita state-management. So the method logout of the login service looks pretty simple: this.actions$.dispatch(logout()); and that’s it.

I will go to in depth the implementation of the login state management in my next article. Here I just show you shortly how a logout effect could look like:

The login service shares the state showLogin and the the login prompt component reads that state like this respectively:

Testing

A short introduction to automated testing

For the further reading I assume you are familiar with testing for JavaScript/TypeScript, e.g. with such framework like Jasmine.

There are cases (e.g. when you exactly know the normal behavior and the error/edge cases) where you can begin defining the test scenarios before implementation. By the example of this article it was not really the case.

When we define the tests, we should try to test the behavior of the subject independent of the implementation details. Tests should still pass, when the implementation changes, but the behavior stays the same (e.g. refactoring). This is a great video describing that topic in details (this video is not obligatory for the further reading, but I recommend to watch it later):

This is also a great lecture reminding you to think about the edge cases as the case may be even before you start the implementation. (The link below will rewind you to the last hour of the lecture. If not, just rewind to 7:30:00 manually. This is the most important part of the video dealing with the topic here. Of course you can watch the whole lecture if you like. If you want to read further fluently, you can skip this video and watch it later):

Coding a Better World Together — with Uncle Bob — Day 1 — YouTube

Automated tests

Let’s define the test cases as stubs in Jasmine.

For test http-requests we create a fictive http service directly inside of the testing file.

As the next step let’s create the test module.

What is the snippet above doing?

For test http-requests to work we put the Http Client Testing Module of Angular in the section import. This module also provides the testing controller with which we will simulate http errors later below. For the login state management effects to work we need them in section import too.

In the section providers we provide LoginService, FictiveHttpService, and HttpCodeService as they are.

Now to PersonService. The person Id comes in from a URL-parameter using Angular’s ActiveRoute. Dealing with faking of ActiveRoute is not necessary here, so we just fake the whole service an reimplement the method getPersonId$ delivering the Observable of a hardcoded person id.

The interceptor which is here the testing subject is also provided the same way as in the productive code.

Now let’s fill in the tests stubs we have defined above. For the sake of better explanation and the article to be shorter I decided to skip creating a PageObject or some other constructs leaning on that.

The following happens in the snippet above:

First we request a login. Then we get the data of that request with the method expectOne. '/getToken' is the URL which is requested by the login. In the expect statements we assert that the request was correctly modified by the interceptor we are testing.

Now let’s fill in the next stub:

The following happens:

A logout consists in this case of removing the token cookie and showing the login dialog. Thus the token is not present any more. Now we spy on the method for the method logout. We also fake the method getToken to return an empty value. Then we make some non-login request. After that request we assert that logout has been called. We also assert that the login service returns the correct state value used for showing a login dialog.

Now to the next stub:

Here we fake the method getToken to return a non-empty value and make a non-login request using the URL fictiveUrl. And again we get the data of that request and assert that the interceptor we are testing has modified that request correctly.

The next stub will be filled in with the following:

This scenario will happen by a non-login request when the token is e.g. expired and was rejected by the backend with an Error status 401. Here we again fake the method getToken to return a non-empty value. Then we spy on the method logout. As the next step we make a non-login request.

To test an error-scenario we simulate an http error with the status 401. And in the next step we assert that the logout happens. We also assert that the login service returns the correct state value used for showing a login dialog.

No we fill in the next stub:

And again we fake the method getToken to return a non-empty value. As the next step we make a non-login request and simulate a non-401 Error. In the error-handling of the request we expect an error to be caught with the same status we defined in the error constant.

Now we are done with automated tests. Putting all together we get the followig testing file:

Local backend for manual tests

Most probably near automated tests you will want to make manual tests using a local backend with the data having the same structure as the productive data. And I think you will want to set up a local backend in a simple way. It may also be reasonable to setup HTTPS to test locally how the productive build behaves in HTTPS.

I decided for Node.js. Even the setup of HTTPS was surprisingly simple, just see the comments at the bottom of the file below.

When generating the certificate and the key to use HTTPS I’d suggest to provide some real information or information you can remember (password is not necessary because of local development). I have put these files in the same directory as the backend file below.

The next question is, if those certificate and key files may be versioned or should every developer create them for her-/himself. If you are not sure, clarify with the admin/DevOps department of your company or just don’t version at all and put them to the ignore-file instead.

Now we install express (npm i express -D) and define the routes /getToken and /someData and some routes for error testing in a JavaScript-file (e.g. backend.js):

Start the backend with the following command:

node src/server/backend/backend.js

Now the backend should be running and listening for the URLs: https://localhost:4300/,https://localhost:4300/someData and https://localhost:4300/getToken and the error routes.

When you go to the URL https://localhost:4300/ the browser will most probably ask, if you trust the certificate we created above. I assume you trust the certificate you created yourself for local development, so you will see the information you provided above for that certificate.
After you trusted the certificate above you will see you productive version running. Here I assume you have already made a productive build (e.g. with ng build --prod), your project name is my-project and you have left the build settings (e.g. outputPath in angular.json as of angular version 11.x) by default.

To access the routes above from the app we need some additional steps.

The server for angular development (you start it normally with the command ng serve) doesn’t usually run under same port as the local backend, here 4300 (but most probably under e.g. 4200 as the default for an angular app). There are developers tending just to prefix the routes in this case with https://localhost:4300. But often the browser will block the requests e.g. from localhost:4200 to localhost:4300 because of CORS-Policy. Disabling or undergoing the CORS-Policy (e. g. with some browser addons/settings or some server settings) should be avoided when possible for both production and development. In many cases a proxy is a better alternative.

The server for angular development offers such a proxy. (Consider the documentation of proxy/redirection/forwarding of your production server). Currently as of angular version 11.x you configure the proxy (proxy.conf.json) as the following:

In angular 11.x you then put the following to the options of the serve section of angular.json: “proxyConfig”: “src/proxy.conf.json”

Next time when you run ng serve and request /getToken or /someData you will be redirected from localhost:4200/getToken or localhost:4200/someData to https://localhost:4300/getToken or to https://localhost:4300/someData respectively. And as long the redirecting happens on the backend side, you will not have to deal with CORS-Policy of the browser.

So that’s all for now. Thank you for reading and happy coding.

--

--