RxJs & angular 4 & restangular: stack error Interceptors

In my angular 4 application, I use ngx-restangularto handle all server calls. It returns the observed result, and this module has hooks for handling errors (e.g. 401, etc.).

But from the 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 that failed with error 403. how can I stack these calls using rxJs? Now I have 3 requests that have 403, and for each of this broken request I update the token - this is not so good, I need to update the token and then repeat all my broken requests. How can I achieve this with Observables?

In angular 1, this was pretty simple:

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;
});

. rxJs angular 4, , angular 4. , - ?

! refreshAccesstoken

const refreshAccesstoken = function () {
  const refreshToken = http.post(environment.apiURL + `/token/refresh`,
    {refreshToken: 'someToken'});
  return refreshToken;
};
+4
4

ngx-restangular - share. , . , 3 403, . , 3 .

share :

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)
  );

, , , HTTP angular.

, refreshAccessToken:

refreshAccessToken Observable . , .

:

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

, :

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

EDIT2

git , angular2 restangular. :

  • app.component 3 , . , " ".
  • . , 401.
  • .module URL API. , 401.
  • , , .
  • , " " , .

: console logs and queries

share, : enter image description here , .

, , RestangularConfigFactory. , Share .

:

-API, , .

EDIT3: , :

@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;
    }
}
Hide result
+3

, :

,

.

, :

HTTP-:

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

, HTTP- , .

:

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

, falsy, HTTP- . , HTTP- .

, 403, , null. .

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;
}

:

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

+1

HTTP-, angular, :

  • HTTP-, :

post<T>(serviceUrl: string, data: any): Observable<T> {
    return Observable.defer(() => {
        return super.post<T>(serviceUrl, data);
    }).retryWhen((error) => {
        return this.refresh(error);
    });
}
Hide result
  1. , 403.Else . , 3 . :)

refresh(obs: Observable<any>): Observable<any> {
    return obs
        .switchMap((x: any) => {
            if (x.status === 403) {
                return Observable.of(x);
            }
            return Observable.throw(x);
        })
        .scan((acc, value) => {
            return acc + 1;
        }, 0)
        .takeWhile(acc => acc < 3)
        .flatMap(() => {
            console.log('Token refresh retry');
            return this.tokenRefreshService.refreshToken();
        });
}
Hide result
+1

RxJs is not a silver bullet, and in such cases this can complicate the situation.

While it may be possible to combine several operators and have if-blocks, repeated blocks in one monitored stream, the final solution will be too complicated, difficult to maintain, understand and refactor. This is especially true for http processing.

0
source

Source: https://habr.com/ru/post/1680538/


All Articles