简体   繁体   中英

How to cache http requests in async style in angular http inteceptor?

I'm coding the angular 5 app. There is refreshAccessToken in authentication service

refreshAccessToken(): Observable<ICredentials> {
     const refreshTokenUrl = this.urlsService.getUrl(Urls.TOKEN);
     const httpParams = new HttpParams()
       .append('grant_type', 'refresh_token')
       .append('refresh_token', this.credentials.refresh_token)
       .append('client_id', Constants.CLIENT_ID)
       .toString();

     const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded');

     return this.http.post(refreshTokenUrl, httpParams, { headers })
       .map((response: any) => {
         this.setCredentials(response);
         localStorage.setItem(credentialsKey, JSON.stringify(this.getCredentials()));
         return response;
  });
}

I want to implement next alghorithm:

  1. Any http request failed because of unauthorized with status 401
  2. Try to get new access token from server
  3. Repeat the request

At the time while getting new access token, new http requests can be created, in this case I want to store them and repeat after new access token was recieved. To reach this purpose I've written the interceptor.

import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { AuthenticationService } from '@app/core/authentication/authentication.service';
import { Urls, UrlsService } from '@app/shared/urls';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class UnauthorizedRequestInterceptor implements HttpInterceptor {
  newAccessToken$: Observable<ICredentials> = null;

  constructor(
    public authService: AuthenticationService,
    private router: Router,
    private urlsService: UrlsService) {
  }

  addAuthHeader(request: HttpRequest<any>) {
    if (this.authService.getCredentials()) {
      return request.clone({
        setHeaders: {
          'Authorization': 'Bearer ' + this.authService.getCredentials().access_token
        }
      });
    }
    return request;
  }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    request = this.addAuthHeader(request);

    return next.handle(request).catch((error: HttpErrorResponse) => {
      let handleRequests$ = null;

      if (this.isNeedNewAccessToken(error, request)) {
        handleRequests$ = this.handleRequestWithNewAccessToken(request, next);
      }

      return handleRequests$ ||
        (this.isUnathorizedError(error)
          ? Observable.empty()
          : Observable.throw(error));
    });
  }

  logout() {
    this.authService.logout();
    this.router.navigate(['login']);
  }

  private isNeedNewAccessToken(error: HttpErrorResponse, request: HttpRequest<any>): boolean {
    return this.isUnathorizedError(error)
      && this.authService.isAuthenticated()
      && this.isSignInRequest(request);
  }

  private getNewAccessToken(): Observable<ICredentials> {
    if (!this.newAccessToken$) {
      this.newAccessToken$ = this.authService.refreshAccessToken();
    }
    return this.newAccessToken$;
  }

  private isUnathorizedError(error: HttpErrorResponse) {
    return error.status === 401;
  }

  private handleRequestWithNewAccessToken(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
    return this.getNewAccessToken()
      .mergeMap(() => {
        request = this.addAuthHeader(request);
        return next.handle(request);
      })
      .catch((err: HttpErrorResponse) => {
        if (err.error.error === 'invalid_grant') {
          this.logout();
        }
        return Observable.empty();
      });
  }

  private isNotSignInRequest(request: HttpRequest<any>): boolean {
    return request.url !== this.urlsService.getUrl(Urls.TOKEN);
  }
}

The behaviour of this interceptor is really strange. On each mergeMap on the handleRequestWithNewAccessToken the angular starts new post httpRequest. I've expected that the observable returned from refreshAccessToken (function from authenticationService , code at the top) would be resolved only once. I don't understand why it is fired for each merge map? I expected the next:

  1. I have observable - http request for token
  2. I use mergeMap - when http request finished, all callbacks that was added with mergeMap will be executed.

I was think to store requests that I need to handle in the global variable and invoke them in the subscribe() to http request, but there is problem, that each request should be resolved in the initial stream inside interceptor. I can't do smth like this: .subscribe(token => this.httpClient.request(storedRequest) because this will create new request, so all actions should be happened inside the observable chain.

Can you please help me to find solution?

PS This solution is working, but I want to get rid off unnecessary TOKEN requests, fe if page need to make 5 requests and token have expired - interceptor will make 5 requests for token.

I think your code is good and all you have to do is share the request for the new token.

refreshAccessToken(): Observable<ICredentials> {
        const refreshTokenUrl = this.urlsService.getUrl(Urls.TOKEN);
        const httpParams = new HttpParams()
            .append('grant_type', 'refresh_token')
            .append('refresh_token', this.credentials.refresh_token)
            .append('client_id', Constants.CLIENT_ID)
            .toString();

        const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded');

        return this.http.post(refreshTokenUrl, httpParams, { headers })
            .map((response: any) => {
                this.setCredentials(response);
                localStorage.setItem(credentialsKey, JSON.stringify(this.getCredentials()));
                return response;
            })
            .share(); // <- HERE
    }

Note share operator at the end of return

EDIT:

I also think you don't ever set back this.newAccessToken$ to null . Maybe consider adding set to null to finally like this:

private getNewAccessToken(): Observable<ICredentials> {
    if (!this.newAccessToken$) {
        this.newAccessToken$ = this.authService.refreshAccessToken()
            .finally(() => {
                this.newAccessToken$ = null;
            });
    }
    return this.newAccessToken$;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM