Axios throwing CanceledError with Abort controller in react
Asked Answered
E

4

16

I have built an axios private instance with interceptors to manage auth request.

The system has a custom axios instance:

const BASE_URL = 'http://localhost:8000';
export const axiosPrivate = axios.create({
  baseURL: BASE_URL,
  headers: {
    'Content-Type': 'application/json',
  },
  withCredentials: true,
});

A custom useRefreshToken hook returns accessToken using the refresh token:

const useRefreshToken = () => {
  const { setAuth } = useAuth();

  const refresh = async () => {
    const response = await refreshTokens();
    // console.log('response', response);
    const { user, roles, accessToken } = response.data;
    setAuth({ user, roles, accessToken });
    // return accessToken for use in axiosClient
    return accessToken;
  };

  return refresh;
};

export default useRefreshToken;

Axios interceptors are attached to this axios instance in useAxiosPrivate.js file to attached accessToken to request and refresh the accessToken using a refresh token if expired.

const useAxiosPrivate = () => {
  const { auth } = useAuth();
  const refresh = useRefreshToken();

  useEffect(() => {
    const requestIntercept = axiosPrivate.interceptors.request.use(
      (config) => {
        // attach the access token to the request if missing
        if (!config.headers['Authorization']) {
          config.headers['Authorization'] = `Bearer ${auth?.accessToken}`;
        }
        return config;
      },
      (error) => Promise.reject(error)
    );

    const responseIntercept = axiosPrivate.interceptors.response.use(
      (response) => response,
      async (error) => {
        const prevRequest = error?.config;
        // sent = custom property, after 1st request - sent = true, so no looping requests
        if (error?.response?.status === 403 && !prevRequest?.sent) {
          prevRequest.sent = true;
          const newAccessToken = await refresh();
          prevRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
          return axiosPrivate(prevRequest);
        }
        return Promise.reject(error);
      }
    );

    // remove the interceptor when the component unmounts
    return () => {
      axiosPrivate.interceptors.response.eject(responseIntercept);
      axiosPrivate.interceptors.request.eject(requestIntercept);
    };
  }, [auth, refresh]);

  return axiosPrivate;
};

export default useAxiosPrivate;

Now, this private axios instance is called in functional component - PanelLayout which is used to wrap around the pages and provide layout.

Here, I've tried to use AbortControllers in axios to terminate the request after the component is mounted.

function PanelLayout({ children, title }) {
  const [user, setUser] = useState(null);
  const axiosPrivate = useAxiosPrivate();
  const router = useRouter();

  useEffect(() => {
    let isMounted = true;
    const controller = new AbortController();
    const signal = controller.signal;

    const getUserProfile = async () => {
      try {
        const response = await axiosPrivate.get('/api/identity/profile', {
          signal,
        });
        console.log(response.data);
        isMounted && setUser(response.data.user);
      } catch (error) {
        console.log(error);
        router.push({
          pathname: '/seller/auth/login',
          query: { from: router.pathname },
        });
      }
    };
    getUserProfile();

    return () => {
      isMounted = false;
      controller.abort();
    };
  }, []);

  console.log('page rendered');

  return (
    <div className='flex items-start'>
      <Sidebar className='h-screen w-[10rem]' />
      <section className='min-h-screen flex flex-col'>
        <PanelHeader title={title} classname='left-[10rem] h-[3.5rem]' />
        <main className='mt-[3.5rem] flex-1'>{children}</main>
      </section>
    </div>
  );
}

export default PanelLayout;

However, the above code is throwing the following error:

CanceledError {message: 'canceled', name: 'CanceledError', code: 'ERR_CANCELED'}
code: "ERR_CANCELED"
message: "canceled"
name: "CanceledError"
[[Prototype]]: AxiosError
constructor: ƒ CanceledError(message)
__CANCEL__: true
[[Prototype]]: Error

Please suggest how to avoid the above error and get axios to work properly.

Enviable answered 27/7, 2022 at 15:17 Comment(2)
it got canceled because PanelLayout is being unmounted, so you need to find it why its happening. Also you must handle cancle error. something like this if (error.name === 'CanceledError' ) return in catch blockSmyrna
Thanks. I'm using the if (error.name === 'CanceledError' ) return in catch block as a hack until i figure out the reason for my error.Enviable
C
16

I also encountered the same issue and I thought that there was some flaw in my logic which caused the component to be mounted twice. After doing some digging I found that react apparently added this feature with with the new version 18 in StrictMode where useEffect was being run twice. Here's a link to the article clearly explaining this new behaviour.

One way you could solve this problem is by removing StrictMode from your application (Temporary Solution)

Another way is by using useRef hook to store some piece of state which is updated when your application is mounted the second time.

// CODE BEFORE USE EFFECT

const effectRun = useRef(false);

useEffect(() => {
    let isMounted = true;
    const controller = new AbortController();
    const signal = controller.signal;

    const getUserProfile = async () => {
      try {
        const response = await axiosPrivate.get('/api/identity/profile', {
          signal,
        });
        console.log(response.data);
        isMounted && setUser(response.data.user);
      } catch (error) {
        console.log(error);
        router.push({
          pathname: '/seller/auth/login',
          query: { from: router.pathname },
        });
      }
    };

    // Check if useEffect has run the first time
    if (effectRun.current) {
      getUserProfile();
    }

    return () => {
      isMounted = false;
      controller.abort();
      effectRun.current = true; // update the value of effectRun to true
    };
  }, []);

 // CODE AFTER USE EFFECT

Found the solution from this YouTube video.

Corrales answered 26/8, 2022 at 7:44 Comment(0)
D
3

I faced the same problem in similar project, lets start by understanding first the root cause of that problem. In react 18 they try to make us convenient to the idea of mounting and unmounting components twice for future features that they are preparing, the useEffect hook now is mounted first time then unmounted the mounted finally. So they need from us adapt our projects to the idea of mount and unmount of components twice So you have two ways, adapting these changes and try to adapt your code to accept mounting twice, or making some turn around code to overcome mounting twice, and I would prefer the first one.

Here in your code after first mount you aborted your API request in clean up function, so when the component dismount and remount again it face an error when try to run previously aborted request, so it throw exception, that's what happens

1st solution (adapting to react changing):

return () => {
      isMounted = false
      isMounted && controller.abort()
    }

so in above code we will abort controller once only when isMounted is true, and that will solve your problem

2nd solution (turn around to react changing):

by using useRef hook and assign it to a variable and update its Boolean value after executing the whole code only one time.

const runOnce = useRef(true) 

useEffect(()=>{
 if(runOnce.current){
   //requesting from API
  return()=>{
   runOnce.current = false
  }
 }
},[]) 

3rd solution (turn around to react changing):

remove React.StrictMode from index.js file

Deucalion answered 15/1, 2023 at 21:28 Comment(1)
this should be runOnce.current = trueUlcerous
C
2

I, too, encountered this issue. What made it worse is that axios doesn't provide an HTTP status code when the request has been canceled, although you do get error.code === "ERR_CANCELED". I solved it by handling the abort within the axios interceptor:

axiosInstance.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.code === "ERR_CANCELED") {
      // aborted in useEffect cleanup
      return Promise.resolve({status: 499})
    }
    return Promise.reject((error.response && error.response.data) || 'Error')    
  }
);

As you can see, I ensure that the error response in the case of an abort supplies a status code of 499.

Cassiodorus answered 7/9, 2022 at 12:8 Comment(0)
A
0

Implementing useRef fixes axios CanceledError.

Annulate answered 3/8, 2024 at 4:34 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Ferroconcrete

© 2022 - 2025 — McMap. All rights reserved.