Angular2 http retry logic

2019-02-09 13:59发布

问题:

I have an API with token based authentication mechanism. After successful signin I store two tokens in the browser's local storage - access and refresh token. The access token contains all necessary information required to authorize a user on the server side and it has expiration date. When the access token is expired the client could request a new access token using refresh token and in the response it will get a pair of new tokens.

In angular 1.x the implementation is pretty simple and straightforward. For instance we could use interceptors:

httpInterceptor.$inject = ['$httpProvider'];
function httpInterceptor($httpProvider) {
  $httpProvider.interceptors.push(handleStaleAccessToken);

  handleStaleAccessToken.$inject = ['$q', '$injector', 'session'];
  function handleStaleAccessToken($q, $injector, session) {

    function logoutAndRedirect() {
      var authenticationRedirect = $injector.get('authenticationRedirect');
      session.destroy();
      authenticationRedirect.toLoginPage();
    }

    return {
      responseError: function(rejection) {
        // Do nothing for non 403 errors
        if (rejection.status !== 403) {
          return $q.reject(rejection);
        }

        var errorCode = rejection.data.error && rejection.data.error.code;
        if (errorCode === 'access_token_expired') {
          var $http = $injector.get('$http');

          // Refresh token
          var params = { refreshToken: session.getRefreshToken() };
          return $http.post('/api/auth/refresh', params).then(function(response) {
            session.setTokens(response.data);
            // Re try failed http request
            return $http(rejection.config);
          }).catch(function(error) {
            logoutAndRedirect();
            return $q.reject(error);
          });
        } else {
          logoutAndRedirect();
        }

        return $q.reject(rejection);
      }
    };
  }
}

But how to implement similar logic in angular 2 / rxjs app?

回答1:

This can be done transparently in Angular2 by extending the Http class and leveraging observable operators like flatMap.

Here is some sample code:

if (hasTokenExpired()) {
  return this.authService
             .refreshAuthenticationObservable()
             .flatMap((authenticationResult:AuthenticationResult) => {
                if (authenticationResult.IsAuthenticated == true) {
                     this.authService.setAuthorizationHeader(request.headers);
                  return this.http.request(url, request);
                }
                return Observable.throw(initialError);
    });
}

This code must be integrated into a custom sub class of the Http one:

An approach could be to extend the HTTP object to intercept errors:

@Injectable()
export class CustomHttp extends Http {
  constructor(backend: ConnectionBackend, defaultOptions: RequestOptions) {
    super(backend, defaultOptions);
  }

  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    console.log('request...');
    return super.request(url, options).catch(res => {
      // do something
    });        
  }

  get(url: string, options?: RequestOptionsArgs): Observable<Response> {
    console.log('get...');
    return super.get(url, options).catch(res => {
      // do something
    });
  }
}

and register it as described below:

bootstrap(AppComponent, [HTTP_PROVIDERS,
    new Provider(Http, {
      useFactory: (backend: XHRBackend, defaultOptions: RequestOptions) => new CustomHttp(backend, defaultOptions),
      deps: [XHRBackend, RequestOptions]
  })
]);

For more details have a look at these questions:

  • Handling refresh tokens using rxjs
  • Angular 2 - How to get Observable.throw globally


回答2:

I had to do something similar in my recent project shafihuzaib/cdp-ng-boilerplate and landed on this question for my answer. I couldn't go for the above suggested solution, as it felt complicated and something not desirable. So I came back to leave my solution after I implemented one. However, the difference being that in my case, I had two such tokens.

So, every request that needs to have tokens' validity checked, is called inside this function.

tokenValidatedRequest(func): Observable<any>{
    let returnObservable = new Observable();

    /**
     * 1. check for auth token expiry - refresh it, if necessary
     */
    if( parseInt(localStorage.getItem('AUTH_TOKEN_EXPIRY')) < (new Date()).valueOf() ){
        //auth expired
        this.refresh().subscribe(res => {
            //refreshed
            //this.postAuthSuccess(res);

             returnObservable = func();

        })
    }
    else{
        //auth not expired

       returnObservable = func();

    }

    return returnObservable;
}

The most important thing here is that func() should return an Observable, so that it can be consumed accordingly.

makeSomeHttpCall(){
   this.tokenValidatedRequest(()=>{
       return this.http.post(...);
   }). subscribe();
}

It may seem a bit complicated for someone new, but I am sure it is a little more efficient.

In the following links, please ignore the details that are irrelevant to this question and focus on the usage of the suggested solution.

Actual implementation of tokenValidatedRequest() in my project.

 tokenValidatedRequest(func , tqlCheck = false): Observable<any>{
    /**
     * Delegate the actual task. However return an Observable, so as to execute 
     * the callback function only when subscribed to..
     */
    //return Observable.create(obs => obs = (this.__tokenValidatedRequest(func, tqlCheck)));

    return this.__tokenValidatedRequest(func, tqlCheck);
}
private __tokenValidatedRequest(func, tqlCheck = false): Observable<any>{
    let returnObservable = new Observable();

    /**
     * 1. check for auth token expiry - refresh it, if necessary
     * 2. after step 1 - check for TQL token expiry (if tqlCheck is true) - refresh it, if necessary
     * 3. 
     */
    if( parseInt(localStorage.getItem('AUTH_TOKEN_EXPIRY')) < (new Date()).valueOf() ){
        //auth expired
        this.refresh().subscribe(res => {
            //refreshed
            this.postAuthSuccess(res);

            if(tqlCheck &&  localStorage.getItem("TQL_TOKEN_EXPIRY") &&
                    parseInt(localStorage.getItem("TQL_TOKEN_EXPIRY")) < (new Date()).valueOf()
               ){

                this.activateUser().subscribe(res => {
                    //TQL token subscribed 
                    returnObservable = func();
                })
            }
            else{
                // Probably not a TQL request
                returnObservable = func();
            }
        })
    }
    else{
        //auth not expired

        //check if tql token has expired
        if(tqlCheck &&  localStorage.getItem("TQL_TOKEN_EXPIRY") &&
                    parseInt(localStorage.getItem("TQL_TOKEN_EXPIRY")) < (new Date()).valueOf()
               ){

                this.activateUser().subscribe(res => {
                    //TQL token subscribed 
                    returnObservable = func();
                })
            }
            else{
                // Probably not a TQL request or none of the tokens expired
                returnObservable = func();
            }
    }

    return returnObservable;
}

How it is used in other services!

getAllParkingSpaces() : Observable<any> {
    let query = {
        Query: {
            ....
        }
    };

    return this.authService.tokenValidatedRequest(()=>{
        return this.api.post( CONFIG.api.engineUrl + 'devices/parking', query);
    }, true);
}

How I finally subscribe to it!

    this.realTimeParkingService.getAllParkingSpaces().subscribe( r => {
  this.parkingSpaces = r;
});