arthurfiorette / axios-cache-interceptor

📬 Small and efficient cache interceptor for axios. Etag, Cache-Control, TTL, HTTP headers and more!

Home Page:https://axios-cache-interceptor.js.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Caching is seemingly broken by React v18's Strict Mode... or is it cancellation?

arthurfiorette opened this issue · comments

Discussed in #664

Originally posted by xml September 1, 2023
React 18 is a significant overhaul, and introduces new behavior in its during development, causing many, many consequences such as this. It double-renders components, in an attempt to show you whether your renderers are idempotent and free of side-effects. Nice idea, although it's breaking lots of brains. I don't in any way believe a-c-i needs to be responsible for React's idiosyncracies, although at a minimum, it would be nice for people to be aware of the risks. That said...

Like many folks, I made all my heaviest/slowest axios requests cancellable to deal with StrictMode, so that when the component is quickly unmounted and remounted, any initial data calls are aborted. I use the ES spec abortController, as described in Axios docs. So far, so good. It cut down dramatically on duplicate API requests during dev.

Until... I added axios-cache-interceptor, and discovered that it refused to cache any of my most important data calls, no matter what I did. This was a brain-bender, until I randomly tried disabling StrictMode. Boom: caching suddenly worked. Here's an example of the debug messages from a-c-i, before and after. You can see that the request ID is the same, but the outcome is very different... AFTER (working normally, for comparison):

{id: '578315523', msg: 'Sending request, waiting for response', data: {…}}
{id: '578315523', msg: 'Useful response configuration found', data: {…}}
{id: '578315523', msg: 'Found waiting deferred(s) and resolved them'}
{id: '578315523', msg: 'Response cached', data: {…}}

BEFORE (failing, in StrictMode)

{id: '578315523', msg: 'Sending request, waiting for response', data: {…}}
{id: '578315523', msg: 'Waiting list had an deferred for this key, waiting for it to finish'}
{id: '578315523', msg: 'Detected concurrent request, waiting for it to finish'}
{id: '578315523', msg: 'Deferred rejected, requesting again', data: undefined}
{id: '578315523', msg: 'Caught an error in the request interceptor', data: {…}}
{id: '578315523', msg: "Response not cached and storage isn't loading", data: {…}}

So: conclusion thus far: a-c-i is perhaps not necessarily optimized for the unique circumstances of StrictMode during DEV, and it'll work fine in PROD, which is what I'm most worried about. So, OK.

But... what if it's not actually StrictMode causing the problem, but something else related to it? My other calls are caching just fine, even in StrictMode. So, what if the problem is cancellable requests?

To test that, I re-enabled StrictMode, went to my most important data calls, and stripped the abortController. Huh. Whaddya' know?! Suddenly, caching works the way I'd expect: with the first request fetching new data, and the second just reading cache! Results:

{id: '578315523', msg: 'Sending request, waiting for response', data: {…}}
{id: '578315523', msg: 'Waiting list had an deferred for this key, waiting for it to finish'}
{id: '578315523', msg: 'Detected concurrent request, waiting for it to finish'}
{id: '578315523', msg: 'Useful response configuration found', data: {…}}
{id: '578315523', msg: 'Found waiting deferred(s) and resolved them'}
{id: '578315523', msg: 'Returning cached response'}
{id: '578315523', msg: 'Response cached', data: {…}}
{id: '578315523', msg: 'Returned cached response'}

Implication: adding an abortController to the requests is causing the issue. But... what if I leave the abortController there... but simply don't invoke it in useEffect? Once again: works fine, with an identical stream of debug messages as above. So, the problem isn't the abortController being present, but invocation of it.

(To be exhaustive: I happen to have an axios interceptor which does some routine error-handling for me. That interceptor returns errors as rejected promises. Could that be the issue? I disabled it. After that, caching still fails, and with the same debug signature. There's nothing else in the interceptor pipeline between axios and a-c-i.)

My hypothesis:

  • we know that when axios cancels a request, it returns an error to the response interceptor pipeline, with a message of 'cancelled'.
  • I haven't read the a-c-i source, but I think what's maybe happening is that: if a-c-i receives duplicate requests and one of them fails with an error, a-c-i refuses to cache the response to the other, successful request. This makes sense.
  • However, does this make sense: a-c-i should filter out any errors related to a legitimate cancellation (those with a message of 'cancelled'), which would not only be more compatible with axios' expected behavior, but also have the side-effect of fixing the problem caused by StrictMode. ??
  • Or, at a minimum: use the docs to advise people not to combine a-c-i with cancellable requests, as that will break things. a-c-i by itself will do a great job of catching and suppressing the duplicates, so let a-c-i take care of all that for you. Downside to this: getting duplicate responses is still generally a Bad Thing, even if you avoid burdening your API server with the duplicate requests. It often results in writing duplicate data to state managers, and is pretty hard to avoid. So... I'd suggest it's actually important to support both cancellation and caching, together, as "defense in depth."

Thank you for a great library. This is an edge-case here, and hard to predict. I hope this documentation of the situation is helpful to... someone.

Hey @xml super thanks for this detailed bug report. Would you mind creating a reproducible example so that I can test it out?

If you are using cancellation to prevent doubling requests upon renders, axios cache Interceptor does it by default and no need to manually implement cancelation.

With this Interceptor, you may only want to use cancellation if you do not need that request anymore and will not request it again.

However, it should not behave like this. Happy to debug a reproducible example...

What's the error shown here?

// <before>
{id: '578315523', msg: 'Caught an error in the request interceptor', data: {}}

The error shown is the expected Axios canceled error:

data: {
  error: {
    code: "ERR_CANCELED"
    config: {transitional: {…}, adapter: 'xhr', transformRequest: Array(1), transformResponse: Array(1), timeout: 0, …}
    message: "canceled"
    name: "CanceledError"
    stack: "CanceledError: canceled\n    at throwIfCancellationRequested (http://localhost:3001/static/js/bundle.js:43196:11)\n    at Axios.dispatchRequest (http://localhost:3001/static/js/bundle.js:43208:3)"
  }
}

And yes, I fully understand that a-c-i can prevent duplicate requests from going out, thus saving latency and server load. What it cannot prevent (without aborting the second request) is the state manager receiving two copies of the same data. The fact that one copy came from cache doesn't help any...

Here's the link to a repo which shows the behavior: https://github.com/xml/a-c-i-issues

As it's C-R-A, all you need to do to run is npm install and then npm start. (Plus, I'm in the habit of using console.debug for all non-temporary console statements, so I apologize that requires setting the Log Level to "All Levels" in Chrome to see the debug messages from a-c-i.)

NOTE: I've noticed something super-weird which may be another issue? The second I augment this axios instance with a-c-i, it begins throwing CORS errors, but only in Chrome (Mac v116). Safari is fine...

So, to see the CORS error, open this repo in Chrome. Note that I tried adding axios CORS-related overrides, but that didn't help at all.

But, to just go straight to the error with cancelled requests preventing caching, use some other browser like Safari. If you then want to see caching behaving normally, simply comment out App.js line 18: return () => controller.abort();

NOTE: I've noticed something super-weird which may be another issue? The second I augment this axios instance with a-c-i, it begins throwing CORS errors, but only in Chrome (Mac v116). Safari is fine...
So, to see the CORS error, open this repo in Chrome. Note that I tried adding axios CORS-related overrides, but that didn't help at all.

Regarding the CORS error, this is a well known problem users may have when using a-c-i since #437. Searching CORS on this repository issues would've shown some examples of it. However I did not know that it wasn't showing anything on the documentation search. I just updated it, so now users may solve this easier.

image.

Hey, after debugging, this problem is the exact one I described at #612. I'll try to resolve it asap. Thanks for the detailed debugging information and reproducible example.

Released at v1.3.0