angular5rxjs5retrywhen

Angular 5.2 & RxJS 5.5 HttpInterceptor retryWhen, but update request?


I am trying to intercept a 401 response, send a refresh token request before trying request again (but with a different header). I have it working except retryWhen does not give me to modify the original request header. So I've been trying to use catchError instead but I cannot seem to execute the request again.

Here is my current retryWhen:

import {Injectable} from '@angular/core';
import {
    HttpInterceptor, HttpHandler, HttpRequest, HttpEvent, HttpErrorResponse
}
    from '@angular/common/http';

import {Observable} from 'rxjs/Observable';
import {CustomerService} from "../customer/customer.service";
import * as ApplicationSettings from "application-settings";
import {retryWhen, map} from "rxjs/operators";
import {LoginResponse} from "./LoginResponse";

/**
 * This takes a request that requires an access_token and refreshes it on 401 errors.
 */
@Injectable()
export class RefreshTokenInterceptor implements HttpInterceptor {

    public constructor(private customer: CustomerService) {}

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(req).pipe(
            retryWhen(errors => {
                return Observable.create(observer => {
                    errors.forEach((error: HttpErrorResponse) => {
                        if (error.status === 401) {
                            let refresh_token = ApplicationSettings.getString('refresh_token');

                            return this.customer.refreshToken(refresh_token).subscribe(
                                (response: LoginResponse) => {
                                    this.customer.setToken(response);

                                    let headers = req.headers.set('Authorization', `Bearer ${response.access_token}`);
                                    console.log(`Bearer ${response.access_token}`);
                                    let newReq = req.clone({headers: headers});
                                    observer.next(next.handle(newReq));
                                    observer.complete();
                                },
                                error2 => {
                                    observer.error();
                                }
                            );
                        } else {
                            observer.error();
                        }
                    });
                });
            })
        );
    }
}

If I swap out retryWhen with catchError:

        catchError((err, caught) => {
            console.log('err: ' + JSON.stringify(err));

            if (err.status === 401) {
                console.log('401 !!!! REFRESH MEH!');
                let newReqOb: Observable<HttpEvent<any>> = Observable.create(observer => {
                    console.log('going to refresh token');
                    let refresh_token = ApplicationSettings.getString('refresh_token');

                    let refresh = this.customer.refreshToken(refresh_token);

                    refresh.subscribe((response: LoginResponse) => {
                        console.log('token refreshed!');
                        this.customer.setToken(response);


                        let access_token = ApplicationSettings.getString('access_token');
                        let headers = req.headers.set('Authorization', `Bearer ${access_token}`);

                        console.log(`Bearer ${access_token}`);

                        let newReq = req.clone({headers: headers});

                        observer.next(next.handle(newReq)); // <-- HERE IT WONT FIRE
                        observer.complete();

                    });

                });

                return newReqOb;
            }

            return caught;
        })

The important part is I am returning next.handle(newReq) and it doesn't seem to fire the request. If I switch it to next.handle(newReq).subscribe(), the request will fire but no callbacks are triggered.

Here is the full example with retryWhen:

import {Inject, Injectable} from '@angular/core';
import {
    HttpInterceptor, HttpHandler, HttpRequest, HttpEvent, HttpErrorResponse, HttpClient
}
    from '@angular/common/http';

import {Observable, ObservableInput} from 'rxjs/Observable';
import {CustomerService} from "../customer/customer.service";
import * as ApplicationSettings from "application-settings";
import {retryWhen, map, catchError} from "rxjs/operators";
import {LoginResponse} from "./LoginResponse";
import {APP_CONFIG, AppConfig} from "../../app.config";

/**
 * This takes a request that requires an access_token and refreshes it on 401 errors.
 */
@Injectable()
export class RefreshTokenInterceptor implements HttpInterceptor {

    public constructor(private customer: CustomerService, private http: HttpClient, @Inject(APP_CONFIG) private config: AppConfig) {}

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(req).pipe(
            catchError((err, caught) => {
                console.log('err: ' + JSON.stringify(err));

                if (err.status === 401) {
                    console.log('401 !!!! REFRESH MEH!');
                    let newReqOb: Observable<HttpEvent<any>> = Observable.create(observer => {
                        console.log('going to refresh token');
                        let refresh_token = ApplicationSettings.getString('refresh_token');

                        let refresh = this.customer.refreshToken(refresh_token);

                        refresh.subscribe((response: LoginResponse) => {
                            console.log('token refreshed!');
                            this.customer.setToken(response);


                            let access_token = ApplicationSettings.getString('access_token');
                            let headers = req.headers.set('Authorization', `Bearer ${access_token}`);

                            console.log(`Bearer ${access_token}`);

                            let newReq = req.clone({headers: headers});

                            observer.next(next.handle(newReq));
                            observer.complete();

                        });

                    });

                    return newReqOb;
                }

                return caught;
            })
        );
    }
}

Solution

  • I did find out the issue, here is the resulting code:

    import {Inject, Injectable} from '@angular/core';
    import {
        HttpInterceptor, HttpHandler, HttpRequest, HttpEvent, HttpErrorResponse, HttpClient
    }
        from '@angular/common/http';
    
    import {Observable, ObservableInput} from 'rxjs/Observable';
    import {CustomerService} from "../customer/customer.service";
    import * as ApplicationSettings from "application-settings";
    import {catchError, switchMap, finalize} from "rxjs/operators";
    import {LoginResponse} from "./LoginResponse";
    import {APP_CONFIG, AppConfig} from "../../app.config";
    import {RouterExtensions} from "nativescript-angular/router";
    
    /**
     * This takes a request that requires an access_token and refreshes it on 401 errors.
     * @TODO What happens on 400 errors?
     */
    @Injectable()
    export class RefreshTokenInterceptor implements HttpInterceptor {
    
        isRefreshingToken: boolean = false;
    
        public constructor(private customer: CustomerService,
                           private http: HttpClient,
                           private router: RouterExtensions,
                           @Inject(APP_CONFIG) private config: AppConfig) {}
    
        intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
            let validate = {
                is_api_v1: req.url.indexOf('api/v1') > -1,
                is_not_register_end_point: !(req.url.indexOf('api/v1/customers') > -1 && req.method === 'POST')
            };
    
            if (validate.is_api_v1 && validate.is_not_register_end_point) {
                return next.handle(req).pipe(
                    catchError((err, caught) => {
                        if (err instanceof HttpErrorResponse && err.status === 401) {
                            console.log(req.url);
                            console.log('Injecting Refresh Token');
    
                            return this.handle401Error(req, next);
                        }
    
                        // return caught;
                    })
                );
            } else {
                return next.handle(req);
            }
    
        }
    
        handle401Error(req: HttpRequest<any>, next: HttpHandler) {
            //-- Test if we are refreshing so we are not stuck in an infinite loop
            if (!this.isRefreshingToken) {
                this.isRefreshingToken = true;
    
                let refresh_token = ApplicationSettings.getString('refresh_token');
                let refresh = this.customer.refreshToken(refresh_token);
    
                return refresh.pipe(
                    switchMap((tokenResp: LoginResponse) => {
                        this.customer.setToken(tokenResp);
    
                        let access_token = ApplicationSettings.getString('access_token');
                        let headers = req.headers.set('Authorization', `Bearer ${access_token}`);
    
                        return next.handle(req.clone({headers: headers}));
                    }),
                    catchError(error => {
                        ApplicationSettings.setBoolean("authenticated", false);
                        this.router.navigate(["/login"], { clearHistory: true, queryParams: {
                            error: 'Your session is no longer valid, please log in again',
                        }});
    
                        return Observable.throw("");
                    }),
                    finalize(() => {
                        this.isRefreshingToken = false;
                    })
                )
            }
        }
    }