reduxjs/redux-toolkit

RTK Query prepare headers : fetchBaseQuery not able to get token, but customBaseQuery using axios is able to

thoughtworks-tcaceres opened this issue ยท 3 comments

Hello,
I have a simple class named TokenManager that sets and gets a value of a token.
I have RTK Query set up and functioning, the only issue is that when using the fetchBaseQuery with prepareHeaders and attempting to get the token from the tokenManager, it returns null when the query is initiate and the query fails.
However, if I make a change in the app (e.g: change a console log) and then it hot reloads, the app is able to fetch the token from the token manager successfully and the api call is able to successfully be made.

The strange part is that if i use the customBaseQuery with axios with an interceptor that uses the same token manager - it works.

token manager

class TokenManager {
  private token?: string;

  setToken(token?: string) {
    this.token = token;
  }

  getToken(): string | undefined {
    return this.token;
  }
}

export default new TokenManager();

axios base query

const axiosBaseQuery =
  (
    { baseUrl }: { baseUrl: string } = { baseUrl: '' },
  ): BaseQueryFn<
    {
      url: string;
      method?: AxiosRequestConfig['method'];
      data?: AxiosRequestConfig['data'];
      params?: AxiosRequestConfig['params'];
      headers?: AxiosRequestConfig['headers'];
    },
    unknown,
    unknown
  > =>
  async ({ url, method, data, params, headers }) => {
    try {
      axios.interceptors.request.use(req => {
        const request = req;
        const token = tokenManager.getToken();
        if (token) {
          request.headers.Authorization = `Bearer ${token}`;
        }
        return request;
      });

      const result = await axios({
        url: baseUrl + url,
        method,
        data,
        params,
        headers,
      });
      return { data: result.data };
    } catch (axiosError) {
      const err = axiosError as AxiosError;
      return {
        error: {
          status: err.response?.status,
          data: err.response?.data || err.message,
        },
      };
    }
  };

(Note: I inject the actual queries from different files. I toggle between this fetchBaseQuery and the axiosBaseQuery above)
base api slice

export const baseApi = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({
    baseUrl: `${config.API_BASE_URL}/api`,
    timeout: config.API_TIMEOUT,
    headers: DEFAULT_HEADERS,
    prepareHeaders: async (headers: Headers) => {
      const token = tokenManager.getToken();
      if (token) {
        headers.set('Authorization', `Bearer ${token}`);
      }
      return headers;
    },
  }),
  endpoints: () => ({}),
});

Does anyone have any idea why these 2 would behave differently? Given that I swap out the default fetchBaseQuery with the custom axiosBaseQuery.
I'm trying to get the fetchBaseQuery to work as it is the default.
Thanks in advance.

I think you need to be careful with hot reloading and singletons; you can end up with multiple instances. I've seen similar issues while debugging my own authentication implementation due to hot loading. I use a BroadcastChannel to keep my authentication synced across tabs, and I could see those messages getting duplicated thanks to the hot-reloading, indicating multiple instances of my singleton in play. I haven't solved this yet - but the solution undoubtedly lies in properly integrating with the hot reloading module.

Thanks for the info @rwilliams3088 ๐Ÿ‘

I should've also specified that this is with react native.
It's weird because with axios it works, but not with fetch (from fetchBaseQuery).
Side note: I seem to have been able to get it working by modifying someone else's code which sets the token in the token manager instance.
I figured it was a timing issue of sorts, and when i removed the useEffect from their code, and directly set it where the functions were being called - it worked. ๐Ÿ‘

Looks like with React Native Debugger stopped, it actually recauses the issue. Very strange, lol.