Search code examples
angularrxjsjwtrxjs-observablesnebular

A new duplicate sign-out HTTP request is sent every time the user logs out: Angular, RxJs


Issue description

First of all, let me start by saying that I am using Angular 10 with the Nebular UI Library for the front-end, Node.js for the back-end API, and JWT with the email/password strategy for authentication. I have noticed that for every time the user sings-in and signs back out without refreshing the application, a new duplicate sign-out request is made to the server (multiple http requests are being sent out). If you refresh the application after you sign back out though, the problem goes away. I'm not sure if I'm skipping something or I'm simply ignorant on the right way to log out and sign back in using JWTs, but I've been trying to find a solution to this problem for days now with no success so I'm eager for some help.

Current behavior:

If the user were to sign in and logs back out again more than once, the sign-out request made to the server is duplicated. This issue persists REGARDLESS of if you use an http interceptor (NbAuthJWTInterceptor or otherwise).

Expected behavior:

If the user were to sign in and log back out again, there should be NO redundant sign-out requests made to the server regardless of how many times the user repeats these steps without refreshing the app.

Steps to reproduce:

  1. The first time the user signs in everything works fine and there are no duplicate requests made to the server when you log out.
  2. After you sign back in for the 2nd time and sign out for the 2nd time without refreshing the application, the 2nd sign out request you make to the server will send out a duplicate sign-out request (2 identical sign-out requests are sent to the server).
  3. If the user signs in again for a 3rd time and signs back out for a 3rd time, then 3 sign-out requests will be sent to the server (a total of 3 identical requests sent out).
  4. If the user were to sign in and log back out again, the sign-out request would sent be duplicated one more time and a total of 4 identical sign-out requests would be sent out. This continues indefinitely.

Here is a screenshot from my dev-tools network tab for these 4 steps (after signing-in and signing back out 4 times): duplicates

Related code: On the client side I have the header.component.ts file from which the sign out process is initiated:

...
ngOnInit() {
    // Context Menu Event Handler.
    this.menuService.onItemClick().pipe(
      filter(({ tag }) => tag === 'my-context-menu'),
      map(({ item: { title } }) => title),
    ).subscribe((title) => {
      // Check if the Logout menu item was clicked.
      if (title == 'Log out') {

        // Logout the user.
        this.authService.logout('email').subscribe(() => {
          // Clear the token.
          this.tokenService.clear()
          // Navigate to the login page.
          return this.router.navigate([`/auth/login`]);
        });

      }
      if (title == 'Profile') {
        return this.router.navigate([`/pages/profile/${this.user["_id"]}`]);
      }
    });
}
...

On the server side, there is the sign-out API route that returns a successful 200 response:

// Asynchronous POST request to logout the user.
router.post('/sign-out', async (req, res) => {
    return res.status(200).send();
});

Solution

  • You’re subscribing inside of another subscription. This causes another subscription to be made each time this.menuService.onItemClick() is called.

    You need to use a flattening strategy by using the proper Rxjs operator (exhaustMap, concatMap, switchMap, mergeMap).

    In your case I would refactor like this (don’t forget to unsubscribe to each subscription in ngOnDestroy)

    const titleChange$ = this.menuService.onItemClick()
      .pipe(
        filter(({ tag }) => tag === 'my-context-menu'),
        map(({ item: { title } }) => title)
      );
    
    this.logOutSubscription = titleChange$.pipe(
      filter((title) => title == 'Log out'),
      exhaustMap((title) => this.authService.logout('email')),
      tap(() => {
        this.tokenService.clear()
        this.router.navigate([`/auth/login`]);
    })
    .subscribe();
    
    this.profileNavSubscription = titleChange$
      .pipe(
        filter((title) => title == 'Profile'),
        tap(title => {
          this.router.navigate([`/pages/profile/${this.user["_id"]}`])
        })
       .subscribe();
    

    `