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?