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?