diff --git a/spec/index.spec.ts b/spec/index.spec.ts index 9c61473..441aca8 100644 --- a/spec/index.spec.ts +++ b/spec/index.spec.ts @@ -569,6 +569,197 @@ describe('axiosRetry(axios, { retries, onRetry })', () => { }); }); +describe('axiosRetry(axios, { disableOtherResponseInterceptors })', () => { + afterEach(() => { + nock.cleanAll(); + nock.enableNetConnect(); + }); + + describe('successful after retry', () => { + it('should not multiple response interceptor', (done) => { + const client = axios.create(); + nock('http://example.com').get('/test').times(2).replyWithError(NETWORK_ERROR); + nock('http://example.com').get('/test').reply(200, 'It worked!'); + + let anotherInterceptorBeforeFulfilledCallCount = 0; + let anotherInterceptorBeforeRejectedCallCount = 0; + client.interceptors.response.use( + (result) => { + anotherInterceptorBeforeFulfilledCallCount += 1; + return result; + }, + (error) => { + anotherInterceptorBeforeRejectedCallCount += 1; + return Promise.reject(error); + } + ); + + axiosRetry(client, { retries: 3, disableOtherResponseInterceptors: true }); + + let anotherInterceptorAfterFulfilledCallCount = 0; + let anotherInterceptorAfterRejectedCallCount = 0; + client.interceptors.response.use( + (result) => { + anotherInterceptorAfterFulfilledCallCount += 1; + return result; + }, + (error) => { + anotherInterceptorAfterRejectedCallCount += 1; + return Promise.reject(error); + } + ); + + client.get('http://example.com/test').then((result) => { + expect(result.status).toBe(200); + expect(anotherInterceptorBeforeFulfilledCallCount).toBe(1); + expect(anotherInterceptorBeforeRejectedCallCount).toBe(1); + expect(anotherInterceptorAfterFulfilledCallCount).toBe(1); + expect(anotherInterceptorAfterRejectedCallCount).toBe(0); + done(); + }, done.fail); + }); + + it('should multiple response interceptor', (done) => { + const client = axios.create(); + nock('http://example.com').get('/test').times(2).replyWithError(NETWORK_ERROR); + nock('http://example.com').get('/test').reply(200, 'It worked!'); + + let anotherInterceptorBeforeFulfilledCallCount = 0; + let anotherInterceptorBeforeRejectedCallCount = 0; + client.interceptors.response.use( + (result) => { + anotherInterceptorBeforeFulfilledCallCount += 1; + return result; + }, + (error) => { + anotherInterceptorBeforeRejectedCallCount += 1; + return Promise.reject(error); + } + ); + + axiosRetry(client, { retries: 3, disableOtherResponseInterceptors: false }); + + let anotherInterceptorAfterFulfilledCallCount = 0; + let anotherInterceptorAfterRejectedCallCount = 0; + client.interceptors.response.use( + (result) => { + anotherInterceptorAfterFulfilledCallCount += 1; + return result; + }, + (error) => { + anotherInterceptorAfterRejectedCallCount += 1; + return Promise.reject(error); + } + ); + + client.get('http://example.com/test').then((result) => { + expect(result.status).toBe(200); + expect(anotherInterceptorBeforeFulfilledCallCount).toBe(1); + expect(anotherInterceptorBeforeRejectedCallCount).toBe(2); + expect(anotherInterceptorAfterFulfilledCallCount).toBe(3); + expect(anotherInterceptorAfterRejectedCallCount).toBe(0); + done(); + }, done.fail); + }); + }); + + describe('failure after retry', () => { + it('should not multiple response interceptor', (done) => { + const client = axios.create(); + nock('http://example.com').get('/test').times(3).replyWithError(NETWORK_ERROR); + + let anotherInterceptorBeforeFulfilledCallCount = 0; + let anotherInterceptorBeforeRejectedCallCount = 0; + client.interceptors.response.use( + (result) => { + anotherInterceptorBeforeFulfilledCallCount += 1; + return result; + }, + (error) => { + anotherInterceptorBeforeRejectedCallCount += 1; + return Promise.reject(error); + } + ); + + axiosRetry(client, { retries: 3, disableOtherResponseInterceptors: true }); + + let anotherInterceptorAfterFulfilledCallCount = 0; + let anotherInterceptorAfterRejectedCallCount = 0; + client.interceptors.response.use( + (result) => { + anotherInterceptorAfterFulfilledCallCount += 1; + return result; + }, + (error) => { + anotherInterceptorAfterRejectedCallCount += 1; + return Promise.reject(error); + } + ); + + client + .get('http://example.com/test') + .then( + () => done.fail(), + (error) => { + expect(anotherInterceptorBeforeFulfilledCallCount).toBe(0); + expect(anotherInterceptorBeforeRejectedCallCount).toBe(1); + expect(anotherInterceptorAfterFulfilledCallCount).toBe(0); + expect(anotherInterceptorAfterRejectedCallCount).toBe(1); + done(); + } + ) + .catch(done.fail); + }); + + it('should multiple response interceptor', (done) => { + const client = axios.create(); + nock('http://example.com').get('/test').times(3).replyWithError(NETWORK_ERROR); + + let anotherInterceptorBeforeFulfilledCallCount = 0; + let anotherInterceptorBeforeRejectedCallCount = 0; + client.interceptors.response.use( + (result) => { + anotherInterceptorBeforeFulfilledCallCount += 1; + return result; + }, + (error) => { + anotherInterceptorBeforeRejectedCallCount += 1; + return Promise.reject(error); + } + ); + + axiosRetry(client, { retries: 3, disableOtherResponseInterceptors: false }); + + let anotherInterceptorAfterFulfilledCallCount = 0; + let anotherInterceptorAfterRejectedCallCount = 0; + client.interceptors.response.use( + (result) => { + anotherInterceptorAfterFulfilledCallCount += 1; + return result; + }, + (error) => { + anotherInterceptorAfterRejectedCallCount += 1; + return Promise.reject(error); + } + ); + + client + .get('http://example.com/test') + .then( + () => done.fail(), + (error) => { + expect(anotherInterceptorBeforeFulfilledCallCount).toBe(0); + expect(anotherInterceptorBeforeRejectedCallCount).toBe(4); + expect(anotherInterceptorAfterFulfilledCallCount).toBe(0); + expect(anotherInterceptorAfterRejectedCallCount).toBe(4); + done(); + } + ) + .catch(done.fail); + }); + }); +}); + describe('isNetworkError(error)', () => { it('should be true for network errors like connection refused', () => { const connectionRefusedError = new AxiosError(); diff --git a/src/index.ts b/src/index.ts index 5e81f91..fba639e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,23 @@ -import type { AxiosError, AxiosRequestConfig, AxiosInstance, AxiosStatic } from 'axios'; +import type { + AxiosError, + AxiosRequestConfig, + AxiosInstance, + AxiosStatic, + AxiosInterceptorManager, + AxiosResponse, + InternalAxiosRequestConfig +} from 'axios'; import isRetryAllowed from 'is-retry-allowed'; +interface AxiosResponseInterceptorManagerExtended extends AxiosInterceptorManager { + handlers: Array<{ + fulfilled: ((value: AxiosResponse) => AxiosResponse | Promise) | null; + rejected: ((error: any) => any) | null; + synchronous: boolean; + runWhen: (config: InternalAxiosRequestConfig) => boolean | null; + }>; +} + export interface IAxiosRetryConfig { /** * The number of times to retry before failing @@ -12,6 +29,11 @@ export interface IAxiosRetryConfig { * default: false */ shouldResetTimeout?: boolean; + /** + * Disable other response interceptors when axios-retry is retrying the request + * default: false + */ + disableOtherResponseInterceptors?: boolean; /** * A callback to further control if a request should be retried. * default: it retries if it is a network error or a 5xx error on an idempotent request (GET, HEAD, OPTIONS, PUT or DELETE). @@ -141,6 +163,7 @@ export const DEFAULT_OPTIONS: Required = { retryCondition: isNetworkOrIdempotentRequestError, retryDelay: noDelay, shouldResetTimeout: false, + disableOtherResponseInterceptors: false, onRetry: () => {} }; @@ -196,13 +219,14 @@ async function shouldRetry( return shouldRetryOrPromise; } +let responseInterceptorId: number; const axiosRetry: AxiosRetry = (axiosInstance, defaultOptions) => { const requestInterceptorId = axiosInstance.interceptors.request.use((config) => { setCurrentState(config, defaultOptions); return config; }); - const responseInterceptorId = axiosInstance.interceptors.response.use(null, async (error) => { + responseInterceptorId = axiosInstance.interceptors.response.use(null, async (error) => { const { config } = error; // If we have no information to retry the request if (!config) { @@ -227,7 +251,23 @@ const axiosRetry: AxiosRetry = (axiosInstance, defaultOptions) => { config.transformRequest = [(data) => data]; await onRetry(currentState.retryCount, error, config); return new Promise((resolve) => { - setTimeout(() => resolve(axiosInstance(config)), delay); + setTimeout(() => { + if (currentState.disableOtherResponseInterceptors && currentState.retryCount === 1) { + const responseInterceptors = axiosInstance.interceptors + .response as AxiosResponseInterceptorManagerExtended; + const interceptors = responseInterceptors.handlers.splice(0, responseInterceptorId + 1); + + // Disable only intercepter on rejected (do not disable fullfilled) + responseInterceptors.handlers = interceptors.map((v, index) => { + if (index === responseInterceptorId) return v; + return { ...v, rejected: null }; + }); + + resolve(axiosInstance(config)); + return; + } + resolve(axiosInstance(config)); + }, delay); }); } return Promise.reject(error);