0
votes

I have been struggling to asynchronously refresh the token in Apollo client, when an Unauthenticated error occurs. I've reviewed several resources on this, like StackOverflow answers, this Github issue and this blog post.

The main thing is to wait before retrying the failed request, after the new token is obtained. Reading the comments in the blog above and the comments in the Github issue, it seems that many people have successfully managed to solve this, however, I get the data with the new token, after a retry is done, which means that the retry is still done with the old token, so it fails.

Here's the function that executes the refresh mutation.


export const getNewToken = (client) => {
  const refreshToken = getRefreshToken();
  return client.mutate({
    mutation: REFRESH_TOKEN,
    variables: { refreshToken },
  });
};

Here's the Auth Link, I do a console.log of the token, to know which token is used.

const authLink = setContext((_, { headers }) => {
  // get the authentication token from local storage if it exists
  const token = localStorage.getItem("token");
  console.log("Token in auth link: ", token);
  return {
    headers: {
      ...headers,
      authorization: token ? `JWT ${token}` : "",
    },
  };
});

Then in Apollo's onError function, I tried to recreate the third example in the linked blog above:

if (token && refreshToken) {
  return fromPromise(
    getNewToken(client)
      .then(({ data: { refreshToken } }) => {
        console.log("Promise data: ", refreshToken);
        localStorage.setItem("token", refreshToken.token);
        localStorage.setItem("refreshToken", refreshToken.refreshToken);
        return refreshToken.token;
      })
      .catch((error) => {
        // Handle token refresh errors e.g clear stored tokens, redirect to login, ...
        console.log("Error after setting token: ", error);
        return;
      })
  )
    .filter((value) => {
      console.log("In filter: ", value);
      return Boolean(value);
    })
    .flatMap(() => {
      console.log("In flat map");
      // retry the request, returning the new observable
      return forward(operation);
    });
}

There are two problems with the execution of this: 1. The filter and flatMap functions aren't called at all. 2. The new token is received AFTER authLink has logged a call with the old token. Which means that forward(operation) doesn't wait for the new token.

I also tried the concurrent request from the linked blog. There's more going on in there, so here's the whole src/index.js in a Gist https://gist.github.com/bzhr/531e1c25a4960fcd06ec06d8b21f143b

Here forward$.flatMap doesn't get called AT ALL. And the same problems as the example above.

I am not sure where's my error, since many people have commented on the blog that it's working for them. I've checked my code multiple times. I need some help in debugging this. The main thing being to wait with the retry, until the new token is ready.

1

1 Answers

0
votes

I was also this facing issue but finally got it working.

  1. Instead of fromPromise. I used a util method which uses Observables to subscribe to our Promise .then method.
const _promiseToObservable = promiseFunc =>
  new Observable(subscriber => {
    promiseFunc.then(
      value => {
        if (subscriber.closed) return;
        subscriber.next(value);
        subscriber.complete();
      },
      err => subscriber.error(err),
    );
    return subscriber; // this line can removed, as per next comment
  });

After that in OnError call your getNewToken() method using this util method instead of fromPromise.

  1. Remove the for-loop if you are using for GraphQlErrors. Take first array element graphQlErrors[0], which generally contains our error data.

Making these two changes worked for me.