Skip to content

Commit

Permalink
feat(request): add retry capability to the requests
Browse files Browse the repository at this point in the history
EME-6117

Co-authored-by: Gabor Soos <gabor.soos@emarsys.com>
  • Loading branch information
Zsombor Margetan and Gabor Soos committed Apr 6, 2023
1 parent 8f8d994 commit 49b3a0d
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 2 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"dependencies": {
"@emartech/json-logger": "7.2.3",
"axios": "1.3.5",
"axios-retry": "3.4.0",
"escher-auth": "3.2.4"
},
"devDependencies": {
Expand Down
23 changes: 23 additions & 0 deletions src/request.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const Escher = require('escher-auth');
import http from 'http';
import https from 'https';
import { EscherRequest, EscherRequestOption } from './request';
import nock from 'nock';
import { IAxiosRetryConfig } from 'axios-retry';

describe('EscherRequest', function() {
const serviceConfig = {
Expand Down Expand Up @@ -35,6 +37,10 @@ describe('EscherRequest', function() {
escherRequest = EscherRequest.create('key-id', 'secret', requestOptions);
});

afterEach(() => {
nock.cleanAll();
});

it('should sign headers of GET request', async () => {
await escherRequest.get('/path');

Expand Down Expand Up @@ -187,4 +193,21 @@ describe('EscherRequest', function() {
expect(requestArgument.httpAgent).to.eql(escherRequest.httpAgent);
expect(requestArgument.httpsAgent).to.eql(escherRequest.httpsAgent);
});

it('should retry the request if retryConfig exists', async () => {
requestStub.restore();
nock('https://localhost:1234')
.get('/api/purchases/1/content').times(1)
.reply(500)
.get('/api/purchases/1/content')
.reply(200, { data: 1 }, { 'content-type': 'application/json' },);
const retryConfig: IAxiosRetryConfig = { retries: 1 };
requestOptions = new EscherRequestOption(serviceConfig.host, { ...serviceConfig, retryConfig });
escherRequest = EscherRequest.create('key-id', 'secret', requestOptions);

const response = await escherRequest.get('/purchases/1/content');

expect(response.statusCode).to.eql(200);
expect(response.body).to.eql({ data: 1 });
});
});
3 changes: 2 additions & 1 deletion src/requestOption.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ describe('EscherRequestOption', function() {
prefix: '/api',
rejectUnauthorized: false,
timeout: 15000,
maxContentLength: 10485760
maxContentLength: 10485760,
retryConfig: null
});
expect(requestOptions.toHash()).to.not.have.property('allowEmptyResponse');
});
Expand Down
8 changes: 7 additions & 1 deletion src/requestOption.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { IAxiosRetryConfig } from 'axios-retry';

const MEGA_BYTE = 1024 * 1024;

export interface RequestOptions {
Expand All @@ -12,6 +14,7 @@ export interface RequestOptions {
maxContentLength?: number;
keepAlive?: boolean;
credentialScope?: string;
retryConfig?: IAxiosRetryConfig | null;
}

export class EscherRequestOption implements RequestOptions {
Expand All @@ -26,6 +29,7 @@ export class EscherRequestOption implements RequestOptions {
maxContentLength = 10 * MEGA_BYTE;
keepAlive = false;
credentialScope = '';
retryConfig: IAxiosRetryConfig | null = null;

public static createForInternalApi(host: string | RequestOptions, rejectUnauthorized: boolean) {
return this.create(host, '/api/v2/internal', rejectUnauthorized);
Expand Down Expand Up @@ -60,6 +64,7 @@ export class EscherRequestOption implements RequestOptions {
this.allowEmptyResponse = false;
this.maxContentLength = options.maxContentLength || 10 * MEGA_BYTE;
this.keepAlive = !!options.keepAlive;
this.retryConfig = options.retryConfig || null;

if (!options) {
options = {};
Expand Down Expand Up @@ -116,7 +121,8 @@ export class EscherRequestOption implements RequestOptions {
headers: this.headers.slice(0),
prefix: this.prefix,
timeout: this.timeout,
maxContentLength: this.maxContentLength
maxContentLength: this.maxContentLength,
retryConfig: this.retryConfig
};

if (!this.rejectUnauthorized) {
Expand Down
53 changes: 53 additions & 0 deletions src/wrapper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ import { ExtendedRequestOption, RequestWrapper } from './wrapper';
import { EscherRequestError } from './requestError';
import { AxiosRequestConfig } from 'axios';
import nock from 'nock';
import { IAxiosRetryConfig } from 'axios-retry';

describe('RequestWrapper', function() {
afterEach(() => {
nock.cleanAll();
});
describe('functionality tests', () => {
let apiResponse: any;
let expectedApiResponse: any;
Expand Down Expand Up @@ -310,4 +314,53 @@ describe('RequestWrapper', function() {
});
});
});
describe('retry test', () => {
const requestOptions = {
secure: true,
port: 443,
host: 'very.host.io',
method: 'get',
url: 'http://very.host.io:443/purchases/1/content',
path: '/purchases/1/content'
};

it('should not retry if error code is below 500', async () => {
nock('http://very.host.io:443')
.get('/purchases/1/content').times(1)
.reply(404, { replyText: '404 Not Found' })
.get('/purchases/1/content')
.reply(200, { data: 1 }, { 'content-type': 'application/json' },);
const retryConfig: IAxiosRetryConfig = { retries: 1 };
const wrapper = new RequestWrapper({ ...requestOptions, retryConfig }, 'http:', undefined);

try {
await wrapper.send();
} catch (err) {
const error = err as EscherRequestError;
expect(error).to.be.an.instanceOf(EscherRequestError);
expect(error.code).to.eql(404);
expect(error.message).to.eql('Error in http response (status: 404)');
expect(error.data).to.eql(JSON.stringify({ replyText: '404 Not Found' }));
}
});

it('should send the request with the correct retry', async () => {
nock('http://very.host.io:443')
.get('/purchases/1/content').times(1)
.reply(500)
.get('/purchases/1/content')
.reply(200, { data: 1 }, { 'content-type': 'application/json' },);
const expectedApiResponse = {
headers: { 'content-type': 'application/json' },
body: { data: 1 },
statusCode: 200
};
const retryConfig: IAxiosRetryConfig = { retries: 1 };
const wrapper = new RequestWrapper({ ...requestOptions, retryConfig }, 'http:', undefined);

const response = await wrapper.send();

expect(response).to.containSubset(expectedApiResponse);
});
});
});
6 changes: 6 additions & 0 deletions src/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Agent as HttpAgent } from 'http';
import { Agent as HttpsAgent } from 'https';
import axios from 'axios';
import { createLogger } from '@emartech/json-logger';
import axiosRetry from 'axios-retry';
const logger = createLogger('suiterequest');
const debugLogger = createLogger('suiterequest-debug');

Expand Down Expand Up @@ -54,6 +55,7 @@ export class RequestWrapper {
const timer = logger.timer();

const method = this.requestOptions.method.toLowerCase();
const retryConfig = this.requestOptions.retryConfig;
const reqOptions = this.getRequestOptions();
const source = axios.CancelToken.source();

Expand All @@ -75,6 +77,10 @@ export class RequestWrapper {

const client = axios.create();

if (retryConfig) {
axiosRetry(client, retryConfig);
}

return client
.request(axiosOptions)
.then(
Expand Down

0 comments on commit 49b3a0d

Please sign in to comment.