Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

rxJs & angular 4 & restangular: stack errorInterceptors

In my angular 4 app I'm using ngx-restangular to handle all server calls. It returns observable as result, and this module has hooks to handle errors (like 401 etc).

But from documentation, i can handle 403 (401) so:

  RestangularProvider.addErrorInterceptor((response, subject, responseHandler) => {
    if (response.status === 403) {

      refreshAccesstoken()
      .switchMap(refreshAccesstokenResponse => {
        //If you want to change request or make with it some actions and give the request to the repeatRequest func.
        //Or you can live it empty and request will be the same.

        // update Authorization header
        response.request.headers.set('Authorization', 'Bearer ' + refreshAccesstokenResponse)

        return response.repeatRequest(response.request);
      })
      .subscribe(
        res => responseHandler(res),
        err => subject.error(err)
      );

      return false; // error handled
    }
    return true; // error not handled
  });

and this is good for one request, which has broken with 403 error. how can i stack this calls using rxJs? Becouse now, for example, i have 3 requests, which have 403 and for each this broken request I'm refreshing token - this is not so good, i have to update my token and then repeat all my broken requests. How can I achive this using Observables?

In angular 1 it was pretty easy:

Restangular.setErrorInterceptor(function (response, deferred, responseHandler) {
  if (response.status == 403) {
    // send only one request if multiple errors exist
    if (!refreshingIsInProgress) {
      refreshingIsInProgress = AppApi.refreshAccessToken(); // Returns promise
    }


    $q.when(refreshingIsInProgress, function () {
      refreshingIsInProgress = null;

      setHeaders(response.config.headers);

      // repeat request with error
      $http(response.config).then(responseHandler, deferred);
    }, function () {
      refreshingIsInProgress = null;

      $state.go('auth');
    });

    return false; // stop the promise chain
  }

  return true;
});

And all was working like a charm. But I'm new to rxJs & angular 4 and I don't have any idea how to achive this with observables and angular 4. Maybe somebody have an idea?

upd! here is my refreshAccesstoken method

const refreshAccesstoken = function () {
  const refreshToken = http.post(environment.apiURL + `/token/refresh`,
    {refreshToken: 'someToken'});
  return refreshToken;
};
like image 337
byCoder Avatar asked Jul 01 '17 06:07

byCoder


People also ask

What is RxJS used for?

Reactive Extensions for JavaScript, or RxJS, is a reactive library used to implement reactive programming to deal with async implementation, callbacks, and event-based programs. It can be used in your browser or with Node. js. RxJS observables allow you to publish events.

Is RxJS the same as React?

React is a javascript library for building user interfaces whereas RxJS is a javascript library for reactive programming using Observables. Both these javascript libraries are not comparable to each other since it serve different purposes. Both these can be used together to create better single page applications.

Is RxJS part of Angular?

Angular currently uses RxJs Observables in two different ways: as an internal implementation mechanism, to implement some of its core logic like EventEmitter. as part of its public API, namely in Forms and the HTTP module.

What is RxJS stand for?

RxJS (Reactive Extensions for JavaScript) is a library for reactive programming using observables that makes it easier to compose asynchronous or callback-based code.


2 Answers

One way I can see of doing this using ngx-restangular is to use the share operator. This way you don't have to implement complicated queueing logic. The idea is that if you have 3 requests all of the with a 403 response, they will all hit your interceptor and call your observable. If you share that observable, you will have only one token request for the 3 requests with a broken token.

You just have to use the share operator in your code like so:

refreshAccesstoken()
  .share()
  .switchMap(refreshAccesstokenResponse => {
    //If you want to change request or make with it some actions and give the request to the repeatRequest func.
    //Or you can live it empty and request will be the same.

    // update Authorization header
    response.request.headers.set('Authorization', 'Bearer ' + refreshAccesstokenResponse)

    return response.repeatRequest(response.request);
  })
  .subscribe(
    res => responseHandler(res),
    err => subject.error(err)
  );

I haven't checked that the code actually works but I have used this approach before for the same use case, but instead of interceptors, I was using the angular HTTP service.

EDIT to change refreshAccessToken:

You need to wrap your refreshAccessToken method in a deferred Observable and share it. This way you will reuse the same observable every time.

In the constructor:

this.source = Observable.defer(() => {
        return this.refreshAccesstoken();
    }).share();

Create another method that will invoke that observable:

refreshToken(): Observable<any> {
    return this.source
        .do((data) => {
            this.resolved(data);
        }, error => {
            this.resolved(error);
        });
}

EDIT2

I have created a git repository which uses angular2 with restangular. The scenario is the following:

  1. In my app.component I'm making 3 concurrent requests to get a list of orders. When the request has finished, I will log "Orders received".
  2. The orders endpoint requires an auth token. If one isn't provided it will return a 401.
  3. In my app.module I set the base URL to my API only. Because I am not setting the authorization token as well, all of my requests will fail with a 401.
  4. When the interceptor code is executed it will set the refresh token, which in my case is hard coded on the request and repeated the request.
  5. The observable that returns the token logs "Getting token" each time it is executed.

Here is what I can see in my console: console logs and requests

If I remove the share operator I will get the following logs: enter image description here meaning that the observable will be created every time.

In order for this to work, it is important that the source is declared and created in the RestangularConfigFactory. It will essentially become a singleton object and that's what allows the Share operator to work.

NOTE:

I created a simple web API hosted locally for this project just because it was faster for me.

EDIT3:Update to include code to refresh the token:

@Injectable()
export class TokenRefreshService {
    source: Observable<any>;
    pausedObservable: Observable<any>;
    constructor(
        private authenthicationStore: AuthenticationStore,
        private router: Router,
        private authenticationDataService: AuthenticationDataService,
        private http: ObservableHttpService) {
        this.source = Observable.defer(() => {
            return this.postRequest();
        }).share();
    }

    refreshToken(): Observable<any> {
        return this.source
            .do((data) => {
                this.resolved(data);
            }, error => {
                this.resolved(error);
            });
    }

    public shouldRefresh(): boolean {
        if (this.getTime() < 0) {
            return true;
        }
        return false;
    }

    private postRequest(): Observable<any> {
        let authData = this.authenticationDataService.getAuthenticationData();
        if (authData == null) {
            return Observable.empty();
        }
        let data: URLSearchParams = new URLSearchParams();
        data.append('grant_type', 'refresh_token');

        let obs = this.http.postWithHeaders(
            'token', data, { 'Content-Type': 'application/x-www-form-urlencoded' })
            .map((response) => {
                return this.parseResult(true, response, 'authenticateUserResult');
            })
            .catch((error) => {
                let errorMessage = this.rejected(error);
                return Observable.throw(errorMessage);
            });
        return obs;
    }

    private rejected(failure) {
        let authenticateUserResult;
        let response = failure;
        let data = response.json();

        if (response &&
            response.status === 400 &&
            data &&
            data.error &&
            data.error === 'invalid_grant') {

            authenticateUserResult = this.parseResult(false, data, 'error_description');

            return authenticateUserResult;
        } else {
            return failure;
        }
    }

    private parseResult(success, data, resultPropertyName) {

        let authenticateResultParts = data[resultPropertyName].split(':');

        data.result = {
            success: success,
            id: authenticateResultParts[0],
            serverDescription: authenticateResultParts[1]
        };

        return data;
    }

    private resolved(data): void {
        let authenticationResult = data.result;
        if (authenticationResult && authenticationResult.success) {
            let authenticationData = this.createAuthenticationData(data);
            this.authenthicationStore.setUserData(authenticationData);
        } else {
            this.authenthicationStore.clearAll();
            this.router.navigate(['/authenticate/login']);
        }
    }

    private createAuthenticationData(data: any): AuthenticationData {
        let authenticationData = new AuthenticationData();
        authenticationData.access_token = data.access_token;
        authenticationData.token_type = data.token_type;
        authenticationData.username = data.username;
        authenticationData.friendlyName = data.friendlyName;
       
        return authenticationData;
    }

    private getTime(): number {
        return this.getNumberOfSecondsBeforeTokenExpires(this.getTicksUntilExpiration());
    }

    private getTicksUntilExpiration(): number {
        let authData = this.authenticationDataService.getAuthenticationData();
        if (authData) {
            return authData.expire_time;
        }
        return 0;
    }

    private getNumberOfSecondsBeforeTokenExpires(ticksWhenTokenExpires: number): number {
        let a;
        if (ticksWhenTokenExpires === 0) {
            a = new Date(new Date().getTime() + 1 * 60000);
        } else {
            a = new Date((ticksWhenTokenExpires) * 1000);
        }

        let b = new Date();
        let utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate(), a.getHours(), a.getMinutes(), a.getSeconds());
        let utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate(), b.getHours(), b.getMinutes(), b.getSeconds());

        let timeInSeconds = Math.floor((utc1 - utc2) / 1000);
        return timeInSeconds - 5;
    }
}
like image 187
Radu Cojocari Avatar answered Sep 21 '22 09:09

Radu Cojocari


I had a similar problem in my application and I solved it this way:

Subscribers keep waiting for source to emit a value and dont do anything till then

We can use this behavior to solve our problem.

What we can do is something like this :

All HTTP Calls :

public doSomeHttpCall(params){
   return authService
             .getToken()
             .switchMap(token => httpcall())
}

With above code, the HTTP call wont happen until we get the token.

Inside auth service :

private token = new BehaviorSubject(null);
public getToken(){
   return this.token
              .filter( t => !!t );
}

Since I have a filter for token, if token is falsy no HTTP call will be executed. As soon as token gets a good value all the HTTP calls continue to execute.

In your case, if there is a 403 , disable the token by setting it to null. After refresh set it to new token value.

private refreshInProgress= false;
public refreshToken(){
   if(refreshInProgress){
      //dont do anything 
      return;
   }
   refreshInProgress = true;
   this.token.next(null);
   // fetch new token 
   this.token.next(newToken);
   refreshInProgress = false;
}

A very simple illustration here :

jsbin : https://jsbin.com/mefepipiqu/edit?html,js,console,output

like image 43
Vamshi Avatar answered Sep 19 '22 09:09

Vamshi