RxJs: Mechanics of defer + share(on tokens refresh example)

Viewed 125

I have the following code snippet:

@Injectable()
class RefreshTokenInterceptor implements HttpInterceptor {
  private readonly renewTokens$: Observable<string | undefined> = defer(() =>
    this.tokens.accessToken$.pipe(
      take(1),
      switchMap((accessToken: string | undefined) =>
        accessToken
          ? this.auth.getAccessTokenSilently()
          : this.auth.loginWithRedirect().pipe(switchMap(() => this.auth.getAccessTokenSilently()))
      ),
      tap((accessToken: string) => this.tokens.renewAccessToken(accessToken)),
      share()
    )
  );

  constructor(private readonly auth: AuthService, private readonly tokens: TokenService) {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    return next.handle(request).pipe(
      catchError((error: unknown) =>
        error instanceof HttpErrorResponse && error.status === 401
          ? this.renewTokens$.pipe(
              switchMap(
                (accessToken: string | undefined) => next.handle(withBearerTokenInAuth(request, accessToken))
              )
            )
          : throwError(() => error))
    );
  }
}

The part I'm curios about is renewTokens$ observable and the way it works. Let's imagine the situation where our accessToken has expired and we did 3 parallel request which require authorization. The results in network tab for the snippet above would be following: enter image description here

The next things happened:

  1. All 3 requests failed with 401
  2. Request for refreshment of accessToken has been fired
  3. All 3 previously failed requests were retried with the new token and, as a result, succeeded.

It's not really clear for me why we have only one request for token(while it's a desired behaviour though). After a little debugging I see that factory function passed to defer is being invoked 3 times(because of 3 requests) and as I understand each time new shared observable is returned. How come that all of the requests wait for one same observable to emit value? I thought that each request is supposed to receive a separate observable returned from the factory function of defer(meaning that each unauthorized request should fire separate token request)

UPD

Implementation of TokenService:

@Injectable({
  providedIn: 'root'
})
export class TokenService {
  private readonly accessTokenSubj: BehaviorSubject<string | undefined> =
    new BehaviorSubject<string | undefined>(undefined);

  constructor(private readonly auth: AuthService) {
    auth.isAuthenticated$.pipe(
      switchMap(
        (isAuthenticated: boolean) => isAuthenticated ? auth.getAccessTokenSilently() : of(undefined)
      )
    ).subscribe((accessToken: string | undefined) => this.accessTokenSubj.next(accessToken))
  }

  renewAccessToken(accessToken: string) {
    this.accessTokenSubj.next(accessToken);
  }

  get accessToken$(): Observable<string | undefined> {
    return this.accessTokenSubj.asObservable();
  }
}

AuthService is provided by @auth0/auth0-angular

1 Answers

You need to put share()outside the defer().

@Injectable()
class RefreshTokenInterceptor implements HttpInterceptor {
  private readonly renewTokens$: Observable<string | undefined> = defer(
    () => this.tokens.accessToken$.pipe(
      /* ... */
    )
  ).pipe(share())
}

The observable you get from defer(fn) always calls fn on subscribe, so while you're technically getting a shared observable, you're getting a different one each time.

Additionally, I'm guessing you'll want to get the last emitted token on subscribe, in which case you can swap out share() for shareReplay(1).

Related