Home > database >  Angular Interceptor - perform async operation
Angular Interceptor - perform async operation

Time:05-05

I have the following interceptor that works as a retry wrapper in case the access token expires:

import { Injectable } from '@angular/core';
import {
  HttpEvent,
  HttpRequest,
  HttpHandler,
  HttpInterceptor,
  HttpHeaders,
} from '@angular/common/http';
import { Observable, EMPTY, throwError } from 'rxjs';
import { catchError, switchMap, take } from 'rxjs/operators';
import { Router } from '@angular/router';
import { AuthService } from '../services';

@Injectable()
export class UnauthorizedInterceptor implements HttpInterceptor {
  constructor(
    private router: Router,
    private readonly authService: AuthService
  ) {}

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError((error, caught) => {
        if (error.status === 401) {
          const newToken = this.authService.getNewToken();
          const updatedReq = req.clone({
            setHeaders: {
              Authorization: `Bearer ${newToken}`,
            },
          });
          return next.handle(updatedReq);
        }
        return EMPTY;
      })
    );
  }
}

witch works as expected. However, the function getNewToken is an async function and if I update the code to

 intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError(async (error, caught) => {
        if (error.status === 401) {
          const newToken = await this.authService.getNewToken();
          const updatedReq = req.clone({
            setHeaders: {
              Authorization: `Bearer ${newToken}`,
            },
          });
          return next.handle(updatedReq);
        }
        return EMPTY;
      })
    );
  }

What is the solution to this use case, when an async operation needs to be performed inside catchError?

P.S: The getNewToken function can be rewritten to return an observable instead of promise if this would help, but I'm not able to figure it out

Update

The error thrown by TypeScript is

Type 'Observable<HttpSentEvent | HttpHeaderResponse | HttpProgressEvent | HttpResponse<any> | HttpUserEvent<any> | Observable<...>>' is not assignable to type 'Observable<HttpEvent<any>>'.
  Type 'HttpSentEvent | HttpHeaderResponse | HttpProgressEvent | HttpResponse<any> | HttpUserEvent<any> | Observable<...>' is not assignable to type 'HttpEvent<any>'.
    Type 'Observable<HttpEvent<any>>' is not assignable to type 'HttpEvent<any>'.ts(2322)

and if I do a @ts-ignore then in the browser the error thrown is

 EmptyError: no elements in sequence

CodePudding user response:

Try this approach:

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.status === 401) {
          return this.handleUnauthorized(request, next);
        } else {
          return throwError(error);
        }
      })
    );
  }

And the "handleUnauthorized" method implementation


  handleUnauthorized(req: HttpRequest<any>, next: HttpHandler): Observable<any> {
    if (!this.isRefreshingToken) {
      this.isRefreshingToken = true;

      // Reset here so that the following requests wait until the token
      // comes back from the refreshToken call.
      this.authService.setToken(null);

      const body = new HttpParams()
        .set('grant_type', 'refresh_token')
        .set('refresh_token', localStorage.getItem('refresh_token'));
      return this.http.post('YOUR_URL', body.toString(), {
        headers: new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
      }).pipe(flatMap((el: any) => {
        if (el && el.access_token) {
          req = req.clone({ setHeaders: { Authorization: 'Bearer '   el.access_token } });
          this.authService.setToken(el);
          return next.handle(req);
        } else {
          this.authService.unAuthorize();
          return throwError('refreshToken');
        }
      }), finalize(() => {
        this.isRefreshingToken = false;
      }), catchError(error => {
        return throwError(error);
      }));
    } else {
      return this.authService.tokenSubject.pipe(
        filter(token => token)
        , take(1)
        , switchMap(token => {
          req = req.clone({ setHeaders: { Authorization: 'Bearer '   token.access_token } });
          this.authService.setToken(token);
          return next.handle(req);
        }));
    }
  }

And inside authService, you should have this functionality

  tokenSubject = new Subject<any>();
  .
  .
  .
  setToken(newToken: any) {
    this.tokenSubject.next(newToken)
  }

  getToken() {
    return this.tokenSubject.asObservable();
  }

I hope this approach helps you, Feel free to ask me any questions in the comment section.

  • Related