@trayio/axios 5.15.0 → 5.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -4,17 +4,18 @@ import { HttpMethod, HttpRequest, HttpResponse } from '@trayio/commons/http/Http
|
|
|
4
4
|
import FormData = require('form-data');
|
|
5
5
|
import { FileStorage } from '@trayio/commons/file/File';
|
|
6
6
|
type AxiosOptions = {
|
|
7
|
-
rejectUnauthorized
|
|
8
|
-
followRedirects
|
|
7
|
+
rejectUnauthorized?: boolean;
|
|
8
|
+
followRedirects?: boolean;
|
|
9
9
|
retries?: number;
|
|
10
10
|
keepAlive?: boolean;
|
|
11
11
|
maxSockets?: number;
|
|
12
|
+
timeout?: number;
|
|
12
13
|
};
|
|
13
14
|
export declare class AxiosHttpClient implements HttpClient {
|
|
14
15
|
private readonly fileStorage;
|
|
15
|
-
private readonly axiosOptions;
|
|
16
16
|
private readonly defaultHttpsAgent;
|
|
17
17
|
private readonly axiosInstance;
|
|
18
|
+
private readonly resolvedOptions;
|
|
18
19
|
constructor(fileStorage?: FileStorage, axiosOptions?: AxiosOptions);
|
|
19
20
|
execute(method: HttpMethod, url: string, request: HttpRequest): TE.TaskEither<Error, HttpResponse>;
|
|
20
21
|
private axiosErrorToHttpResponse;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AxiosHttpClient.d.ts","sourceRoot":"","sources":["../../src/http/AxiosHttpClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAEvC,OAAO,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAC7D,OAAO,EAEN,UAAU,EACV,WAAW,EAEX,YAAY,EAEZ,MAAM,2BAA2B,CAAC;AAWnC,OAAO,QAAQ,GAAG,QAAQ,WAAW,CAAC,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAMxD,KAAK,YAAY,GAAG;IACnB,kBAAkB,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"AxiosHttpClient.d.ts","sourceRoot":"","sources":["../../src/http/AxiosHttpClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAEvC,OAAO,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAC7D,OAAO,EAEN,UAAU,EACV,WAAW,EAEX,YAAY,EAEZ,MAAM,2BAA2B,CAAC;AAWnC,OAAO,QAAQ,GAAG,QAAQ,WAAW,CAAC,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAMxD,KAAK,YAAY,GAAG;IACnB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAWF,qBAAa,eAAgB,YAAW,UAAU;IAMhD,OAAO,CAAC,QAAQ,CAAC,WAAW;IAL7B,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAQ;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA8B;IAC5D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAuB;gBAGrC,WAAW,GAAE,WAAqC,EACnE,YAAY,GAAE,YAAiB;IAgDhC,OAAO,CACN,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,WAAW,GAClB,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,YAAY,CAAC;IAsHrC,OAAO,CAAC,wBAAwB;IAmBhC,OAAO,CAAC,2BAA2B;IAUnC,YAAY,aAAc,QAAQ,cAAc,OAAO,MAAM,EAAE,MAAM,CAAC,cAKpE;CACF"}
|
|
@@ -39,26 +39,33 @@ const BufferExtensions_1 = require("@trayio/commons/buffer/BufferExtensions");
|
|
|
39
39
|
const function_1 = require("fp-ts/lib/function");
|
|
40
40
|
class AxiosHttpClient {
|
|
41
41
|
fileStorage;
|
|
42
|
-
axiosOptions;
|
|
43
42
|
defaultHttpsAgent;
|
|
44
43
|
axiosInstance;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
followRedirects: true,
|
|
48
|
-
retries: 0,
|
|
49
|
-
}) {
|
|
44
|
+
resolvedOptions;
|
|
45
|
+
constructor(fileStorage = new NodeFsFileStorage_1.NodeFsFileStorage(), axiosOptions = {}) {
|
|
50
46
|
this.fileStorage = fileStorage;
|
|
51
|
-
|
|
47
|
+
// Resolve all defaults in one place
|
|
48
|
+
this.resolvedOptions = {
|
|
49
|
+
rejectUnauthorized: axiosOptions.rejectUnauthorized ?? false,
|
|
50
|
+
followRedirects: axiosOptions.followRedirects ?? true,
|
|
51
|
+
// Validate and clamp retries to 0-10 range, default to 0
|
|
52
|
+
retries: Math.max(0, Math.min(10, axiosOptions.retries ?? 0)),
|
|
53
|
+
keepAlive: axiosOptions.keepAlive ?? true,
|
|
54
|
+
maxSockets: axiosOptions.maxSockets,
|
|
55
|
+
/* Default to 0 (no timeout) to match axios default and maintain
|
|
56
|
+
behavioural backwards compatibility with codebases already using
|
|
57
|
+
this module. */
|
|
58
|
+
// Users should explicitly set a timeout value (e.g., 30000 for 30s) if they need timeout protection.
|
|
59
|
+
timeout: axiosOptions.timeout ?? 0,
|
|
60
|
+
};
|
|
52
61
|
// We need a keep-alive heartbeat shorter than 350 seconds to bypass the idle timeout in AWS NAT/LB servers:
|
|
53
62
|
// https://repost.aws/knowledge-center/lambda-vpc-timeout
|
|
54
63
|
this.defaultHttpsAgent = new https_1.Agent({
|
|
55
|
-
keepAlive: this.
|
|
64
|
+
keepAlive: this.resolvedOptions.keepAlive,
|
|
56
65
|
keepAliveMsecs: 42000,
|
|
57
|
-
rejectUnauthorized: this.
|
|
58
|
-
maxSockets: this.
|
|
66
|
+
rejectUnauthorized: this.resolvedOptions.rejectUnauthorized,
|
|
67
|
+
maxSockets: this.resolvedOptions.maxSockets,
|
|
59
68
|
});
|
|
60
|
-
// Validate and clamp retries to 0-10 range, default to 0
|
|
61
|
-
const retries = Math.max(0, Math.min(10, this.axiosOptions.retries ?? 0));
|
|
62
69
|
// Create isolated axios instance
|
|
63
70
|
this.axiosInstance = axios_1.default.create();
|
|
64
71
|
// Clear all default headers to ensure no default headers are added
|
|
@@ -70,7 +77,7 @@ class AxiosHttpClient {
|
|
|
70
77
|
this.axiosInstance.defaults.headers.delete = {};
|
|
71
78
|
// Configure retry behavior
|
|
72
79
|
(0, axios_retry_1.default)(this.axiosInstance, {
|
|
73
|
-
retries,
|
|
80
|
+
retries: this.resolvedOptions.retries,
|
|
74
81
|
retryDelay: axios_retry_1.exponentialDelay,
|
|
75
82
|
retryCondition: (error) =>
|
|
76
83
|
// Retry on network errors (ECONNREFUSED, ETIMEDOUT, etc.) and 5xx errors
|
|
@@ -96,10 +103,10 @@ class AxiosHttpClient {
|
|
|
96
103
|
const axiosHttpsAgent = (0, function_1.pipe)(request.agent, O.chain((agent) => agent.certificate), O.match(() => this.defaultHttpsAgent, (certificate) => new https_1.Agent({
|
|
97
104
|
cert: certificate.cert,
|
|
98
105
|
key: certificate.key,
|
|
99
|
-
keepAlive: this.
|
|
106
|
+
keepAlive: this.resolvedOptions.keepAlive,
|
|
100
107
|
keepAliveMsecs: 42000,
|
|
101
|
-
rejectUnauthorized: this.
|
|
102
|
-
maxSockets: this.
|
|
108
|
+
rejectUnauthorized: this.resolvedOptions.rejectUnauthorized,
|
|
109
|
+
maxSockets: this.resolvedOptions.maxSockets,
|
|
103
110
|
})));
|
|
104
111
|
if (headers['content-type'] &&
|
|
105
112
|
headers['content-type'].includes(Http_1.HttpContentType.MultipartRequestBody)) {
|
|
@@ -121,6 +128,8 @@ class AxiosHttpClient {
|
|
|
121
128
|
headers,
|
|
122
129
|
httpsAgent: axiosHttpsAgent,
|
|
123
130
|
params: request.queryString,
|
|
131
|
+
...(this.resolvedOptions.followRedirects ? {} : { maxRedirects: 0 }),
|
|
132
|
+
timeout: (0, function_1.pipe)(request.timeout ?? O.none, O.getOrElse(() => this.resolvedOptions.timeout)),
|
|
124
133
|
};
|
|
125
134
|
return (0, Task_1.createTaskEitherFromPromiseWithSimpleError)(() => this.axiosInstance(axiosConfig)
|
|
126
135
|
.then(this.axiosResponseToHttpResponse)
|
|
@@ -134,7 +143,8 @@ class AxiosHttpClient {
|
|
|
134
143
|
headers,
|
|
135
144
|
httpsAgent: axiosHttpsAgent,
|
|
136
145
|
params: request.queryString,
|
|
137
|
-
...(this.
|
|
146
|
+
...(this.resolvedOptions.followRedirects ? {} : { maxRedirects: 0 }),
|
|
147
|
+
timeout: (0, function_1.pipe)(request.timeout ?? O.none, O.getOrElse(() => this.resolvedOptions.timeout)),
|
|
138
148
|
};
|
|
139
149
|
return (0, Task_1.createTaskEitherFromPromiseWithSimpleError)(() => this.axiosInstance(axiosConfig)
|
|
140
150
|
.then(this.axiosResponseToHttpResponse)
|
|
@@ -28,6 +28,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
29
|
const HttpClient_abstract_test_1 = require("@trayio/commons/http/HttpClient.abstract.test");
|
|
30
30
|
const NodeFsFileStorage_1 = require("@trayio/commons/file/NodeFsFileStorage");
|
|
31
|
+
const O = __importStar(require("fp-ts/Option"));
|
|
31
32
|
const E = __importStar(require("fp-ts/Either"));
|
|
32
33
|
const Http_1 = require("@trayio/commons/http/Http");
|
|
33
34
|
const BufferExtensions_1 = require("@trayio/commons/buffer/BufferExtensions");
|
|
@@ -196,4 +197,163 @@ describe('AxiosHttpClient Tests', () => {
|
|
|
196
197
|
}
|
|
197
198
|
});
|
|
198
199
|
});
|
|
200
|
+
describe('Timeout mechanism', () => {
|
|
201
|
+
afterEach(() => {
|
|
202
|
+
nock_1.default.cleanAll();
|
|
203
|
+
});
|
|
204
|
+
test('should timeout when request exceeds configured timeout', async () => {
|
|
205
|
+
const axiosHttpClient = new AxiosHttpClient_1.AxiosHttpClient(new NodeFsFileStorage_1.NodeFsFileStorage(), {
|
|
206
|
+
rejectUnauthorized: false,
|
|
207
|
+
followRedirects: true,
|
|
208
|
+
timeout: 50, // 50ms timeout (smaller = faster test)
|
|
209
|
+
});
|
|
210
|
+
const testUrl = 'http://test-timeout.example.com';
|
|
211
|
+
// Mock a slow response with much larger delay for reliability
|
|
212
|
+
(0, nock_1.default)(testUrl)
|
|
213
|
+
.get('/')
|
|
214
|
+
.delay(500) // 500ms delay >> 50ms timeout (10x margin)
|
|
215
|
+
.reply(200, { success: true });
|
|
216
|
+
const response = await axiosHttpClient.execute(Http_1.HttpMethod.Get, testUrl, Http_1.HttpRequest.create({
|
|
217
|
+
headers: {},
|
|
218
|
+
pathParams: {},
|
|
219
|
+
queryString: {},
|
|
220
|
+
body: BufferExtensions_1.BufferExtensions.arrayBufferToReadable(new ArrayBuffer(0)),
|
|
221
|
+
}))();
|
|
222
|
+
// Should get error response due to timeout
|
|
223
|
+
expect(E.isRight(response)).toBe(true);
|
|
224
|
+
if (E.isRight(response)) {
|
|
225
|
+
// AxiosHttpClient converts timeout errors to 500 responses
|
|
226
|
+
expect(response.right.statusCode).toBe(500);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
test('should use default timeout of 0 (no timeout) when not specified', async () => {
|
|
230
|
+
const axiosHttpClient = new AxiosHttpClient_1.AxiosHttpClient(new NodeFsFileStorage_1.NodeFsFileStorage(), {
|
|
231
|
+
rejectUnauthorized: false,
|
|
232
|
+
followRedirects: true,
|
|
233
|
+
// timeout not specified, should default to 0 (matching axios default)
|
|
234
|
+
});
|
|
235
|
+
// Verify the resolved timeout is 0 (no timeout, matching axios default)
|
|
236
|
+
// Using bracket notation to access private field in tests (TypeScript allows this for dynamic access)
|
|
237
|
+
const resolvedTimeout = axiosHttpClient['resolvedOptions'].timeout;
|
|
238
|
+
expect(resolvedTimeout).toBe(0);
|
|
239
|
+
const testUrl = 'http://test-default-timeout.example.com';
|
|
240
|
+
// Also verify it works functionally - request succeeds (no timeout)
|
|
241
|
+
(0, nock_1.default)(testUrl)
|
|
242
|
+
.get('/')
|
|
243
|
+
.delay(50) // Fast response
|
|
244
|
+
.reply(200, { success: true });
|
|
245
|
+
const response = await axiosHttpClient.execute(Http_1.HttpMethod.Get, testUrl, Http_1.HttpRequest.create({
|
|
246
|
+
headers: {},
|
|
247
|
+
pathParams: {},
|
|
248
|
+
queryString: {},
|
|
249
|
+
body: BufferExtensions_1.BufferExtensions.arrayBufferToReadable(new ArrayBuffer(0)),
|
|
250
|
+
}))();
|
|
251
|
+
expect(E.isRight(response)).toBe(true);
|
|
252
|
+
if (E.isRight(response)) {
|
|
253
|
+
expect(response.right.statusCode).toBe(200);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
test('should allow custom timeout values', async () => {
|
|
257
|
+
const axiosHttpClient = new AxiosHttpClient_1.AxiosHttpClient(new NodeFsFileStorage_1.NodeFsFileStorage(), {
|
|
258
|
+
rejectUnauthorized: false,
|
|
259
|
+
followRedirects: true,
|
|
260
|
+
timeout: 5000, // 5 second custom timeout
|
|
261
|
+
});
|
|
262
|
+
const testUrl = 'http://test-custom-timeout.example.com';
|
|
263
|
+
(0, nock_1.default)(testUrl).get('/').reply(200, { success: true });
|
|
264
|
+
const response = await axiosHttpClient.execute(Http_1.HttpMethod.Get, testUrl, Http_1.HttpRequest.create({
|
|
265
|
+
headers: {},
|
|
266
|
+
pathParams: {},
|
|
267
|
+
queryString: {},
|
|
268
|
+
body: BufferExtensions_1.BufferExtensions.arrayBufferToReadable(new ArrayBuffer(0)),
|
|
269
|
+
}))();
|
|
270
|
+
expect(E.isRight(response)).toBe(true);
|
|
271
|
+
if (E.isRight(response)) {
|
|
272
|
+
expect(response.right.statusCode).toBe(200);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
test('should use per-request timeout when provided, overriding constructor timeout', async () => {
|
|
276
|
+
const axiosHttpClient = new AxiosHttpClient_1.AxiosHttpClient(new NodeFsFileStorage_1.NodeFsFileStorage(), {
|
|
277
|
+
rejectUnauthorized: false,
|
|
278
|
+
followRedirects: true,
|
|
279
|
+
timeout: 5000, // 5 second constructor-level timeout (long)
|
|
280
|
+
});
|
|
281
|
+
const testUrl = 'http://test-per-request-timeout.example.com';
|
|
282
|
+
// Mock a slow response (500ms delay)
|
|
283
|
+
(0, nock_1.default)(testUrl)
|
|
284
|
+
.get('/')
|
|
285
|
+
.delay(500) // 500ms delay
|
|
286
|
+
.reply(200, { success: true });
|
|
287
|
+
const response = await axiosHttpClient.execute(Http_1.HttpMethod.Get, testUrl, Http_1.HttpRequest.create({
|
|
288
|
+
headers: {},
|
|
289
|
+
pathParams: {},
|
|
290
|
+
queryString: {},
|
|
291
|
+
body: BufferExtensions_1.BufferExtensions.arrayBufferToReadable(new ArrayBuffer(0)),
|
|
292
|
+
timeout: O.some(50), // 50ms per-request timeout (short) - should override constructor timeout
|
|
293
|
+
}))();
|
|
294
|
+
// Should timeout because per-request timeout (50ms) is used instead of constructor timeout (5000ms)
|
|
295
|
+
expect(E.isRight(response)).toBe(true);
|
|
296
|
+
if (E.isRight(response)) {
|
|
297
|
+
// AxiosHttpClient converts timeout errors to 500 responses
|
|
298
|
+
expect(response.right.statusCode).toBe(500);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
test('should use constructor timeout when per-request timeout is O.none', async () => {
|
|
302
|
+
const axiosHttpClient = new AxiosHttpClient_1.AxiosHttpClient(new NodeFsFileStorage_1.NodeFsFileStorage(), {
|
|
303
|
+
rejectUnauthorized: false,
|
|
304
|
+
followRedirects: true,
|
|
305
|
+
timeout: 50, // 50ms constructor-level timeout (short)
|
|
306
|
+
});
|
|
307
|
+
const testUrl = 'http://test-none-timeout.example.com';
|
|
308
|
+
// Mock a slow response (500ms delay)
|
|
309
|
+
(0, nock_1.default)(testUrl)
|
|
310
|
+
.get('/')
|
|
311
|
+
.delay(500) // 500ms delay
|
|
312
|
+
.reply(200, { success: true });
|
|
313
|
+
const response = await axiosHttpClient.execute(Http_1.HttpMethod.Get, testUrl, Http_1.HttpRequest.create({
|
|
314
|
+
headers: {},
|
|
315
|
+
pathParams: {},
|
|
316
|
+
queryString: {},
|
|
317
|
+
body: BufferExtensions_1.BufferExtensions.arrayBufferToReadable(new ArrayBuffer(0)),
|
|
318
|
+
timeout: O.none, // No per-request timeout - should use constructor timeout (50ms)
|
|
319
|
+
}))();
|
|
320
|
+
// Should timeout because constructor timeout (50ms) is used
|
|
321
|
+
expect(E.isRight(response)).toBe(true);
|
|
322
|
+
if (E.isRight(response)) {
|
|
323
|
+
// AxiosHttpClient converts timeout errors to 500 responses
|
|
324
|
+
expect(response.right.statusCode).toBe(500);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
test('should use constructor timeout when timeout field is omitted (backward compatibility)', async () => {
|
|
328
|
+
const axiosHttpClient = new AxiosHttpClient_1.AxiosHttpClient(new NodeFsFileStorage_1.NodeFsFileStorage(), {
|
|
329
|
+
rejectUnauthorized: false,
|
|
330
|
+
followRedirects: true,
|
|
331
|
+
timeout: 50, // 50ms constructor-level timeout (short)
|
|
332
|
+
});
|
|
333
|
+
const testUrl = 'http://test-omitted-timeout.example.com';
|
|
334
|
+
// Mock a slow response (500ms delay)
|
|
335
|
+
(0, nock_1.default)(testUrl)
|
|
336
|
+
.get('/')
|
|
337
|
+
.delay(500) // 500ms delay
|
|
338
|
+
.reply(200, { success: true });
|
|
339
|
+
// Create request without timeout field (backward compatible with old code)
|
|
340
|
+
const request = {
|
|
341
|
+
headers: {},
|
|
342
|
+
pathParams: {},
|
|
343
|
+
queryString: {},
|
|
344
|
+
body: BufferExtensions_1.BufferExtensions.arrayBufferToReadable(new ArrayBuffer(0)),
|
|
345
|
+
headerOptions: O.none,
|
|
346
|
+
agent: O.none,
|
|
347
|
+
clientIp: O.none,
|
|
348
|
+
// timeout field omitted - should use constructor timeout (50ms)
|
|
349
|
+
};
|
|
350
|
+
const response = await axiosHttpClient.execute(Http_1.HttpMethod.Get, testUrl, request)();
|
|
351
|
+
// Should timeout because constructor timeout (50ms) is used
|
|
352
|
+
expect(E.isRight(response)).toBe(true);
|
|
353
|
+
if (E.isRight(response)) {
|
|
354
|
+
// AxiosHttpClient converts timeout errors to 500 responses
|
|
355
|
+
expect(response.right.statusCode).toBe(500);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
});
|
|
199
359
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trayio/axios",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.17.0",
|
|
4
4
|
"description": "Axios extensions and implementations",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./*": "./dist/*.js"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"access": "public"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@trayio/commons": "5.
|
|
17
|
+
"@trayio/commons": "5.17.0",
|
|
18
18
|
"axios": "1.13.5",
|
|
19
19
|
"axios-retry": "4.5.0",
|
|
20
20
|
"form-data": "4.0.4"
|