Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Axios race condition with cancel token? #4043

Open
9mm opened this issue Sep 10, 2021 · 4 comments
Open

Axios race condition with cancel token? #4043

9mm opened this issue Sep 10, 2021 · 4 comments

Comments

@9mm
Copy link

9mm commented Sep 10, 2021

Describe the issue

I'm using axios in the browser.

Cancel token exceptions do not appear to correspond 100% with what Chrome developer tools considers a response failure.

Essentially I want to retry certain requests (even POST requests) if the server doesn't respond within 5000ms. Obviously with non-idempotent requests it's important that success/failure of response matches the server.

So I tried to implement a connection timeout using the following code. A more simple version of this code is here, and it has the same problem.

I noticed that if I set the connection timeout to be as close as possible to the time it takes for server to respond (to emulate a request finishing right at the time out), there appears to be a race condition where cancel is retried even though it was successful

I have tried axios-retry and it has the same issue.

Example Code

Code snippet to illustrate your question

const makeRequest = async (args, connTimeout, responseTimeout) => {
  const source = axios.CancelToken.source();

  const argsWithToken = {
    ...args,
    cancelToken: source.token,
  };

  const api = buildAxios(responseTimeout);

  const timeout = setTimeout(source.cancel, connTimeout);

  return api(argsWithToken).then(result => {
    clearTimeout(timeout);
    return result;
  });
};

const handleRetries = (args, maxRetries) => (
  new Promise(async (resolve, reject) => { /* eslint-disable-line */
    let retries = 0;
    let success = false;

    while (!success && retries < maxRetries) {
      try {
        const result = await makeRequest(args, 300, 30000); /* eslint-disable-line */
        success = true;
        resolve(result);
      } catch (err) {
        retries += 1;
        // console.log(`Error making ${args.method} request to ${args.url}, retrying... #${retries}`);
      }
    }
    // line below is included to prevent process leaks
    if (!success) reject(new Error(`Retried ${retries} times and still failed: ${args.url}`));
  })
);

handleRetries({url: '/settings', method: 'get'}, 3)

Expected behavior, if applicable

I don't know if it's my code, a limitation of JS, or axios, but I would expect that cancel would never get called, as the timeout is cleared in the success callback.

Environment

  • Axios Version [e.g. 0.18.0]: ^0.21.1
  • Adapter [e.g. XHR/HTTP]: XHR
  • Browser [e.g. Chrome, Safari]: Chrome
  • Browser Version [e.g. 22]: latest
  • Node.js Version [e.g. 13.0.1]: N/A
  • OS: [e.g. iOS 12.1.0, OSX 10.13.4]: OSX latest
  • Additional Library Versions [e.g. React 16.7, React Native 0.58.0]: vue 3

Additional context/Screenshots

Add any other context about the problem here. If applicable, add screenshots to help explain.

@9mm
Copy link
Author

9mm commented Sep 10, 2021

It seems @rohankulshreshtha touches on what I am noticing in this issue here: #2418 (comment)

If the request has already been sent to the server then the server will process the request even if we abort the request but the client will not wait for/handle the response.

I'm not quite sure if there is a way to resolve this though? Can I cancel a request without sending it to the server if it happens at the exact same time as it's cancelled?

@DigitalBrainJS
Copy link
Collaborator

Yes, the current Axios cancellation API has race conditions due to the asynchronous nature of CancelToken, since it uses Promises internally, so the internal abort method of the XMLHTTPRequest can be fired not within the same event tick. This is potentially fixed by #3305 which adds the AbortController synchronous interface to the lib as an alternative cancellation API.
In addition, receiving the response data takes some time, so your timer will only be cleared after a full response is received, not when the server starts responding (#1739)

@9mm
Copy link
Author

9mm commented Sep 10, 2021

@DigitalBrainJS wow awesome, thanks so much for that info. I felt like I was going crazy.

So would a workaround for now (until it's merged into axios), be to use cp-axios until then?

Thanks again!

@DigitalBrainJS
Copy link
Collaborator

@9mm Unfortunately no. cp-axios only fixes CancelToken memory leak by using alternative cancellation API and supports a custom promise API (CPromise) that provides cancellation/retries/progress capturing/etc. for use in complex async flows, including cancellable React effects.
You can try axios-lab fork that has AbortController support just for testing purposes, but it built on a bit outdated Axios version and is not intended for production. At the moment, there is no practical solution to this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants