angularrxjsangular-http-interceptors

New Interceptors from Angular 17, local use of a BehaviorSubject does not work


In the new definition of interceptors in Angular 17, the local use of a BehaviorSubject does not work.

I have an interceptor that seeks to fulfill the use case of the refreshToken by seeking to generate a queue of the http requests while the refreshToken process is being processed.

For this I use a BehaviorSubject locally that can update the value of the response of the refreshToken process and then through the Observable process the pending requests.

However, this BehaviorSubject never updates or emits the next value and the queue is not processed with the update of the pending requests with the new access token.

export const RequestHeadersInterceptor: HttpInterceptorFn = (request, next) => {

  const logger: NGXLogger = inject(NGXLogger);
  const refreshTokenManageService: RefreshTokenManageService = inject(RefreshTokenManageService);
  const authService: AuthService = inject(AuthService);
  const notificationService: NotificationService = inject(NotificationService);
  const router: Router = inject(Router);

  refreshTokenManageService.isRefreshing = false;

  if (request.url === URL_AUTH_LOGIN) {
    return next(request);
  }

  if (request.url === URL_AUTH_REFRESH_TOKEN) {
    const requestClone = refreshTokenManageService.addTokenHeader(request);
    return next(requestClone);
  }

  if (refreshTokenManageService.isRefreshing) {
    return EMPTY;
  }

  const usuarioDataStorage = refreshTokenManageService.getUsuarioDataStorage();

  if (!usuarioDataStorage || !usuarioDataStorage.accessToken) {
    router.navigateByUrl('/').then();
    return EMPTY;
  }
  const requestAuthClone = refreshTokenManageService.addTokenHeader(request);
  const requestContentTypeClone = refreshTokenManageService.addContentTypeHeader(requestAuthClone);

  const refreshTokenSubject: BehaviorSubject<RefreshTokenResponse> = new BehaviorSubject<RefreshTokenResponse>({
    accessToken: '', refreshToken: ''
  });

  return next(requestContentTypeClone).pipe(
    catchError((requestError: HttpErrorResponse) => {
      if (requestError && requestError.status === HttpStatusCode.Unauthorized) {
        if (!refreshTokenManageService.isRefreshing) {
          refreshTokenManageService.isRefreshing = true;
          refreshTokenSubject.next({
            accessToken: '', refreshToken: ''
          });

          return authService.refreshToken().pipe(
            switchMap((refreshTokenResponse: RefreshTokenResponse) => {
              refreshTokenManageService.isRefreshing = false;
              refreshTokenSubject.next(refreshTokenResponse);
              refreshTokenManageService.updateTokens(refreshTokenResponse.accessToken, refreshTokenResponse.refreshToken);

              const requestBearerClone = refreshTokenManageService.addTokenHeader(request);
              const requestContentTypeClone = refreshTokenManageService.addContentTypeHeader(requestBearerClone);
              return next(requestContentTypeClone);
            }),
            catchError((error: HttpErrorResponse) => {
              refreshTokenManageService.isRefreshing = false;
              const authenticationError: AuthenticationError = new AuthenticationError(error.error.message);
              router.navigateByUrl('/').then();
              throw authenticationError;
            }),
          )
        } else {
          return refreshTokenSubject.pipe(
            filter((result: RefreshTokenResponse) => {
              return result.accessToken !== ''
            }),
            take(1),
            switchMap(() => {
              const requestBearerClone = refreshTokenManageService.addTokenHeader(request);
              const requestContentTypeClone = refreshTokenManageService.addContentTypeHeader(requestBearerClone);
              return next(requestContentTypeClone)
            })
          );
        }
      } else {
        return throwError(() => {
          const serverError: ServerError = new ServerError('Por el momento el servicio no se encuentra disponible. Favor de intentar más tarde.')
          throw serverError;
        });
      }
    })
  );
}

I would expect the BehaviorSubject to emit the new value of the access token with which the filter is activated, the headers to be updated and the requests that are made while the refreshToken process is taking place to be processed.


Solution

  • You build up the refreshTokenSubject only locally once for each request. The else-path where you return refreshTokenSubject.pipe(... just uses your initially created object-value and won't be updated anywhere.

    The next request (that you like to add to the queue as well?) will build up its own new subject and won't get any update of it ever, neither.

    Probably you want to keep its data somewhere outside the interceptor so all requests will be informed about its change? Maybe you should even consider some state dataIsCurrentlyGenerating that prevents generating new data as long as a previous request already has triggered one.