2

I have an Angular application that uses JWT tokens for authentication. When an HTTP request returns a 401 error (unauthorized), I need to refresh the token and retry the request.

My previous Question Angular HTTP Interceptor wait http requests until get a refresh token

I've implemented an HTTP interceptor that handles the 401 error by calling a function that refreshes the token and retries the request.

This works fine when there's only one HTTP request at a time. However, my application has multiple HTTP requests that need to be executed in parallel. I'm using forkJoin inside my route resolver to execute them all at once, but when one of them returns a 401 error, the other requests keep going and also return 401.

I'd like to implement a solution that queues the requests that fail with a 401 error until the token is refreshed, and then resumes them automatically with the new token. How can I do that?

Previously i have

Thanks in advance for your help.

Interceptor:

export class HttpErrorInterceptor implements HttpInterceptor {
    constructor(
        private _router: Router,
        private _logger: LoggerService,
        private _authService: AuthenticationService
    ) {}

    private isRefreshingToken = false;
    private tokenSubject: BehaviorSubject<string | null> = new BehaviorSubject<
        string | null
    >(null);

    public intercept(
        request: HttpRequest<any>,
        next: HttpHandler
    ): Observable<HttpEvent<IResponse>> {
        return next.handle(request).pipe(
            timeout(appSettings.ajaxTimeout),
            catchError((error) => this.errorHandler(error, request, next))
        );
    }
    
    private errorHandler(
        error: HttpErrorResponse,
        request: HttpRequest<any>,
        next: HttpHandler
    ): Observable<HttpEvent<IResponse>> {
        if (error.error instanceof ErrorEvent) {
            if (!environment.production) {
                /**
                 * !A client-side or network error occurred. Handle it accordingly.
                 * !in development mode printing errors in console
                 */
                this._logger.log('Request error ' + error);
            }
        } else {
            const httpErrorCode: number = error['status'];
            switch (httpErrorCode) {
                case StatusCodes.INTERNAL_SERVER_ERROR:
                    this._router.navigate(['/internal-server-error']);
                    break;
                case StatusCodes.UNAUTHORIZED:
                    return this.handle401Error(request, next);
                default:
                    this._logger.log('Request error ' + error);
                    break;
            }
        }

        return throwError(() => error.error || error);
    }

    
    private handle401Error(
       request: HttpRequest<any>,
       next: HttpHandler
    ): Observable<HttpEvent<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.tokenSubject.next(null);
            return this._authService.regenerateTokens().pipe(
                switchMap((apiResult) => {
                    const authData = apiResult.dataset as IAuthResult;
                    this._authService.updateRefreshedTokens(authData);

                    this.tokenSubject.next(authData.tokens.access_token);
                    return next.handle(
                        this.addTokenInHeader(
                            request,
                            authData.tokens.access_token
                        )
                    );
                }),
                catchError((error) => {
                    // If there is an exception calling 'refreshToken', bad news so logout.
                    this._authService.logout();
                    this._router.navigate(['/']);
                    return throwError(() => error);
                }),
                finalize(() => {
                    this.isRefreshingToken = false;
                })
            );
        } else {
            return this.tokenSubject.pipe(
                filter((token) => token !== null),
                take(1),
                switchMap((token) => {
                    return next.handle(this.addTokenInHeader(request, token));
                })
            );
        }
    }

    
    private addTokenInHeader(
        request: HttpRequest<any>,
        token: string | null
    ): HttpRequest<any> {
        return request.clone({
            setHeaders: { Authorization: 'Bearer ' + token }
        });
    }
}
1
  • You can use the expand operator and takeUntilyou got your token. Commented Apr 25, 2023 at 5:22

1 Answer 1

0

It sounds like you want to halt requests once you encounter 401 response, and continue them once the token has been refreshed.

One way to accomplish this is to declare your interceptor starting with the current token value, handle the request using that token, then catch 401 responses and initiate a refresh. This assumes you have a service that exposes the current token value as an observable and also a method to initiate a refresh.

export const authInterceptor: HttpInterceptorFn = (request, next) => {
  const authService = inject(AuthService);

  return authService.token$.pipe(
    filter(token => !!token),
    map(token => request.clone({setHeaders: { Authorization: `Bearer ${token}`}})),
    switchMap(authRequest => next(authRequest).pipe(
      catchError(error => {
        if(error.status === 401) {
          authService.refreshToken();
          return EMPTY; 
        }
        return throwError(() => error);
      })
    )),
    takeWhile(event => !(event instanceof HttpResponse), true)
  );
};

With the above code, you can see instead of directly calling next(request), we instead create an observable that starts with the token$ observable, filter out undefined tokens, apply the valid token to the header and continue the request.

We then catch potential errors in the response, initiating a token refresh. Since the refreshToken() method on the authService will cause token$ to emit undefined, any other requests will effectively be paused until a new token is emitted.

Finally, we use takeWhile to allow only one HttpResponse response (we don't want to repeat the request when the token changes after HttpResponse has been received).

Summary of each operator in the pipeline:

  • filter - pause execution until a non-null token is received
  • map - append the auth header to the request
  • switchMap - handle the request
  • catchError - identify 401 responses and initiate a token refresh; return EMPTY observable
  • throwError - forward unhandled error responses
  • takeWhile - limit to one HttpResponse; use inclusive: true to emit final response.

Here is a StackBlitz example.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.