Skip to content

Commit

Permalink
Retry on 429, and handle Retry-After header
Browse files Browse the repository at this point in the history
  • Loading branch information
ctranstrum committed Apr 22, 2024
1 parent b48aa6c commit 6385598
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 5 deletions.
57 changes: 57 additions & 0 deletions spec/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import axiosRetry, {
isSafeRequestError,
isIdempotentRequestError,
exponentialDelay,
retryAfter,
isRetryableError,
namespace
} from '../src/index';
Expand Down Expand Up @@ -859,6 +860,62 @@ describe('exponentialDelay', () => {
});
});

describe('retryAfter', () => {
it('should understand a numeric Retry-After header', () => {
const errorResponse = new AxiosError('Error response');
errorResponse.response = { status: 429 } as AxiosError['response'];
// @ts-ignore
errorResponse.response.headers = { 'retry-after': '10' };
const time = retryAfter(errorResponse);

expect(time).toBe(10000);
});

it('should ignore a negative numeric Retry-After header', () => {
const errorResponse = new AxiosError('Error response');
errorResponse.response = { status: 429 } as AxiosError['response'];
// @ts-ignore
errorResponse.response.headers = { 'retry-after': '-10' };
const time = retryAfter(errorResponse);

expect(time).toBe(0);
});

it('should understand a date Retry-After header', () => {
const errorResponse = new AxiosError('Error response');
errorResponse.response = { status: 429 } as AxiosError['response'];
const date = new Date();
date.setSeconds(date.getSeconds() + 10);
// @ts-ignore
errorResponse.response.headers = { 'retry-after': date.toUTCString() };
const time = retryAfter(errorResponse);

expect(time >= 9000 && time <= 10000).toBe(true);
});

it('should ignore a past Retry-After header', () => {
const errorResponse = new AxiosError('Error response');
errorResponse.response = { status: 429 } as AxiosError['response'];
const date = new Date();
date.setSeconds(date.getSeconds() - 10);
// @ts-ignore
errorResponse.response.headers = { 'retry-after': date.toUTCString() };
const time = retryAfter(errorResponse);

expect(time).toBe(0);
});

it('should ignore an invalid Retry-After header', () => {
const errorResponse = new AxiosError('Error response');
errorResponse.response = { status: 429 } as AxiosError['response'];
// @ts-ignore
errorResponse.response.headers = { 'retry-after': 'a couple minutes' };
const time = retryAfter(errorResponse);

expect(time).toBe(0);
});
});

describe('isRetryableError(error)', () => {
it('should be false for aborted requests', () => {
const errorResponse = new AxiosError('Error response');
Expand Down
27 changes: 22 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ const IDEMPOTENT_HTTP_METHODS = SAFE_HTTP_METHODS.concat(['put', 'delete']);
export function isRetryableError(error: AxiosError): boolean {
return (
error.code !== 'ECONNABORTED' &&
(!error.response || (error.response.status >= 500 && error.response.status <= 599))
(!error.response ||
error.response.status === 429 ||
(error.response.status >= 500 && error.response.status <= 599))
);
}

Expand All @@ -127,16 +129,31 @@ export function isNetworkOrIdempotentRequestError(error: AxiosError): boolean {
return isNetworkError(error) || isIdempotentRequestError(error);
}

function noDelay() {
return 0;
export function retryAfter(error: AxiosError | undefined = undefined): number {
const retryAfterHeader = error?.response?.headers['retry-after'];
if (!retryAfterHeader) {
return 0;
}
// if the retry after header is a number, convert it to milliseconds
let retryAfterMs = (Number(retryAfterHeader) || 0) * 1000;
// If the retry after header is a date, get the number of milliseconds until that date
if (retryAfterMs === 0) {
retryAfterMs = (new Date(retryAfterHeader).valueOf() || 0) - Date.now();
}
return Math.max(0, retryAfterMs);
}

function noDelay(_retryNumber = 0, error: AxiosError | undefined = undefined) {
return Math.max(0, retryAfter(error));
}

export function exponentialDelay(
retryNumber = 0,
_error: AxiosError | undefined = undefined,
error: AxiosError | undefined = undefined,
delayFactor = 100
): number {
const delay = 2 ** retryNumber * delayFactor;
const calculatedDelay = 2 ** retryNumber * delayFactor;
const delay = Math.max(calculatedDelay, retryAfter(error));
const randomSum = delay * 0.2 * Math.random(); // 0-20% of the delay
return delay + randomSum;
}
Expand Down

0 comments on commit 6385598

Please sign in to comment.