diff --git a/src/axios.ts b/src/axios.ts index 73ca050..4883869 100644 --- a/src/axios.ts +++ b/src/axios.ts @@ -1,6 +1,7 @@ import axios, {AxiosError, AxiosInstance, AxiosRequestConfig} from 'axios'; import axiosRetry, {IAxiosRetryConfig, isRetryableError} from 'axios-retry'; import isRetryAllowed from 'is-retry-allowed'; +import {isNil} from 'lodash'; import {Logger} from 'pino'; import {wrapApiError} from './errors'; @@ -43,8 +44,13 @@ export function makeAxiosInstanceWithRetry( return makeAxiosInstance(config, { retries, retryCondition: (error) => { + const isGraphQLEndpoint = !isNil(error.config.url) + && error.config.url.endsWith('graphql') + && error.config.method === 'post'; // Timeouts should be retryable - return isNetworkError(error) || isRetryableError(error); + // 409 is an edit conflict error, which is retryable for GraphQL endpoints + return isNetworkError(error) || isRetryableError(error) + || isGraphQLEndpoint && error.response?.status === 409; }, retryDelay: (retryNumber, error) => { if (logger) { diff --git a/test/axios.test.ts b/test/axios.test.ts index 800e741..21c12bb 100644 --- a/test/axios.test.ts +++ b/test/axios.test.ts @@ -11,6 +11,18 @@ describe('axios', () => { transport: {target: 'pino-pretty', options: {levelFirst: true}}, }); + function mockPostCode(url: string, code: number) { + return nock(apiUrl) + .post(url) + .reply(code) + .post(url) + .reply(code) + .post(url) + .reply(code) + .post(url) + .reply(200, {tenantId: '1'}); + } + test('get resource with retry', async () => { const mock = nock(apiUrl) .get('/hi') @@ -33,16 +45,22 @@ describe('axios', () => { mock.done(); }); + test('get graphql with 409 should fail', async () => { + const mock = nock(apiUrl).get('/graphs/foo/graphql').reply(409); + const client = sut.makeAxiosInstanceWithRetry( + {baseURL: apiUrl}, + undefined, + 3, + 100 + ); + await expect(client.get('/graphs/foo/graphql')).rejects.toThrowError( + 'Request failed with status code 409' + ); + mock.done(); + }); + test('post resource with retry', async () => { - const mock = nock(apiUrl) - .post('/hi') - .reply(502) - .post('/hi') - .reply(502) - .post('/hi') - .reply(502) - .post('/hi') - .reply(200, {tenantId: '1'}); + const mock = mockPostCode('/hi', 502); const client = sut.makeAxiosInstanceWithRetry( {baseURL: apiUrl}, undefined, @@ -55,6 +73,34 @@ describe('axios', () => { mock.done(); }); + test('post graphql with 409 should retry', async () => { + const mock = mockPostCode('/graphs/foo/graphql', 409); + const client = sut.makeAxiosInstanceWithRetry( + {baseURL: apiUrl}, + undefined, + 3, + 100 + ); + const res = await client.post('/graphs/foo/graphql'); + expect(res.status).toBe(200); + expect(res.data).toStrictEqual({tenantId: '1'}); + mock.done(); + }); + + test('post non-graphql with 409 should fail', async () => { + const mock = nock(apiUrl).post('/hi').reply(409); + const client = sut.makeAxiosInstanceWithRetry( + {baseURL: apiUrl}, + undefined, + 3, + 100 + ); + await expect(client.post('/hi')).rejects.toThrowError( + 'Request failed with status code 409' + ); + mock.done(); + }); + test('give up after a retry', async () => { const mock = nock(apiUrl).get('/hi').reply(502).get('/hi').reply(404); const client = sut.makeAxiosInstanceWithRetry(