lifeomic / axios-fetch

A WebAPI Fetch implementation backed by an Axios client
MIT License
172 stars 30 forks source link

buildAxiosFetch not adding X-XSRF-TOKEN header on requests #113

Closed lukadriel7 closed 2 years ago

lukadriel7 commented 2 years ago

Hello, Thank you for the library, it is very useful. I encountered a problem I didn't have before, I unfortunately do not remember the last version of axios it worked with. basically, I am using this library to with my own axios instance and urql to fetch datafrom a graphql server without having to configure urql for csrf. I updated my dependencies to the latest versions today and tried to make some request but they all fail with an incorrect csrf token error. Checking the request headers showed that the X-XSRF-TOKEN header is not set for graphql post request, but normal axios request automatically set the header. I tried setting up an interceptor on the axios instance to log the config, but it also only works when making regular axios request.

Axios

import { boot } from 'quasar/wrappers';
import Axios, { AxiosInstance } from 'axios';

declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $axios: AxiosInstance;
  }
}

const axios = Axios.create();
axios.defaults.withCredentials = true;
axios.interceptors.request.use(
  (config) => {
    console.log('config: ', config);
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

export default boot(async ({ app }) => {
  await axios.get(`${process.env.API_URL}/auth/csrf-token`);
  app.config.globalProperties.$axios = axios;
});

export { axios };

urql

import { createClient as createWSClient } from 'graphql-ws';
import { buildAxiosFetch } from '@lifeomic/axios-fetch';
import axios from 'axios';
import { authExchange } from '@urql/exchange-auth';
import { AuthState } from 'src/interfaces';
import { useJwt } from '@vueuse/integrations/useJwt';

const getAuth = async () => {
  const token = localStorage.getItem('token');
  if (!token) {
    return token ? { token } : null;
  }
  const result = await axios.get<string>(
    `${process.env.API_URL}/auth/refresh-token`
  );
  if (result.data) {
    localStorage.setItem('token', result.data);
    return {
      token: result.data,
    };
  }
  localStorage.clear();
  //logout logic here
  return null;
};

const wsClient = createWSClient({
  url: `${process.env.API_WS_URL}/graphql`,
  connectionParams: async () => ({
    authToken: (await getAuth())?.token,
  }),
});

const client = createClient({
  fetch: buildAxiosFetch(axios),
  url: `${process.env.API_URL}/graphql`,
  requestPolicy: 'cache-and-network',
  exchanges: [
    dedupExchange,
    cacheExchange,
    authExchange({
      addAuthToOperation: ({ authState, operation }) => {
        const state = authState as AuthState;
        if (!state || !state.token) {
          return operation;
        }
        const fetchOptions =
          typeof operation.context.fetchOptions === 'function'
            ? operation.context.fetchOptions()
            : operation.context.fetchOptions || {};
        return makeOperation(operation.kind, operation, {
          ...operation.context,
          fetchOptions: {
            ...fetchOptions,
            headers: {
              ...fetchOptions.headers,
              Authorization: `Bearer ${state.token}`,
            },
          },
        });
      },
      willAuthError: ({ authState }) => {
        const state = authState as AuthState;
        if (!state || !state.token) return true;
        const { payload } = useJwt(state.token);
        if (payload.value && payload.value.exp) {
          const isTokenExpired = payload.value.exp * 1000 < Date.now();
          return isTokenExpired;
        }
        return !!payload;
      },
      didAuthError: ({ error }) => {
        return error.graphQLErrors.some(
          (err) => err.extensions?.code === 'FORBIDDEN'
        );
      },
      getAuth,
    }),
    multipartFetchExchange,
    subscriptionExchange({
      forwardSubscription(operation) {
        return {
          subscribe: (sink) => {
            const dispose = wsClient.subscribe(operation, sink);
            return {
              unsubscribe: dispose,
            };
          },
        };
      },
    }),
  ],
});

export default boot(({ app }) => {
  app.use(UrqlPlugin, client);
});

axios: v0.26 @lifeomic/axios-fetch: v3.0.0 @urql/vue: v0.6.1

lukadriel7 commented 2 years ago

As a temporary fix, I currently have to set the configuration in the buildAxiosFetch method as

fetch: buildAxiosFetch(axios, (config) => {
    config.withCredentials = true;
    return config;
  }),
mdlavin commented 2 years ago

I spent some time reading your report, and I'm not completely sure what your primary problem is. I have a couple questions:

  1. Are you reporting a bug about the lack of support for axios.defaults.withCredentials ? It wouldn't surprise me if that was broken, it's not a pattern we use much.
  2. Are you reporting a regression in Axios itself? Does the missing header problem happen with just Axios, or is it specific to axios-fetch?
  3. Is there a smaller testcase that demonstrates the problem, maybe that does not depend on GraphQL? I'd love to update our testsuite to catch a problem and avoid future regressions
lukadriel7 commented 2 years ago

Hello, I think it's the 1, lack of support for axios.default.withCredentials. It works while using axios only but not with axios-fetch. I unfortunately do not have a test case without graphql. I currently do not have access to my computer unfortunately.