@trayio/axios 5.14.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: 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;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
- 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: 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: 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.14.0",
3
+ "version": "5.16.0",
4
4
  "description": "Axios extensions and implementations",
5
5
  "exports": {
6
6
  "./*": "./dist/*.js"
@@ -14,8 +14,8 @@
14
14
  "access": "public"
15
15
  },
16
16
  "dependencies": {
17
- "@trayio/commons": "5.14.0",
18
- "axios": "1.12.0",
17
+ "@trayio/commons": "5.16.0",
18
+ "axios": "1.13.5",
19
19
  "axios-retry": "4.5.0",
20
20
  "form-data": "4.0.4"
21
21
  },