I'm working on an Angular application with an HTTP interceptor for handling authentication tokens. I'm facing a couple of challenges and need guidance on best practices.
The interceptor currently checks if the request is a refresh token request using a separate function (isRefreshTokenRequest). I'd like to simplify the interceptor by removing this check. However, I realize that removing this check could potentially cause an infinite loop, as the refresh token request itself would trigger the interceptor. How can I handle this situation without explicitly checking for the refresh token endpoint?
import { inject } from '@angular/core';
import { HttpErrorResponse, HttpEvent, HttpHandlerFn, HttpRequest } from '@angular/common/http';
import { BehaviorSubject, catchError, debounceTime, filter, Observable, switchMap, take, throwError } from 'rxjs';
import { AuthService } from '../services/auth/auth.service';
import { AuthResponse } from '../interfaces/auth-response';
let isRefreshing = false;
const refreshTokenSubject: BehaviorSubject<string | null> = new BehaviorSubject<string | null>(null);
export function authInterceptor(request: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
const authService = inject(AuthService);
if (authService.accessToken()) {
request = addToken(request, authService.accessToken()!);
}
if (isRefreshTokenRequest(request)) {
return next(request);
}
function handle401Error(request: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
if (!isRefreshing) {
isRefreshing = true;
refreshTokenSubject.next(null);
return authService.refreshToken().pipe(
switchMap((response: AuthResponse) => {
isRefreshing = false;
refreshTokenSubject.next(response.accessToken);
return next(addToken(request, response.accessToken));
}),
catchError((error) => {
isRefreshing = false;
if (
error instanceof HttpErrorResponse &&
error.status === 401 &&
(error.error?.message === 'Token expired' || error.error?.message === 'Invalid token')
) {
authService.logOut();
}
return throwError(() => error);
}),
);
}
return refreshTokenSubject.pipe(
filter((token) => token !== null),
take(1),
switchMap((token) => next(addToken(request, token!))),
);
}
return next(request).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
return handle401Error(request, next);
}
return throwError(() => error);
}),
);
}
function addToken(request: HttpRequest<unknown>, token: string) {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
},
});
}
function isRefreshTokenRequest(request: HttpRequest<unknown>): boolean {
return request.url.includes('/api/authentication/refresh-token');
}