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 }
});
}
}
Resolver:
public resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<IApplicationEditDataset | null> {
const encryptedParams = route.params['id'];
const decryptedParams = decryption(encryptedParams);
if (decryptedParams === null) {
return this.handleError(
new Error('Failed to decrypt route parameters.')
);
}
const routeParams: IProductNodeRouteParams =
JSON.parse(decryptedParams);
const _modalities$ =
this._commonService.fetchProductHierarchyNodes('modality');
const _families$ = this._commonService.fetchProductHierarchyNodes(
'family',
routeParams.modalityId
);
const _applicationDetail$ = this._http.get(
`products/application/${routeParams.applicationId}`
);
return forkJoin([_modalities$, _families$, _applicationDetail$]).pipe(
map((apiResults) => {
const [modalities, families, applicationDetailResult] =
apiResults;
const applicationDetail: IProductSegment =
applicationDetailResult.dataset?.['application_details'];
applicationDetail.parent = routeParams;
this._commonService.setPageTitleAddon(
applicationDetail.name || null
);
return {
modalities_list: modalities,
families_list: families[0].children,
application_details: applicationDetail
};
}),
catchError((error: Error) => {
return this.handleError(error);
})
);
}