@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: boolean;
8
- followRedirects: boolean;
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;IAC5B,eAAe,EAAE,OAAO,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,qBAAa,eAAgB,YAAW,UAAU;IAKhD,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAL9B,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAQ;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA8B;gBAG1C,WAAW,GAAE,WAAqC,EAClD,YAAY,GAAE,YAI9B;IAoCF,OAAO,CACN,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,WAAW,GAClB,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,YAAY,CAAC;IA6GrC,OAAO,CAAC,wBAAwB;IAmBhC,OAAO,CAAC,2BAA2B;IAUnC,YAAY,aAAc,QAAQ,cAAc,OAAO,MAAM,EAAE,MAAM,CAAC,cAKpE;CACF"}
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
- constructor(fileStorage = new NodeFsFileStorage_1.NodeFsFileStorage(), axiosOptions = {
46
- rejectUnauthorized: false,
47
- followRedirects: true,
48
- retries: 0,
49
- }) {
44
+ resolvedOptions;
45
+ constructor(fileStorage = new NodeFsFileStorage_1.NodeFsFileStorage(), axiosOptions = {}) {
50
46
  this.fileStorage = fileStorage;
51
- this.axiosOptions = axiosOptions;
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.axiosOptions.keepAlive || true,
64
+ keepAlive: this.resolvedOptions.keepAlive,
56
65
  keepAliveMsecs: 42000,
57
- rejectUnauthorized: this.axiosOptions.rejectUnauthorized,
58
- maxSockets: this.axiosOptions.maxSockets,
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.axiosOptions.keepAlive || true,
106
+ keepAlive: this.resolvedOptions.keepAlive,
100
107
  keepAliveMsecs: 42000,
101
- rejectUnauthorized: this.axiosOptions.rejectUnauthorized,
102
- maxSockets: this.axiosOptions.maxSockets,
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.axiosOptions.followRedirects ? {} : { maxRedirects: 0 }),
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.15.0",
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.15.0",
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"