3

So here is my issue. I am using JWT authentication in my project and i have an axiosInstance setup in my react project . I also have an interceptor for axiosInstance which takes care of intercepting and refreshing tokens when required.

const axiosInstance = axios.create({
 ​baseURL: baseURL,
 ​timeout: 360000,
 ​transformRequest: [
   ​function (data, headers) {
     ​const accessToken = window.localStorage.getItem('access_token');
     ​if (accessToken) {
       ​headers['Authorization'] = `Bearer ${accessToken}`;
     ​} else {
       ​delete headers.Authorization;
     ​}

     ​return JSON.stringify(data);
   ​},
 ​],
 ​headers: {
   ​'Content-Type': 'application/json',
   ​accept: 'application/json',
 ​},
});


axiosInstance.interceptors.response.use(
  (response) => {
    return response;
  },
  async function (error) {
    const originalRequest = error.config;

    console.log(
      'Caught the error response. Here is your request  ',
      originalRequest,
    );

    // case 1: No error specified Most likely to be server error

    if (typeof error.response === 'undefined') {
      //  Uncomment this later
      alert('Server error occured');

      return Promise.reject(error);
    }

    //  case 2: Tried to refresh the token but it is expired. So ask user to login again

    if (
      error.response.status === 401 &&
      originalRequest.url === baseURL + 'auth/api/token/refresh/'
    ) {
      store.dispatch(setLoginFalse());
      return Promise.reject(error);
    }

    // Case 3: Got 401 Unauthorized error. There are different possiblities
    console.log('Error message in axios = ', error.response.data);
    if (
      error.response.status === 401 &&
      error.response.statusText === 'Unauthorized'
    ) {
      const refreshToken = localStorage.getItem('refresh_token');
      console.log('Refresh token = ', refreshToken);

      // See if refresh token exists
      // Some times undefined gets written in place of refresh token.
      // To avoid that we check if refreshToken !== "undefined". This bug is still unknown need to do more research on this

      if (refreshToken !== undefined && refreshToken !== 'undefined') {
        console.log(typeof refreshToken == 'undefined');
        console.log('Refresh token is present = ', refreshToken);
        const tokenParts = JSON.parse(atob(refreshToken.split('.')[1]));

        // exp date in token is expressed in seconds, while now() returns milliseconds:
        const now = Math.ceil(Date.now() / 1000);
        console.log(tokenParts.exp);

        // Case 3.a Refresh token is present and it is not expired - use it to get new access token

        if (tokenParts.exp > now) {
          return axiosInstance
            .post('auth/api/token/refresh/', { refresh: refreshToken })
            .then((response) => {
              localStorage.setItem('access_token', response.data.access);

              axiosInstance.defaults.headers['Authorization'] =
                'Bearer ' + response.data.access;
              originalRequest.headers['Authorization'] =
                'Bearer ' + response.data.access;

              console.log('access token updated');

              // After refreshing the token request again user's previous url
              // which was blocked due to unauthorized error

              // I am not sure by default axios performs get request
              // But since we are passing the entire config of previous request
              // It seems to perform same request method as previous

              return axiosInstance(originalRequest);
            })

            .catch((err) => {
              // If any error occurs at this point we cannot guess what it is
              // So just console log it

              console.log(err);
            });
        } else {
          // Refresh token is expired ask user to login again.

          console.log('Refresh token is expired', tokenParts.exp, now);
          store.dispatch(setLoginFalse());
        }
      } else {
        // refresh token is not present in local storage so ask user to login again

        console.log('Refresh token not available.');
        store.dispatch(setLoginFalse());
      }
    }

    // specific error handling done elsewhere
    return Promise.reject(error);
  },
);
export default axiosInstance;

Note that i have Content-Type set as 'application/json' in axiosIntance.

But my problem is inorder to upload images the content type should be 'multipart/form-data --boundary: set-automatically'.

(NOTE: Manually setting boundary for multipart data doesn't seem to work)

The boundary for multipart data is set automatically by axios if we don't put the content-type in header. But for that i have to somehow delete the content-type from axiosInstance at one place (from where i am uploading the image) without disturbing axiosInstance used at other parts of the project.

I tested it with fetch and by setting up new axios instance it works as expected. But the problem is these requests won't be intercepted by axios for refreshing JWT tokens if needed to.

I read various posts on this, but i still don't see a way to solve this.

I cann provide any more details if required. Please help me, i already spent 8+ hours debugging this.

Thank you.

Edit 1

I changed the handleSubmit function to this


  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(file);

    let formData = new FormData();
    formData.append('profile_pic', file);
    formData.append('name', 'root');

    axiosInstance.defaults.headers.common['Content-Type'] =
      'multipart/form-data';

    axiosInstance
      .put('/users/profile-pic-upload/', formData)
      .then((res) => console.log(res))
      .catch((err) => console.log(err));
  };

But the content type is still application/json content type is still same

But Let's say i changed the content-type in core axios.js to 'multipart/form-data' it changes the content type of all requests. It will break other things but as expected it won't fix this issue. Because setting manual boundary doesn't seems to work. Even this post says to remove the content type during multipart data so that it is handled automatically by library (axios in this case)

Sanketh B. K
  • 759
  • 8
  • 22

4 Answers4

2
axiosInstance.defaults.headers.put['Content-Type'] = "multipart/form-data";

Or

axiosInstance.interceptors.request.use(config => {
  config.headers.put['Content-Type'] = 'multipart/form-data';
  return config;
});

Try this for your specific instance.

Lovlesh Pokra
  • 722
  • 4
  • 14
  • Hi, thanks for the reply. I edited the question. Please see the edit. – Sanketh B. K Jun 25 '21 at 09:46
  • 1
    @SankethB.K please check the latest update. – Lovlesh Pokra Jun 25 '21 at 10:37
  • This helped me. In my case of checking the access token when downloading a file - response needs to be parsed for the new access token. However since this interceptor is within the class that is used to download the file with the responseType set to arrayBuffer while creation responseType: 'arraybuffer' I had to change the responseType to json like below youraxiosinstance.defaults.responseType = "json"; and then setting it back to arraybuffer - so file download can continue youraxiosinstance.defaults.responseType = "arraybuffer"; – KManish May 23 '22 at 08:47
2

For passing anything dynamic to your axios instance, use a function that returns the axios instance like this:

import axios from 'axios';

const customAxios = (contentType) => {
  // axios instance for making requests
  const axiosInstance = axios.create({
    // your other properties for axios instance
    headers: {
      'Content-Type': contentType,
    },
  });

  // your response interceptor
  axiosInstance.interceptors.response.use(// handle response);

  return axiosInstance;
};

export default customAxios;

And now, you can use axios like:

import customAxios from './customAxios';

const axiosForJSON = customAxios('application/json');
const axiosForMultipart = customAxios('multipart/form-data');

axiosForJSON.get('/hello');
axiosForMultipart.post('/hello', {});

// OR
cusomAxios('application/json').get('/hello');
Haseeb Anwar
  • 2,438
  • 19
  • 22
  • Thanks a lot. This is exactly something i was looking for. I have a question, earlier i was using a single axiosInstance in entire project. but now everytime i import axiosinstance a new instance will be created and returned will that cause any performance issue? , as every instance comes with an interceptor attached. – Sanketh B. K Jun 25 '21 at 13:24
  • You can [memoize](https://en.wikipedia.org/wiki/Memoization) the call by `contentType`. You don't have to, but let's say you have [lodash's `memoize`](https://lodash.com/docs/4.17.15#memoize) available: `const customAxios = _.memoize((contentType) => { ... });` – romellem Jun 25 '21 at 14:11
0

Answer above from Lovlesh Pokra helped me.

In my case of checking the access token when downloading a file - response needs to be parsed for the new access token. However since this interceptor is within the class that is used to download the file with the responseType set to arrayBuffer while creation

responseType: 'arraybuffer'

I had to change the responseType to json like below

youraxiosinstance.defaults.responseType = "json"; 

and then setting it back to arraybuffer - so file download can continue

youraxiosinstance.defaults.responseType = "arraybuffer";

based upon your need - just before the call - the change can be done as required by you.

KManish
  • 1,551
  • 2
  • 11
  • 7
0

I was also struggling same as you for uploading media to the server. As my global Axios instance has Content-Type 'application/json' and then when the request was made I was updating the Content-Type to 'multipart/form-data' using below script.

// Not Working
this.axiosInstance.defaults.headers.common['Content-Type'] = 'multipart/form-data';

which was still not updated as the request header in the network tab still contains 'application/json' from global configurations (This could be the possible reason - the global header is saved in some other reference and we are updating it in some other reference)

So the fix is to intercept the request just before it flies and then modify the header as shown below

// Working
this.axiosInstance.interceptors.request.use(config => {
    config.headers['Content-Type'] = 'multipart/form-data';
    return config;
});

Once Content-Type 'multipart/form-data' has been set Boundary will be automatically handled by Axios

Hope this will help you or somebody else. Thanks!

Happy Coding :-)

Aman Kumar Gupta
  • 2,640
  • 20
  • 18