@trayio/axios 5.15.0 → 5.16.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;IAgHrC,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: 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: this.resolvedOptions.timeout,
|
|
138
148
|
};
|
|
139
149
|
return (0, Task_1.createTaskEitherFromPromiseWithSimpleError)(() => this.axiosInstance(axiosConfig)
|
|
140
150
|
.then(this.axiosResponseToHttpResponse)
|
|
@@ -196,4 +196,80 @@ describe('AxiosHttpClient Tests', () => {
|
|
|
196
196
|
}
|
|
197
197
|
});
|
|
198
198
|
});
|
|
199
|
+
describe('Timeout mechanism', () => {
|
|
200
|
+
afterEach(() => {
|
|
201
|
+
nock_1.default.cleanAll();
|
|
202
|
+
});
|
|
203
|
+
test('should timeout when request exceeds configured timeout', async () => {
|
|
204
|
+
const axiosHttpClient = new AxiosHttpClient_1.AxiosHttpClient(new NodeFsFileStorage_1.NodeFsFileStorage(), {
|
|
205
|
+
rejectUnauthorized: false,
|
|
206
|
+
followRedirects: true,
|
|
207
|
+
timeout: 50, // 50ms timeout (smaller = faster test)
|
|
208
|
+
});
|
|
209
|
+
const testUrl = 'http://test-timeout.example.com';
|
|
210
|
+
// Mock a slow response with much larger delay for reliability
|
|
211
|
+
(0, nock_1.default)(testUrl)
|
|
212
|
+
.get('/')
|
|
213
|
+
.delay(500) // 500ms delay >> 50ms timeout (10x margin)
|
|
214
|
+
.reply(200, { success: true });
|
|
215
|
+
const response = await axiosHttpClient.execute(Http_1.HttpMethod.Get, testUrl, Http_1.HttpRequest.create({
|
|
216
|
+
headers: {},
|
|
217
|
+
pathParams: {},
|
|
218
|
+
queryString: {},
|
|
219
|
+
body: BufferExtensions_1.BufferExtensions.arrayBufferToReadable(new ArrayBuffer(0)),
|
|
220
|
+
}))();
|
|
221
|
+
// Should get error response due to timeout
|
|
222
|
+
expect(E.isRight(response)).toBe(true);
|
|
223
|
+
if (E.isRight(response)) {
|
|
224
|
+
// AxiosHttpClient converts timeout errors to 500 responses
|
|
225
|
+
expect(response.right.statusCode).toBe(500);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
test('should use default timeout of 0 (no timeout) when not specified', async () => {
|
|
229
|
+
const axiosHttpClient = new AxiosHttpClient_1.AxiosHttpClient(new NodeFsFileStorage_1.NodeFsFileStorage(), {
|
|
230
|
+
rejectUnauthorized: false,
|
|
231
|
+
followRedirects: true,
|
|
232
|
+
// timeout not specified, should default to 0 (matching axios default)
|
|
233
|
+
});
|
|
234
|
+
// Verify the resolved timeout is 0 (no timeout, matching axios default)
|
|
235
|
+
// Using bracket notation to access private field in tests (TypeScript allows this for dynamic access)
|
|
236
|
+
const resolvedTimeout = axiosHttpClient['resolvedOptions'].timeout;
|
|
237
|
+
expect(resolvedTimeout).toBe(0);
|
|
238
|
+
const testUrl = 'http://test-default-timeout.example.com';
|
|
239
|
+
// Also verify it works functionally - request succeeds (no timeout)
|
|
240
|
+
(0, nock_1.default)(testUrl)
|
|
241
|
+
.get('/')
|
|
242
|
+
.delay(50) // Fast response
|
|
243
|
+
.reply(200, { success: true });
|
|
244
|
+
const response = await axiosHttpClient.execute(Http_1.HttpMethod.Get, testUrl, Http_1.HttpRequest.create({
|
|
245
|
+
headers: {},
|
|
246
|
+
pathParams: {},
|
|
247
|
+
queryString: {},
|
|
248
|
+
body: BufferExtensions_1.BufferExtensions.arrayBufferToReadable(new ArrayBuffer(0)),
|
|
249
|
+
}))();
|
|
250
|
+
expect(E.isRight(response)).toBe(true);
|
|
251
|
+
if (E.isRight(response)) {
|
|
252
|
+
expect(response.right.statusCode).toBe(200);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
test('should allow custom timeout values', async () => {
|
|
256
|
+
const axiosHttpClient = new AxiosHttpClient_1.AxiosHttpClient(new NodeFsFileStorage_1.NodeFsFileStorage(), {
|
|
257
|
+
rejectUnauthorized: false,
|
|
258
|
+
followRedirects: true,
|
|
259
|
+
timeout: 5000, // 5 second custom timeout
|
|
260
|
+
});
|
|
261
|
+
const testUrl = 'http://test-custom-timeout.example.com';
|
|
262
|
+
(0, nock_1.default)(testUrl).get('/').reply(200, { success: true });
|
|
263
|
+
const response = await axiosHttpClient.execute(Http_1.HttpMethod.Get, testUrl, Http_1.HttpRequest.create({
|
|
264
|
+
headers: {},
|
|
265
|
+
pathParams: {},
|
|
266
|
+
queryString: {},
|
|
267
|
+
body: BufferExtensions_1.BufferExtensions.arrayBufferToReadable(new ArrayBuffer(0)),
|
|
268
|
+
}))();
|
|
269
|
+
expect(E.isRight(response)).toBe(true);
|
|
270
|
+
if (E.isRight(response)) {
|
|
271
|
+
expect(response.right.statusCode).toBe(200);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
});
|
|
199
275
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trayio/axios",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.16.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.16.0",
|
|
18
18
|
"axios": "1.13.5",
|
|
19
19
|
"axios-retry": "4.5.0",
|
|
20
20
|
"form-data": "4.0.4"
|