Codementor Events

Rx.JS retryWhen use-case in iframe-d Angular SPA

Published Dec 29, 2017Last updated Jun 26, 2018
Rx.JS retryWhen use-case in iframe-d Angular SPA

Reactive

Prerequisite: You should be familiar with ngRxStore, Angular 4, HttpClient, and Observables.

I should confess — I like RxJS, both parts: I like JS (the freedom of both functional and OOP) and I like Rx, which pleasantly turns my brain. If you feel that way too — this article is for you 😃.

Once upon a time, I was asked to do some expired Token refresh procedure with preconditions:

  • Our Angular 4 SPA is located in IFRAME (further SPA).
  • Parent application (parent) is authorized with AccessToken (to get it we need credentials) with expiration time. After the token expires, we can use RefreshToken to make an HTTP request to renew AccessToken (and respectively to get new RefreshToken as well).
  • On Parent page load event, Parent sends AccessToken and RefreshToken to SPA (in IFRAME, remember?) with window. PostMessage method (more here).

So now SPA can make HTTP requests to the corporate resources (it demands AccessToken in ‘Authorization’ header in each request).

So the task:

  • SPA makes HTTP requests to get data (with A).
  • Each time SPA HTTP request returns error with status 401 (non-authorized), we should renew our access-token with refreshToken. In case it is some other error, we should provide request subscriber with that error.
  • If RefreshToken is not valid anymore (we got an error on HTTP POST request to respective URL), we should ask Parent (with window.PostMessage) to request for a new AccessToken and provide it to SPA (so SPA doesn’t need creds to renew AccessToken).
  • After that, SPA should repeat the initial HTTP request and provide data to subscriber OR return error (including by timeout).

Solution plan

Here is a little brief of the flow:

As you know, Angular 4 HttpClient gets method returns Observable.

In the simplest case, the HTTP request function without any logic will look like this:

//someComponent.ts
class SomeComponent {

constructor(private httpService: HttpService) {} 

public getImportantData() {
    let url = 'some_data_api_url';
    this.httpService.getWithRetryLogic(url)
        .subscribe(
            (data: any) => console.log('We got data! ', data),
            (err: any) => console.log

//httpService.ts
import { HttpClient } from ‘@angular/common/http’;

…

@Injectable()
class HttpService {
    constructor(private http: HttpClient) {}
    public getWithRetryLogic(url) {
        return this.ht

But this is not what we want, so let the party begin 😃

Adding handling 401 error only

As you may know to handle errors in Observable streams, we can use catch operator. In our case, it will look like this:

this.http.get(url)
**    .catch((err) => {
        if (error.status === 401) {
            return Observable.throw(error);
        }
        return Observable.of(error);
    })**

I intentionally didn't do any optimization, so you can see all of the logic details.

This code does next:

  • If this http.get(URL)returns Observable with error, catch will execute its callback.
  • In catch callback, we can check error statusCode. If it is not 401 — returns normal observable (so possible subscribers will get this data in onNext callback, not in onError, read more).
  • Otherwise, we should return Observable.throw (In this case subscribers onError callback would execute but we need that for retryWhen operator to be triggered, but more about this a bit later).

Adding retryWhen to be able to handle retry logic of HTTP request

So after filtering response with 401 error, we need to implement retry logic. Before this, read a little explanation of retryWhen operator.

In two words — it will trigger on error in Observable sequence. Its callback function should return retry-Observable. Each time retry-Observable gets that value — source observable sequence will be repeated.

To apply it to our task, we need:

  • if 401 error — retryWhen callback will start refreshToken procedure.
  • And will create Subject instance (read more here). Subject can be used as both Observable and Observer. We use it to trigger retry logic by emitting (any) values to it.

Let's code it:

private retryLogicSubject: Subject < any > ;

…

public getWithRetryLogic(url) {
    this.http.get(url)
        .catch((err) => {
            if (error.status === 401) {
                return Observable.throw(error);
            }
            return Observable.of(error);
        })
**        .retryWhen((error) => {
            this.retryLogicSubject = new Subject();
            this.startRefreshTokensProcess();
            return this.retryLogicSubject.asObservable()
        })**
}

Just FYI — retryWhen callback is called only once on first error. After that, it will only start source Observable repeating on each this.retryLogicSubject.next ({any: ‘value’}).

Handling non-401 error

Now we handle 401 (authentication error) errors, but how about other errors? We move all of them to succeed stream, but we need to fix it, so getWithRetryLogic subscribers can handle them (data or error) properly.

We can do that with the usual map operator:

private retryLogicSubject: Subject < any > ;
…
public getWithRetryLogic(url) {
    this.http.get(url)
        .catch((err) => {
            if (error.status === 401) {
                return Observable.throw(error);
            }
            return Observable.of(error);
        })
        .retryWhen((error) => {
            this.retryLogicSubject = new Subject();
            this.startRefreshTokensProcess();
            return this.retryLogicSubject.asObservable()
        })
        .map(() => {
            if (data.status < 400 && data.status >= 200) { //check if not err 
                return data.json();
            }
            throw data; // back to error stream 
        })
}

Adding timeout

Why do we need it?

In case startRefreshTokensProces or getTokenFromParentAndSaveToStore methods (see next) will take some time or don’t return values, we should emit error to subscribers.

This is the simplest thing in that chain — we will use timeout operator:

private retryLogicSubject: Subject < any > ;
…
public getWithRetryLogic(url) {
    this.http.get(url)
        .catch((err) => {
            if (error.status === 401) {
                return Observable.throw(error);
            }
            return Observable.of(error);
        })
        .retryWhen((error) => {
            this.retryLogicSubject = new Subject();
            this.startRefreshTokensProcess();
            return this.retryLogicSubject.asObservable()
        })
        .map(() => {
            if (data.status < 400 && data.status >= 200) { //check if not err 

                return data.json();
            }
            throw data; // back to error stream 
        })
       .timeout(5000)
}

We have finished our getWithRetryLogic method. Now it's time to implement how repeat logic will be called after this.startRefreshTokensProcess is done.

Example of startRefreshTokensProcess method:

private startRefreshTokensProcess() {
    this.http.post(‘refreshTokenUrl’, this.data) //data contains refreshToken 
        .subscribe(
            (data: Response) => this.saveTokensToStore(data.json()),
            (error) => this.getTokenFro

What it does:

  • Send request with refreshToken value to API.
  • If error (refreshToken is expired as well, for example) — then call parent application to get new tokens and return them. postMessage mechanism and postMessage listener will save it to ngRxStore.
  • If we get new tokens — just save them to Store.

I will not write saveTokensToStore and getTokenFromParentAndSaveToStore methods here because these are not in the scope of the article.

What we should do now — just to subscribe to Store updates. So each time new tokens come, we should call retryLogic (if it is needed).

To start retry logic, we emit value with our Subject instance we returned from RetryWhen callback:

constructor(private http: HttpClient, private store: Store < any > ) {}

ngOnInit() {
    this.store.map((state) => state.tokens)
        .subscribe((tokens) => {
            if (this.retryLogicSubject) {
                this.retryLogicSubject.next({})
            }
        });
}

So it is done now. The full list of httpService class is:

//httpService.tsimport { HttpClient } from‘ @angular / common / http’;

…

@Injectable()
class HttpService {
    private retryLogicSubject: Subject < any > ;
    constructor(private http: HttpClient, private store: Store < any > ) {}
    ngOnInit() {
        this.store
            .map((state) => state.tokens)
            .subscribe((tokens) => {
                if (this.retryLogicSubject) {
                    this.retryLogicSubject.next({})
                }
            });
    }
    public getWithRetryLogic(url) {
        this.http.get(url)
            .catch((err) => {
                if (error.status === 401) {
                    return Observable.throw(error);
                }
                return Observable.of(error);
            })
            .retryWhen((error) => {
                this.retryLogicSubject = new Subject();
                this.startRefreshTokensProcess();
                return this.retryLogicSubject.asObservable()
            })
            .map(() => {
                if (data.status < 400 && data.status >= 200) { //check for errors
                    this.retryLogicSubject = undefined;
                    return data.json();
                }
                throw data; // back to error stream
            })
            .timeout(5000)
    }
    private startRefreshTokensProcess() {
        let data = { refreshToken: this.refreshToken }
        this.http
            .post('refreshTokenUrl', data)
            .subscribe(
                (data: Response) => this.saveTokensToStore(data.json()),
                (error) => this.getTokenFromParentAndSaveToStore()
            );
    }
}

The questions I didn’t touch in the scope of this article:

  • If Auth server returns non valid new tokens — then we just get error by timeout. That could add some counter to catch operator callback — but I didn’t want to clutter up the article.
  • Unsubscribe policy — good article about it.
  • Callbacks can/must be extracted to separate functions to improve code readability — I didn’t do this to keep the main idea obvious.
  • There is no separation of handling retry logic if we have a few concurrent get requests. It is easy to implement with some subjectManager object where we can generate symbols as keys for each getWithRetryLogic call, which have Subject as s value, and then use it in .map callback to delete respective key from subjectManager.

Conclusion

Code fast, die young… haha, joking! All of these ideas on how to improve the logic are welcome! Let’s go this reactive way together.

Discover and read more posts from Oleksandr Poshtaruk
get started
post commentsBe the first to share your opinion
Show more replies