diff --git a/src/interceptors/response.ts b/src/interceptors/response.ts index 8361f84b..171ce956 100644 --- a/src/interceptors/response.ts +++ b/src/interceptors/response.ts @@ -15,9 +15,15 @@ export function defaultResponseInterceptor(axios: AxiosCacheInstance): ResponseI * * Also update the waiting list for this key by rejecting it. */ - const rejectResponse = async (responseId: string, config: CacheRequestConfig) => { + const rejectResponse = async ( + responseId: string, + config: CacheRequestConfig, + clearCache: boolean + ) => { // Updates the cache to empty to prevent infinite loading state - await axios.storage.remove(responseId, config); + if (clearCache) { + await axios.storage.remove(responseId, config); + } // Rejects the deferred, if present const deferred = axios.waiting.get(responseId); @@ -116,7 +122,7 @@ export function defaultResponseInterceptor(axios: AxiosCacheInstance): ResponseI !cache.data && !(await testCachePredicate(response, cacheConfig.cachePredicate)) ) { - await rejectResponse(response.id, config); + await rejectResponse(response.id, config, true); if (__ACI_DEV__) { axios.debug({ @@ -154,7 +160,7 @@ export function defaultResponseInterceptor(axios: AxiosCacheInstance): ResponseI // Cache should not be used if (expirationTime === 'dont cache') { - await rejectResponse(response.id, config); + await rejectResponse(response.id, config, true); if (__ACI_DEV__) { axios.debug({ @@ -276,7 +282,7 @@ export function defaultResponseInterceptor(axios: AxiosCacheInstance): ResponseI } // Rejects all other requests waiting for this response - await rejectResponse(id, config); + await rejectResponse(id, config, true); throw error; } @@ -297,7 +303,12 @@ export function defaultResponseInterceptor(axios: AxiosCacheInstance): ResponseI } // Rejects all other requests waiting for this response - await rejectResponse(id, config); + await rejectResponse( + id, + config, + // Do not clear cache if this request is cached, but the request was cancelled before returning the cached response + error.code !== 'ERR_CANCELED' || (error.code === 'ERR_CANCELED' && cache.state !== 'cached') + ); throw error; } @@ -381,7 +392,7 @@ export function defaultResponseInterceptor(axios: AxiosCacheInstance): ResponseI } // Rejects all other requests waiting for this response - await rejectResponse(id, config); + await rejectResponse(id, config, true); throw error; }; diff --git a/test/interceptors/response.test.ts b/test/interceptors/response.test.ts index cd5bd15c..40841cda 100644 --- a/test/interceptors/response.test.ts +++ b/test/interceptors/response.test.ts @@ -361,4 +361,47 @@ describe('Response Interceptor', () => { assert.equal(storage.state, 'cached'); assert.equal(storage.data?.data, true); }); + + // https://github.com/arthurfiorette/axios-cache-interceptor/issues/922 + it('Aborted requests should preserve non-stale valid cache entries', async () => { + const instance = Axios.create({}); + const axios = setupCache(instance, {}); + + const id = '1'; + + const cache = { + data: true, + headers: {}, + status: 200, + statusText: 'Ok' + }; + + // Cache request + axios.storage.set(id, { + state: 'cached', + ttl: 5000, + createdAt: Date.now(), + data: cache + }); + + // First request cancelled immediately + const controller = new AbortController(); + const cancelled = axios.get('http://unknown.url.lan:1234', { id, signal: controller.signal }); + controller.abort(); + try { + await cancelled; + assert.fail('should have thrown an error'); + } catch (error: any) { + assert.equal(error.code, 'ERR_CANCELED'); + } + + // Second request cancelled after a macrotask + const controller2 = new AbortController(); + const promise = axios.get('http://unknown.url.lan:1234', { id, signal: controller2.signal }); + // Wait for eventual request to be sent + await new Promise((res) => setTimeout(res)); + controller2.abort(); + const response = await promise; + assert.ok(response.cached); + }); });