React native Refresh works but next call still uses the last token

I have a slightly different setup in handling. Instead of handling the refresh token logic in middleware, I define it as helper function. This way I can do all token validation right before any network request where I see fit, and any redux action that doesn't involves a network request will not needed this function

export const refreshToken = async () => {
  let valid = true;

  if (!validateAccessToken()) {
    try {
      //logic to refresh token
      valid = true;
    } catch (err) {
      valid = false;
    }

    return valid;
  }
  return valid;
};

const validateAccessToken = () => {
  const currentTime = new Date();

  if (
    moment(currentTime).add(10, 'm') <
    moment(jwtDecode(token).exp * 1000)
  ) {
    return true;
  }
  return false;
};

Now that we have this helper function, I call it for all redux action that needed

const shouldRefreshToken = await refreshToken();
    if (!shouldRefreshToken) {
      dispatch({
        type: OPERATION_FAILED,
        payload: apiErrorGenerator({ err: { response: { status: 401 } } })
      });
    } else { 
      //...
    }

In your middleware you are making store.dispatch asynchronous, but the original signature of store.dispatch is synchronous. This can have serious side effects.

Let's consider a simple middleware, that logs every action that happens in the app, together with the state computed after it:

const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

Writing the above middleware is essentially doing the following:

const next = store.dispatch  // you take current version of store.dispatch
store.dispatch = function dispatchAndLog(action) {  // you change it to meet your needs
  console.log('dispatching', action)
  let result = next(action) // and you return whatever the current version is supposed to return
  console.log('next state', store.getState())
  return result
}

Consider this example with 3 such middleware chained together:

const {
  createStore,
  applyMiddleware,
  combineReducers,
  compose
} = window.Redux;

const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;

    default:
      return state;
  }
};

const rootReducer = combineReducers({
  counter: counterReducer
});


const logger = store => next => action => {
  console.log("dispatching", action);
  let result = next(action);
  console.log("next state", store.getState());
  return result;
};

const logger2 = store => next => action => {
  console.log("dispatching 2", action);
  let result = next(action);
  console.log("next state 2", store.getState());
  return result;
};

const logger3 = store => next => action => {
  console.log("dispatching 3", action);
  let result = next(action);
  console.log("next state 3", store.getState());
  return result;
};

const middlewareEnhancer = applyMiddleware(logger, logger2, logger3);

const store = createStore(rootReducer, middlewareEnhancer);

store.dispatch({
  type: "INCREMENT"
});

console.log('current state', store.getState());
<script src="https://unpkg.com/[email protected]/dist/redux.js"></script>

First logger gets the action, then logger2, then logger3 and then it goes to the actual store.dispatch & the reducer gets called. The reducer changes the state from 0 to 1 and logger3 gets the updated state and propagates the return value (the action) back to logger2 and then logger.

Now, lets consider what happens when you change the store.dispatch to a async function somewhere in the middle of the chain:

const logger2 = store => next => async action => {
  function wait(ms) {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve();
      }, ms);
    });
  }
  await wait(5000).then(v => {
    console.log("dispatching 2", action);
    let result = next(action);
    console.log("next state 2", store.getState());
    return result;
  });
};

I have modified logger2, but logger (the one up the chain) has no idea that the next is now asynchronous. It will return a pending Promise and will come back with the "unupdated" state because the dispatched action had not reached the reducer yet.

const {
  createStore,
  applyMiddleware,
  combineReducers,
  compose
} = window.Redux;

const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;

    default:
      return state;
  }
};

const rootReducer = combineReducers({
  counter: counterReducer
});


const logger = store => next => action => {
  console.log("dispatching", action);
  let result = next(action); // will return a pending Promise
  console.log("next state", store.getState());
  return result;
};

const logger2 = store => next => async action => {
  function wait(ms) {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve();
      }, ms);
    });
  }
  await wait(2000).then(() => {
    console.log("dispatching 2", action);
    let result = next(action);
    console.log("next state 2", store.getState());
    return result;
  });
};

const logger3 = store => next => action => {
  console.log("dispatching 3", action);
  let result = next(action);
  console.log("next state 3", store.getState());
  return result;
};

const middlewareEnhancer = applyMiddleware(logger, logger2, logger3);

const store = createStore(rootReducer, middlewareEnhancer);

store.dispatch({ // console.log of it's return value is too a pending `Promise`
  type: "INCREMENT"
});

console.log('current state', store.getState());
<script src="https://unpkg.com/[email protected]/dist/redux.js"></script>

So my store.dispatch returns immediately from the chain of middleware with that pending Promise and console.log('current state', store.getState()); still prints 0. The action reaches original store.dispatch and the reducer looong after that.


I don't know your whole setup, but my guess is something like that is happening in your case. You are assuming your middleware has done something and made the round trip, but actually it hasn't finished the job (or no one awaited him to finish it). May be you are dispatching an action to fetch /templates and since you wrote a middleware to auto update the bearer token, you are assuming the fetch helper utility will be called with a brand new token. But the dispatch has returned early with a pending promise and your token is still the old one.

Apart from that, only one thing seems wrong visibly: you are dispatching the same action twice in your middleware via next:

const tokenMiddleware = store => next => async action => {
  if (something) {
    if (something) {
      await fetch('/token/refresh',)
        .then(async (data) => {
            return await Promise.all([
              // ...
            ]).then((values) => {
              return next(action); // First, after the `Promise.all` resolves
            });
        });
      return next(action); // then again after the `fetch` resolves, this one seems redundant & should be removed
    } else {
      return next(action);
    }
  }

Recommendations:

  1. Keep your tokens in redux store, persist them in storage and re-hydrate the redux store from storage
  2. Write one Async Action Creator for all api calls, that will refresh the token if necessary and dispatch an action asynchronously only after token has been refreshed.

Example with redux thunk:

function apiCallMaker(dispatch, url, actions) {
  dispatch({
    type: actions[0]
  })

  return fetch(url)
    .then(
      response => response.json(),
      error => {
        dispatch({
          type: actions[2],
          payload: error
        })
      }
    )
    .then(json =>
      dispatch({
        type: actions[1],
        payload: json
      })
    )
  }
}

export function createApiCallingActions(url, actions) {
  return function(dispatch, getState) {

    const { accessToken, refreshToken } = getState();
    if(neededToRefresh) {
      return fetch(url)
        .then(
          response => response.json(),
          error => {
            dispatch({
              type: 'TOKEN_REFRESH_FAILURE',
              payload: error
            })
          }
        )
        .then(json =>
          dispatch({
              type: 'TOKEN_REFRESH_SUCCESS',
              payload: json
          })
          apiCallMaker(dispatch, url, actions)
        )
    } else {
      return apiCallMaker(dispatch, url, actions)
    }
}

You would use it like so:

dispatch(createApiCallingActions('/api/foo', ['FOO FETCH', 'FOO SUCCESS', 'FOO FAILURE'])

dispatch(createApiCallingActions('/api/bar', ['BAR FETCH', 'BAR SUCCESS', 'BAR FAILURE'])

You have a race condition of requests and there is no right solution which will totally solve this problem. Next items can be used as a starting point for solving this issue:

  • Use token refresh separately and wait for its execution on the client side, e.g. send token refresh (smth like GET /keepalive) in case any request was sent in half period of the session timeout - this will lead to the fact that all requests will be 100% authorized (Option that I'd definitely use - it can be also used to track not only requests but events)
  • Cleanup token after receiving 401 - you won't see working application after reload assuming that deletion of valid token in case of boundary scenarios is positive scenario (Simple to implement solution)
  • Repeat query that received 401 with some delay (not the best option actually)
  • Force token updates more frequently then the timeout - changing them at 50-75% of timeout will reduce amount of failing requests (but they will still persist if user was iddle for the all session time). So any valid request will return new valid token which will be used instead of the old one.

  • Implement token extension period when old token can be counted valid for the transfer period - old token is extended for some limited time in order to bypass the problem (sounds not very good but it is an option at least)