From 65d70edf8044db70ef8fc3093067da21605cfc5c Mon Sep 17 00:00:00 2001 From: yutak23 Date: Tue, 19 Mar 2024 19:46:35 +0900 Subject: [PATCH 1/2] feat: add onMaxRetryTimesExceeded option --- spec/index.spec.ts | 115 +++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 19 +++++++- 2 files changed, 133 insertions(+), 1 deletion(-) diff --git a/spec/index.spec.ts b/spec/index.spec.ts index 9c61473..89f9369 100644 --- a/spec/index.spec.ts +++ b/spec/index.spec.ts @@ -569,6 +569,121 @@ describe('axiosRetry(axios, { retries, onRetry })', () => { }); }); +describe('axiosRetry(axios, { onMaxRetryTimesExceeded })', () => { + const customError = new Error('CustomErrorAfterMaxRetryTimesExceeded'); + + afterEach(() => { + nock.cleanAll(); + nock.enableNetConnect(); + }); + + describe('when the onMaxRetryTimesExceeded is handled', () => { + it('should use onMaxRetryTimesExceeded set on request', (done) => { + const client = axios.create(); + setupResponses(client, [() => nock('http://example.com').get('/test').reply(500, 'Failed!')]); + let calledCount = 0; + let finalRetryCount = 0; + const onMaxRetryTimesExceeded = (err, retryCount) => { + calledCount += 1; + finalRetryCount = retryCount; + + expect(err).not.toBe(undefined); + }; + axiosRetry(client, { retries: 2, onMaxRetryTimesExceeded }); + client + .get('http://example.com/test') + .then( + () => done.fail(), + (error) => { + expect(calledCount).toBe(1); + expect(finalRetryCount).toBe(2); + done(); + } + ) + .catch(done.fail); + }); + + it('should reject with the custom error', (done) => { + const client = axios.create(); + setupResponses(client, [() => nock('http://example.com').get('/test').reply(500, 'Failed!')]); + const onMaxRetryTimesExceeded = () => { + throw customError; + }; + axiosRetry(client, { + retries: 2, + onMaxRetryTimesExceeded + }); + client + .get('http://example.com/test') + .then( + () => done.fail(), + (error) => { + expect(error).toEqual(customError); + done(); + } + ) + .catch(done.fail); + }); + }); + + describe('when the onMaxRetryTimesExceeded is returning a promise', () => { + it('should use onMaxRetryTimesExceeded set on request', (done) => { + const client = axios.create(); + setupResponses(client, [() => nock('http://example.com').get('/test').reply(500, 'Failed!')]); + let calledCount = 0; + let finalRetryCount = 0; + const onMaxRetryTimesExceeded = (err, retryCount) => + new Promise((resolve) => { + setTimeout(() => { + calledCount += 1; + finalRetryCount = retryCount; + + expect(err).not.toBe(undefined); + resolve(void 0); + }, 100); + }); + axiosRetry(client, { retries: 2, onMaxRetryTimesExceeded }); + client + .get('http://example.com/test') + .then( + () => done.fail(), + (error) => { + expect(calledCount).toBe(1); + expect(finalRetryCount).toBe(2); + done(); + } + ) + .catch(done.fail); + }); + + it('should reject with the custom error', (done) => { + const client = axios.create(); + setupResponses(client, [() => nock('http://example.com').get('/test').reply(500, 'Failed!')]); + const onMaxRetryTimesExceeded = (err, retryCount) => + new Promise((_resolve, reject) => { + setTimeout(() => { + expect(err).not.toBe(undefined); + reject(customError); + }, 100); + }); + axiosRetry(client, { + retries: 2, + onMaxRetryTimesExceeded + }); + client + .get('http://example.com/test') + .then( + () => done.fail(), + (error) => { + expect(error).toEqual(customError); + 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..898ed07 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,11 @@ export interface IAxiosRetryConfig { error: AxiosError, requestConfig: AxiosRequestConfig ) => Promise | void; + /** + * After all the retries are failed, this callback will be called with the last error + * before throwing the error. + */ + onMaxRetryTimesExceeded?: (error: AxiosError, retryCount: number) => Promise | void; } export interface IAxiosRetryConfigExtended extends IAxiosRetryConfig { @@ -141,7 +146,8 @@ export const DEFAULT_OPTIONS: Required = { retryCondition: isNetworkOrIdempotentRequestError, retryDelay: noDelay, shouldResetTimeout: false, - onRetry: () => {} + onRetry: () => {}, + onMaxRetryTimesExceeded: () => {} }; function getRequestOptions( @@ -196,6 +202,14 @@ async function shouldRetry( return shouldRetryOrPromise; } +async function handleMaxRetryTimesExceeded( + currentState: Required, + error: AxiosError +) { + if (currentState.retryCount >= currentState.retries) + await currentState.onMaxRetryTimesExceeded(error, currentState.retryCount); +} + const axiosRetry: AxiosRetry = (axiosInstance, defaultOptions) => { const requestInterceptorId = axiosInstance.interceptors.request.use((config) => { setCurrentState(config, defaultOptions); @@ -230,6 +244,9 @@ const axiosRetry: AxiosRetry = (axiosInstance, defaultOptions) => { setTimeout(() => resolve(axiosInstance(config)), delay); }); } + + await handleMaxRetryTimesExceeded(currentState, error); + return Promise.reject(error); }); From aeda463147c844cd6e5444c60757177138690d6f Mon Sep 17 00:00:00 2001 From: yutak23 Date: Tue, 19 Mar 2024 19:53:44 +0900 Subject: [PATCH 2/2] docs: add explanation for onMaxRetryTimesExceeded --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 243ea6e..5bf3076 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ client | shouldResetTimeout | `Boolean` | false | Defines if the timeout should be reset between retries | | retryDelay | `Function` | `function noDelay() { return 0; }` | A callback to further control the delay in milliseconds between retried requests. By default there is no delay between retries. Another option is exponentialDelay ([Exponential Backoff](https://developers.google.com/analytics/devguides/reporting/core/v3/errors#backoff)). The function is passed `retryCount` and `error`. | | onRetry | `Function` | `function onRetry(retryCount, error, requestConfig) { return; }` | A callback to notify when a retry is about to occur. Useful for tracing and you can any async process for example refresh a token on 401. By default nothing will occur. The function is passed `retryCount`, `error`, and `requestConfig`. | +| onMaxRetryTimesExceeded | `Function` | `function onMaxRetryTimesExceeded(error, retryCount) { return; }` | After all the retries are failed, this callback will be called with the last error before throwing the error. | ## Testing