'React Apollo Link - How to forward operation after a promise has resolved?

So I figured out how to setup a middleware to handle my auth tokens, as well as getting new ones, if need be. The problem is that, there is an edge case here when, after the promise is resolved, the operation gets forwarded without the proper headers set, leading to another call that could potentially be unauthenticated. I feel the trick here is pretty straightforward, but I can't seem to figure it out. Is there a way to return the results from a promise back up to the enclosed function? I haven't found much luck regarding this, but perhaps there is another way. Here is the code for setting up my middleware and Apollo client:

const authLink = new ApolloLink((operation, forward) => {
  operation.setContext(({ headers = {} }) => {
    const token = localStorage.getItem('token');
    const tokenExp = token ? decodeJWT(token).exp : null;
    const currentTime = Date.now() / 1000;

    if(token && tokenExp >= currentTime) {
      // Check if token is expired. If so, get a new one and THEN
      // move forward
      headers = { ...headers,authorization: token ? `Bearer ${token}` : "", };
      return { headers };
    } else {

    // TODO: This would be replaced with the token service that actually
    // takes an expired token and sends back a valid one
    return fetch('http://localhost:4000/topics', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          query: `mutation LOGIN_USER(
            $email: String
            $password: String!
          ) {
            login(email: $email, password: $password) {
              id
              token
            }
          }
        `,
          variables: {
            email: "[email protected]",
            password: "test"
          }
        }),
      }).then(response => {
        return response.json()
      })
      .then(({ data: { login: { token } }}) => {
        // Put updated token in storage
        localStorage.setItem('token', token);
        headers = { ...headers,authorization: token ? `Bearer ${token}` : "", };
        return { headers };
      });
    }
  });
  return forward(operation);
});


/**
 * Setup the URLs for each service
 */
const httpTopicsServiceLink = createHttpLink({
  uri: 'http://localhost:4000/topics',
});

/**
 * Create the client instance for each GraphQL server URL
 */
export const TopicsClient = new ApolloClient({
  link:authLink.concat(httpTopicsServiceLink),
  cache: new InMemoryCache(),
});


Solution 1:[1]

You can return your own Promise that will resolve either with the headers or another Promise from your fetch request:

const authLink = new ApolloLink(async (operation, forward) => {
  return await operation.setContext(({ headers = {} }) => {
    const token = localStorage.getItem('token');
    const tokenExp = token ? decodeJWT(token).exp : null;
    const currentTime = Date.now() / 1000;

    return new Promise((resolve, reject) => {
        if(token && tokenExp >= currentTime) {
      // Check if token is expired. If so, get a new one and THEN
      // move forward
      headers = { ...headers,authorization: token ? `Bearer ${token}` : "", };
      resolve({ headers });
    } else {

    // TODO: This would be replaced with the token service that actually
    // takes an expired token and sends back a valid one
    resolve(fetch('http://localhost:4000/topics', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          query: `mutation LOGIN_USER(
            $email: String
            $password: String!
          ) {
            login(email: $email, password: $password) {
              id
              token
            }
          }
        `,
          variables: {
            email: "[email protected]",
            password: "test"
          }
        }),
      }).then(response => {
        return response.json()
      })
      .then(({ data: { login: { token } }}) => {
        // Put updated token in storage
        localStorage.setItem('token', token);
        headers = { ...headers,authorization: token ? `Bearer ${token}` : "", };
        return { headers };
      }));
    }
    });

  }).then(res => {
    return forward(operation);
  });
});

Can't test this so I may have missed something, but that should ensure the request is finished before forwarding.

Solution 2:[2]

To be able to add a promise for an ApolloLink you can add fromPromise and toPromise

import { ApolloLink, fromPromise, toPromise } from '@apollo/client';

const withToken = new ApolloLink((operation, forward) => {
  return fromPromise(
    fetch(...).then(({ token }) => {
      operation.setContext(({ headers }) => ({
        headers: {
          ...headers,
          authorization: `Bearer ${token}`,
        },
      }));
      return toPromise(forward(operation));
    }),
  );
});

ApolloLink expects an Observable which fromPromise will help convert the promise from your fetch to the correct type.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2