Angular HTTP Interceptor executing inner http request twice

Viewed 1005

My application uses a JWT Token to authenticate with the backend. I have an angular Interceptor that catches any errors in an http call. This interceptor checks if the call returns a 401 Unauthorized response and if so, it makes a call to an endpoint to refresh the token and then retries the original call again.

My issue is that the call to the refreshToken endpoint is being done twice for some reason.

Here's my interceptor (edited for brevity):

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError((error: any) => {
        switch (error.status) {
          // unauthorized
          case 401:
            // the token is stored in an NGRX store, so retrieve it first
            return this.userStore.pipe(select(fromUser.getUserAccessToken)).pipe(
              take(1),
              mergeMap(userToken => {
                if (userToken) {
                  // token exists but may be invalid, try to refresh
                  return this.userService.refreshToken(userToken).pipe(
                    switchMap(refreshedToken => {
                      if (refreshedToken) {
                        ...
                      } else {
                        // token was not refreshed, delete session
                        ...
                      }
                    })
                  );
                }
              })
            );
        }
      })
    );
  }

return this.userService.refreshToken(userToken) is the statement that gets executed twice (using the same, original token), so the first call to refresh will go through, and the second call will obviously fail since its trying to refresh the same token again, resulting in the user session getting deleted.

Any ideas why?

Update: I'm still digging through this. I've put a bunch of breakpoints and console.logs in my code:

      catchError((error: any) => {
        >>>>> console.log('1');
        switch (error.status) {
          case 401:
            return this.userStore.pipe(select(fromUser.getUserAccessToken)).pipe(
              take(1),
              mergeMap(userToken => {
                >>>>> console.log('2');
                if (userToken) {
                  return this.userService.refreshToken(userToken).pipe(
                    switchMap(refreshedToken => {
                      >>>>> console.log('3');

Console.log #1 and #2 get printed out to the console just once, which means that the http error is only being caught once. But #3 gets printed twice, which I figure means either the call to the refresh endpoint is being made twice, or the observable is returning two values somehow?

3 Answers

Not sure why, but it seems like refreshToken outputs multiple values. This can easily be fix by the take operator. Something like this:

 return this.userService.refreshToken(userToken).pipe(
                take(1),
                switchMap(refreshedToken => {
                  >>>>> console.log('3');

Your backend call could be an OPTIONS request

Preflighted requests

Unlike simple requests (discussed above), "preflighted" requests first send an HTTP OPTIONS request header to the resource on the other domain, in order to determine whether the actual request is safe to send. Cross-site requests are preflighted like this since they may have implications to user data. In particular, a request is preflighted if:

It uses methods other than GET or POST. Also, if POST is used to send request data with a Content-Type other than application/x-www-form-urlencoded, multipart/form-data, or text/plain, e.g. if the POST request sends an XML payload to the server using application/xml or text/xml, then the request is preflighted. It sets custom headers in the request (e.g. the request uses a header such as X-PINGOTHER)

First, you really need to provide a StackBlitz or something similar with the bug so we can help diagnose this. That being said though, I was curious about this because I have had problems like this before so I did some research. Most of the examples I found show that the Observable returned by the refreshToken method is shared to prevent multiple subscriptions. In this answer https://stackoverflow.com/a/52720791/5799931 you can see that the this.authService.refreshToken() Observable is shared. I would try this

return this.userService.refreshToken(userToken).pipe(
       share(), <----- ADD THE SHARE HERE
       switchMap(refreshedToken => {
          if (refreshedToken) {
             ...
          } else {
            // token was not refreshed, delete session
            ...
          }
       })
   );

You can also share the Observable returned by HttpClient inside the refreshToken method.

Related