1

I have an interceptor that check for http response with status code 401 so that it can request refresh-token by calling refreshToken() before trying the initial request once more.

The refreshToken() observe refreshTokenLockSubject$ so that only one refresh-token request can be made, while the others will have to wait for the refresh-token request to be completed before trying the initial requests once more.

For instance, let say I have requests A, B, and C that returned status code 401. Request A got to make refresh-token request, while B and C will wait for A's refresh-token request to be completed before trying their initial requests once more.

@Injectable()
export class HttpHandlerInterceptor implements HttpInterceptor {
  /** unrelated codes **/

  intercept(req: HttpRequest<any>, next: HttpHandler): any {
    /** unrelated codes **/
    return next.handle(req).pipe(
      catchError((err) => {
        // handle unauthorized
        if (err.status == 401) return this.refreshToken(req, next, err);
        return throwError(() => err);
      })
    ) as Observable<any>;
  }

  /** Handle refresh token **/
  refreshToken(
    req: HttpRequest<any>,
    next: HttpHandler,
    err: any
  ): Observable<HttpEvent<any>> {
    return this.authService.refreshTokenLockSubject$.pipe(
      first(),
      switchMap((lock) => {
        // condition unlocked
        if (!lock) {
          this.authService.lockRefreshTokenSubject();
          return this.authService.refreshToken().pipe(
            switchMap(() => {
              this.authService.unlockRefreshTokenSubject();
              return next.handle(req);
            }),
            catchError((err) => {
              /** unrelated codes **/
              return throwError(() => err);
            })
          );
        // condition locked
        } else {
          return this.authService.refreshTokenLockSubject$.pipe(
            // only unlocked can pass through
            filter((lock) => !lock),
            switchMap(() => {
              return next.handle(req);
            }),
            catchError((err) => {
              /** unrelated codes **/
              return throwError(() => err);
            })
          );
        }
      })
    );
  }
}

The refreshTokenLockSubject$ is a boolean behavior subject:

@Injectable()
export class AuthService {
  /** unrelated codes **/
  private _refreshTokenLockSubject$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
  lockRefreshTokenSubject = () => this._refreshTokenLockSubject$.next(true);
  unlockRefreshTokenSubject = () => this._refreshTokenLockSubject$.next(false);

  get refreshTokenLockSubject$(): Observable<boolean> {
    return this._refreshTokenLockSubject$.asObservable();
  }

  // requests A, B, and C were constructed similar to this
  refreshToken = (): Observable<any> =>
    this.http.post<any>(
      this.authEndpoint(AUTH_ENDPOINT['refreshToken']),
      {},
      { withCredentials: true }
    );
}

Continue with the example above (requests A, B, and C), I noticed that only request A finalize get called, while requests B and C were not.

Request A:

this.companyService
    .A(true)
    .pipe(finalize(() => (this.loading = false)))
    .subscribe({
      next: (res) => { /** unrelated codes **/ },
    });

Request B and C:

forkJoin({
  bRes: this.companyService.B(),
  cRes: this.companyService.C()
})
  .pipe(finalize(() => (this.loading = false)))
  .subscribe({
    next: ({ bRes, cRes}) => {
       /** unrelated codes **/
    },
  });

Whether requests B and C are a forkJoin does not have an impact as far as I observed.

The B and C subscription complete callback were also not called.

Does anyone know why finalize is not called?

Besides that, does this mean the B and C subscriptions were never unsubscribed?

ytan11
  • 908
  • 8
  • 18
  • `forkJoin()` requires all source Observables to emit at least once and to complete. Do the methods `B()` and `C()` emit a value? Try this in the `forkjoin`: `bRes: this.companyService.B().pipe(first()), cRes: this.companyService.C().pipe(first())` in order to emit at least one value – derstauner Jan 04 '23 at 08:14
  • @derstauner Interesting. This seems to solve the issue. However, B() and C() are also http observable so they should emit at least once, and they indeed do `next`, Just not `complete` nor `finalize`. Why didn't they? – ytan11 Jan 05 '23 at 03:24
  • Because calling them doesn't simply mean, that they will complete. You have to care yourself of it. – derstauner Jan 05 '23 at 07:23

1 Answers1

1

As per definition, forkjoin does the following: when all observables complete, it emits the last emitted value from each. The stress is here on complete.

One can think, that calling a http get method within forkjoin may be sufficient, but it isn't. The observable has to be completed. This can be done f. e. with the operators first() or take.

You can f.e. do this:

bRes: this.companyService.B().pipe(first()),   
cRes: this.companyService.C().pipe(first())
derstauner
  • 1,478
  • 2
  • 23
  • 44
  • Thanks for the answer. However, this conflict with the [answer by born2net](https://stackoverflow.com/a/35043309/7099900) on the question: "Is it necessary to unsubscribe from observables created by Http methods?" – ytan11 Jan 05 '23 at 11:35
  • Furthermore, the forkJoin indeed `complete` whenever the response was not intercepted to make the `refresh-token` request. – ytan11 Jan 05 '23 at 11:36