Skip to content

Commit 2a401fa

Browse files
authored
Merge pull request #232 from reportportal/develop
Release 5.4.2
2 parents 6c0530a + 7cc14cc commit 2a401fa

9 files changed

Lines changed: 158 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
### Added
2+
- Allow configuring the HTTP retry strategy via `restClientConfig.retry` and tune the default policy.
3+
### Security
4+
- Updated versions of vulnerable packages (axios).
15

26
## [5.4.1] - 2025-07-24
37
### Added

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ When creating a client instance, you need to specify the following options:
6161
| headers | Optional | {} | The object with custom headers for internal http client. |
6262
| debug | Optional | false | This flag allows seeing the logs of the client. Useful for debugging. |
6363
| isLaunchMergeRequired | Optional | false | Allows client to merge launches into one at the end of the run via saving their UUIDs to the temp files at filesystem. At the end of the run launches can be merged using `mergeLaunches` method. Temp file format: `rplaunch-${launch_uuid}.tmp`. |
64-
| restClientConfig | Optional | Not set | `axios` like http client [config](https://github.com/axios/axios#request-config). May contain `agent` property for configure [http(s)](https://nodejs.org/api/https.html#https_https_request_url_options_callback) client, and other client options eg. `timeout`. For debugging and displaying logs you can set `debug: true`. |
64+
| restClientConfig | Optional | Not set | `axios` like http client [config](https://github.com/axios/axios#request-config). May contain `agent` property for configure [http(s)](https://nodejs.org/api/https.html#https_https_request_url_options_callback) client, and other client options eg. `timeout`. For debugging and displaying logs you can set `debug: true`. Use the `retry` property (number or [`axios-retry`](https://github.com/softonic/axios-retry#options) config) to customise automatic retries. |
6565
| launchUuidPrint | Optional | false | Whether to print the current launch UUID. |
6666
| launchUuidPrintOutput | Optional | 'STDOUT' | Launch UUID printing output. Possible values: 'STDOUT', 'STDERR', 'FILE', 'ENVIRONMENT'. Works only if `launchUuidPrint` set to `true`. File format: `rp-launch-uuid-${launch_uuid}.tmp`. Env variable: `RP_LAUNCH_UUID`. |
6767
| token | Deprecated | Not set | Use `apiKey` instead. |
@@ -88,6 +88,26 @@ There is a timeout on axios requests. If for instance the server your making a r
8888

8989
You can simply change this timeout by adding a `timeout` property to `restClientConfig` with your desired numeric value (in _ms_) or *0* to disable it.
9090

91+
### Retry configuration
92+
93+
The client retries failed HTTP calls up to 6 times with an exponential backoff (starting at 200 ms and capping at 5 s) and resets the axios timeout before each retry. Provide a `retry` option in `restClientConfig` to change that behaviour. The value can be either a number (overriding just the retry count) or a full [`axios-retry` configuration object](https://github.com/softonic/axios-retry#options):
94+
95+
```javascript
96+
const axiosRetry = require('axios-retry').default;
97+
98+
const client = new RPClient({
99+
// ... other options
100+
restClientConfig: {
101+
retry: {
102+
retries: 5,
103+
retryDelay: axiosRetry.exponentialDelay,
104+
},
105+
},
106+
});
107+
```
108+
109+
Setting `retry: 0` disables automatic retries.
110+
91111
### checkConnect
92112

93113
`checkConnect` - asynchronous method for verifying the correctness of the client connection

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
5.4.1
1+
5.4.2-SNAPSHOT

__tests__/client-id.spec.js

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,33 @@
11
const fs = require('fs');
22
const util = require('util');
3-
const os = require('os');
43
const path = require('path');
54
const { v4: uuidv4 } = require('uuid');
5+
6+
const testHomeDir = path.join(__dirname, '__tmp__', 'rp-home');
7+
process.env.RP_CLIENT_JS_HOME = testHomeDir;
68
const { getClientId } = require('../statistics/client-id');
79

810
const uuidv4Validation = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i;
9-
const clientIdFile = path.join(os.homedir(), '.rp', 'rp.properties');
11+
const clientIdFile = path.join(testHomeDir, '.rp', 'rp.properties');
1012

1113
const unlink = util.promisify(fs.unlink);
1214
const readFile = util.promisify(fs.readFile);
1315
const writeFile = util.promisify(fs.writeFile);
16+
const removeTestHomeDir = () => fs.promises.rm(testHomeDir, { recursive: true, force: true });
17+
const unlinkFile = async (filePath) => {
18+
try {
19+
await unlink(filePath);
20+
} catch (error) {
21+
if (error.code !== 'ENOENT') {
22+
throw error;
23+
}
24+
}
25+
};
1426

1527
describe('Client ID test suite', () => {
28+
beforeAll(removeTestHomeDir);
29+
afterAll(removeTestHomeDir);
30+
1631
it('getClientId should return the same client ID for two calls', async () => {
1732
const clientId1 = await getClientId();
1833
const clientId2 = await getClientId();
@@ -22,7 +37,7 @@ describe('Client ID test suite', () => {
2237

2338
it('getClientId should return different client IDs if store file removed', async () => {
2439
const clientId1 = await getClientId();
25-
await unlink(clientIdFile);
40+
await unlinkFile(clientIdFile);
2641
const clientId2 = await getClientId();
2742
expect(clientId2).not.toEqual(clientId1);
2843
});
@@ -33,14 +48,14 @@ describe('Client ID test suite', () => {
3348
});
3449

3550
it('getClientId should save client ID to ~/.rp/rp.properties', async () => {
36-
await unlink(clientIdFile);
51+
await unlinkFile(clientIdFile);
3752
const clientId = await getClientId();
3853
const content = await readFile(clientIdFile, 'utf-8');
3954
expect(content).toMatch(new RegExp(`^client\\.id\\s*=\\s*${clientId}\\s*(?:$|\n)`));
4055
});
4156

4257
it('getClientId should read client ID from ~/.rp/rp.properties', async () => {
43-
await unlink(clientIdFile);
58+
await unlinkFile(clientIdFile);
4459
const clientId = uuidv4(undefined, undefined, 0);
4560
await writeFile(clientIdFile, `client.id=${clientId}\n`, 'utf-8');
4661
expect(await getClientId()).toEqual(clientId);
@@ -50,7 +65,7 @@ describe('Client ID test suite', () => {
5065
'getClientId should read client ID from ~/.rp/rp.properties if it is not empty and client ID is the ' +
5166
'first line',
5267
async () => {
53-
await unlink(clientIdFile);
68+
await unlinkFile(clientIdFile);
5469
const clientId = uuidv4(undefined, undefined, 0);
5570
await writeFile(clientIdFile, `client.id=${clientId}\ntest.property=555\n`, 'utf-8');
5671
expect(await getClientId()).toEqual(clientId);
@@ -61,15 +76,15 @@ describe('Client ID test suite', () => {
6176
'getClientId should read client ID from ~/.rp/rp.properties if it is not empty and client ID is not the ' +
6277
'first line',
6378
async () => {
64-
await unlink(clientIdFile);
79+
await unlinkFile(clientIdFile);
6580
const clientId = uuidv4(undefined, undefined, 0);
6681
await writeFile(clientIdFile, `test.property=555\nclient.id=${clientId}\n`, 'utf-8');
6782
expect(await getClientId()).toEqual(clientId);
6883
},
6984
);
7085

7186
it('getClientId should write client ID to ~/.rp/rp.properties if it is not empty', async () => {
72-
await unlink(clientIdFile);
87+
await unlinkFile(clientIdFile);
7388
await writeFile(clientIdFile, `test.property=555`, 'utf-8');
7489
const clientId = await getClientId();
7590
const content = await readFile(clientIdFile, 'utf-8');

__tests__/rest.spec.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ describe('RestClient', () => {
1919
},
2020
};
2121
const noOptions = {};
22+
const getRetryAttempts = (client) => client.getRetryConfig().retries + 1;
2223
const restClient = new RestClient(options);
24+
const retryAttempts = getRetryAttempts(restClient);
2325

2426
const unathorizedError = {
2527
error: 'unauthorized',
@@ -53,6 +55,64 @@ describe('RestClient', () => {
5355
});
5456
});
5557

58+
describe('retry configuration', () => {
59+
it('uses a production-ready retry policy by default', () => {
60+
const retryConfig = restClient.getRetryConfig();
61+
62+
expect(retryConfig.retries).toBe(6);
63+
expect(retryAttempts).toBe(retryConfig.retries + 1);
64+
expect(retryConfig.shouldResetTimeout).toBe(true);
65+
expect(retryConfig.retryDelay(1)).toBe(200);
66+
expect(retryConfig.retryDelay(4)).toBe(1600);
67+
expect(retryConfig.retryDelay(10)).toBe(5000);
68+
});
69+
70+
it('uses custom retry attempts when a numeric value is provided', (done) => {
71+
const customRetries = 2;
72+
const client = new RestClient({
73+
...options,
74+
restClientConfig: {
75+
...options.restClientConfig,
76+
retry: customRetries,
77+
},
78+
});
79+
expect(getRetryAttempts(client)).toBe(customRetries + 1);
80+
81+
const scope = nock(options.baseURL)
82+
.get('/users/custom-retry-number')
83+
.replyWithError(netErrConnectionResetError);
84+
85+
client.retrieve('users/custom-retry-number', noOptions).catch((error) => {
86+
expect(error instanceof Error).toBeTruthy();
87+
expect(error.message).toMatch(netErrConnectionResetError.message);
88+
expect(scope.isDone()).toBeTruthy();
89+
90+
done();
91+
});
92+
});
93+
94+
it('merges retry configuration object from settings', () => {
95+
const customDelay = () => 250;
96+
const client = new RestClient({
97+
...options,
98+
restClientConfig: {
99+
...options.restClientConfig,
100+
retry: {
101+
retries: 4,
102+
retryDelay: customDelay,
103+
shouldResetTimeout: true,
104+
},
105+
},
106+
});
107+
108+
const retryConfig = client.getRetryConfig();
109+
110+
expect(retryConfig.retries).toBe(4);
111+
expect(retryConfig.retryDelay).toBe(customDelay);
112+
expect(retryConfig.shouldResetTimeout).toBe(true);
113+
});
114+
});
115+
56116
describe('buildPath', () => {
57117
it('compose path basing on base', () => {
58118
expect(restClient.buildPath('users')).toBe(`${options.baseURL}/users`);

lib/rest.js

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@ const https = require('https');
55
const logger = require('./logger');
66

77
const DEFAULT_MAX_CONNECTION_TIME_MS = 30000;
8-
9-
axiosRetry(axios, {
10-
retryDelay: () => 100,
11-
retries: 10,
8+
const DEFAULT_RETRY_ATTEMPTS = 6;
9+
const RETRY_BASE_DELAY_MS = 200;
10+
const RETRY_MAX_DELAY_MS = 5000;
11+
const DEFAULT_RETRY_CONFIG = {
12+
retryDelay: (retryCount = 1) =>
13+
Math.min(RETRY_BASE_DELAY_MS * 2 ** Math.max(retryCount - 1, 0), RETRY_MAX_DELAY_MS),
14+
retries: DEFAULT_RETRY_ATTEMPTS,
1215
retryCondition: axiosRetry.isRetryableError,
13-
});
16+
shouldResetTimeout: true,
17+
};
18+
const SKIPPED_REST_CONFIG_KEYS = ['agent', 'retry'];
1419

1520
class RestClient {
1621
constructor(options) {
@@ -24,6 +29,8 @@ class RestClient {
2429
...this.getRestConfig(this.restClientConfig),
2530
});
2631

32+
axiosRetry(this.axiosInstance, this.getRetryConfig());
33+
2734
if (this.restClientConfig?.debug) {
2835
logger.addLogger(this.axiosInstance);
2936
}
@@ -69,7 +76,7 @@ method: ${method}`,
6976
if (!this.restClientConfig) return {};
7077

7178
const config = Object.keys(this.restClientConfig).reduce((acc, key) => {
72-
if (key !== 'agent') {
79+
if (!SKIPPED_REST_CONFIG_KEYS.includes(key)) {
7380
acc[key] = this.restClientConfig[key];
7481
}
7582
return acc;
@@ -87,6 +94,26 @@ method: ${method}`,
8794
return config;
8895
}
8996

97+
getRetryConfig() {
98+
const retryOption = this.restClientConfig?.retry;
99+
100+
if (typeof retryOption === 'number') {
101+
return {
102+
...DEFAULT_RETRY_CONFIG,
103+
retries: retryOption,
104+
};
105+
}
106+
107+
if (retryOption && typeof retryOption === 'object') {
108+
return {
109+
...DEFAULT_RETRY_CONFIG,
110+
...retryOption,
111+
};
112+
}
113+
114+
return { ...DEFAULT_RETRY_CONFIG };
115+
}
116+
90117
create(path, data, options = {}) {
91118
return this.request('POST', this.buildPath(path), data, {
92119
...options,

package-lock.json

Lines changed: 12 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
"node": ">=14.x"
2626
},
2727
"dependencies": {
28-
"axios": "^1.8.4",
29-
"axios-retry": "^4.1.0",
28+
"axios": "^1.12.2",
29+
"axios-retry": "^4.5.0",
3030
"glob": "^8.1.0",
3131
"ini": "^2.0.0",
3232
"uniqid": "^5.4.0",

statistics/constants.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ const PJSON_NAME = pjson.name;
88
const CLIENT_ID_KEY = 'client.id';
99
const RP_FOLDER = '.rp';
1010
const RP_PROPERTIES_FILE = 'rp.properties';
11-
const RP_FOLDER_PATH = path.join(os.homedir(), RP_FOLDER);
11+
const HOME_DIRECTORY = process.env.RP_CLIENT_JS_HOME || os.homedir();
12+
const RP_FOLDER_PATH = path.join(HOME_DIRECTORY, RP_FOLDER);
1213
const RP_PROPERTIES_FILE_PATH = path.join(RP_FOLDER_PATH, RP_PROPERTIES_FILE);
1314
const CLIENT_INFO = Buffer.from(
1415
'Ry1XUDU3UlNHOFhMOmVFazhPMGJ0UXZ5MmI2VXVRT19TOFE=',

0 commit comments

Comments
 (0)