Home > OS >  How to handle JWT Token Refresh cycle with React and RTK Query: Refetching the failed queries automa
How to handle JWT Token Refresh cycle with React and RTK Query: Refetching the failed queries automa

Time:08-26

In my application I have a token refresh endpoint /refresh which sets new JWT refresh token in a cookie and sends back new JWT access token as json.

Since access token expires after 5 minutes, I need to implement an under the hood refresh logic to kick in whenever access token is expired.

Currently what I am doing is to call the api through RTK Query and if api rejects the query, I call the refreshTokens mutation.

And I need to put this logic in all api queries like this:

 updateProfile(body)
      .unwrap()
      .catch((error) => {
        if (error && error.status === 403) {
          refreshTokens(null); // RTK Query Mutation
          updateProfile(body); // RTK Query Mutation
        }
      });

Doing this seems repetition of code since it needs to be implemented to all the calls to the api.

I wonder if there is a global solution to automatically call the refresh token endpoint when the queries are rejected.

CodePudding user response:

Hi I have done something similar to what you have done ,, and without the need to repeat the code to each API call,,

I have created my own fetch function which will receive two parameters, one is a function that call the fetch function and the other one is a function that calls the refresh token.

const fetchOrRefresh = async (fetchFun, refreshFun) => {
  // first fetch call
  let fetchResponse = await fetchFun();
  let refreshResponse;

  // check if the call was ok, in my case api is returning and object that 
  // contains error object
  if (Object.keys(fetchResponse).includes("error")) {
    refreshResponse = await refreshFun();
  }

  // check if the refresh call was successful 
  if (refreshResponse && Object.keys(refreshResponse).includes("error")) {
  // if not ,, return anything or thow an error
    return null;
  }

  // recall the fetch and return what it returns
  return fetchFun();
};

Maybe you can refactor it using the returned information you will get from the error message to build it in a better way, or to build it as a middleware...

CodePudding user response:

NOTE: This solution automatically refreshes tokens after failed fetch but doesnt refetch the initial failed query automatically. So it still causes repetition of code.

This is how i solved it so far:

  1. Created a middleware to catch authorization errors on macro level based on this doc. It catches the authorization errors and calls the mutation which refreshes JWTs, without using React hooks which is explained in this doc. Also its important to attach error handling middleware before RTK Query middleware.
const jwtTokenRefresher =
  ({ dispatch }: Record<any, any>) =>
  (next: any) =>
  async (action: any) => {
    if (isRejectedWithValue(action)) {
      // Catch the authorization error and refresh the tokens
      if (action.payload.status === 403) {
        console.warn('We got a rejected action!');
        await dispatch(
          api.endpoints.refreshTokens.initiate(null)
        );
      }
    }

    return next(action);
  };

export const store = configureStore({
  reducer: {
//...
    [api.reducerPath]: api.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware()
      .concat(jwtTokenRefresher)
      .concat(api.middleware),
});
      
  1. After we get new tokens by middleware, we are calling the initial mutation within its try/catch block again 'without react hooks'.
update: build.mutation({
      query: (body) => ({
        url: //,
        method: 'POST',
        body,
      }),
      async onQueryStarted(
        arg,
        { dispatch, getState, queryFulfilled, requestId, extra, getCacheEntry }
      ) {
        try {
     //
        } catch (error: any) {
          if (error.error.status === 403) {
            await dispatch(api.endpoints.update.initiate(arg));
          }
        }
      },
    }),

So far it works as I wanted it to be. My only concern is the need to catch error on all the mutations and calling the mutation again. I would be interested if anyone has any idea to handle it globally as well.

CodePudding user response:

On my initial solution, I was able to refetch JWTs if access token is expired but I was unable to refetch the initial failed query automatically for all endpoints. So I had to write try-catch blocks to all endpoints. I didnt want to edit the initial answer not to make the solution look complicated, and its also a valid answer.

Here is the final solution where Redux RTK catches authorization error globally and refreshes the JWTs and after refetches the initial failed query automatically.

Same middleware like in my initial answer. But this time refreshTokens is not a mutation but a query.

// middleware for redux store

const jwtTokenRefresher =
  ({ dispatch }: Record<any, any>) =>
  (next: any) =>
  async (action: any) => {
    if (action && isRejectedWithValue(action)) {
      // Catch the authorization error and refresh the tokens
      console.warn('We got a rejected action!', action.payload.status);
      // console.log({ action });
      if (action.payload.status === 403) {
        const { endpointName, originalArgs } = action.meta.arg;
        // console.log({ type, originalArgs });
        await dispatch(setRejectedAction({ endpointName, originalArgs }));
        await dispatch(backendApi.util.invalidateTags(['Tokens']));
      }
    }

    return next(action);
  };

  1. We retrigger token refresh by .invalidateTagsmethod
  2. We get the meta data of the rejected query from action.meta and save it to the store to regenerate the failed query after we refresh the tokens.

Then we use onQueryStarted method of RTK Query to wait for token refresh to complete:

// query inside createApi

refreshTokens: build.query({
      query: () => ({
        url: API_ROUTE.REFRESH_TOKENS,
        method: 'PATCH',
      }),
      providesTags: ['Tokens'],
      async onQueryStarted(
        arg,
        {
          dispatch,
          getState,,
          queryFulfilled,
          getCacheEntry
        }
      ) {
        await queryFulfilled;

        if (getCacheEntry().isSuccess) {
          const state = <Record<any, any>>getState();
          const {
            app: { rejectedAction = {} },
          } = state;
          const { endpointName, originalArgs } = rejectedAction;
          // console.log({ rejectedAction });
          const queryTrigger = <ThunkAction<any, any, any, any>>(
            backendApi.endpoints[
              endpointName as keyof typeof backendApi.endpoints
            ].initiate(originalArgs)
          );
          await dispatch(queryTrigger);
        }
      },
    }),
  1. await queryFulfilled makes sure the tokens are refreshed.
  2. Get the meta data from the store by const { endpointName, originalArgs } = rejectedAction;
  3. And regenerate the inital query.

Thanks to this flow, Redux RTK Query catches all the the failed queries, refreshes JWTs and refetches failed queries automatically.

  • Related