n8n-nodes-better-http-request 0.1.0 → 0.3.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.
@@ -8,38 +8,10 @@ const set_1 = __importDefault(require("lodash/set"));
8
8
  const n8n_workflow_1 = require("n8n-workflow");
9
9
  const Description_1 = require("./Description");
10
10
  const helpers_1 = require("./helpers");
11
- function toText(data) {
12
- if (typeof data === 'object' && data !== null) {
13
- return JSON.stringify(data);
14
- }
15
- return data;
16
- }
17
- function parseJsonParameter(node, jsonString, fieldName, itemIndex) {
18
- try {
19
- return JSON.parse(jsonString);
20
- }
21
- catch (e) {
22
- const error = (0, n8n_workflow_1.ensureError)(e);
23
- throw new n8n_workflow_1.NodeOperationError(node, `The value in the "${fieldName}" field is not valid JSON`, {
24
- itemIndex,
25
- description: error.message,
26
- });
27
- }
28
- }
29
- function isDomainAllowedLocal(url, opts) {
30
- try {
31
- const parsedUrl = new URL(url);
32
- const hostname = parsedUrl.hostname.toLowerCase();
33
- const allowedDomains = opts.allowedDomains
34
- .split(',')
35
- .map((d) => d.trim().toLowerCase())
36
- .filter((d) => d.length > 0);
37
- return allowedDomains.some((domain) => hostname === domain || hostname.endsWith('.' + domain));
38
- }
39
- catch {
40
- return false;
41
- }
42
- }
11
+ const RequestUtils_1 = require("./RequestUtils");
12
+ const DomainValidator_1 = require("./DomainValidator");
13
+ const CredentialHandler_1 = require("./CredentialHandler");
14
+ const constants_1 = require("./constants");
43
15
  class BetterHttpRequest {
44
16
  constructor() {
45
17
  this.description = {
@@ -71,10 +43,11 @@ class BetterHttpRequest {
71
43
  };
72
44
  }
73
45
  async execute() {
74
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3;
46
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
75
47
  const items = this.getInputData();
76
48
  const nodeVersion = this.getNode().typeVersion;
77
- const fullResponseProperties = ['body', 'headers', 'statusCode', 'statusMessage'];
49
+ this.logger.debug('Starting Better HTTP Request node execution', { numberOfItems: items.length });
50
+ const fullResponseProperties = constants_1.FULL_RESPONSE_PROPERTIES;
78
51
  let authentication;
79
52
  try {
80
53
  authentication = this.getNodeParameter('authentication', 0);
@@ -96,14 +69,14 @@ class BetterHttpRequest {
96
69
  };
97
70
  let returnItems = [];
98
71
  const errorItems = {};
99
- const requestPromises = [];
72
+ const requestExecutors = new Array(items.length);
100
73
  let fullResponse = false;
101
74
  let autoDetectResponseFormat = false;
102
75
  let responseFileName;
103
76
  const pagination = this.getNodeParameter('options.pagination.pagination', 0, null, {
104
77
  rawExpressions: true,
105
78
  });
106
- const requests = [];
79
+ const requests = new Array(items.length);
107
80
  const updadeQueryParameter = (0, helpers_1.updadeQueryParameterConfig)(nodeVersion);
108
81
  for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
109
82
  try {
@@ -138,58 +111,7 @@ class BetterHttpRequest {
138
111
  nodeCredentialType = this.getNodeParameter('nodeCredentialType', itemIndex);
139
112
  }
140
113
  const url = this.getNodeParameter('url', itemIndex);
141
- if (typeof url !== 'string') {
142
- const actualType = url === null ? 'null' : typeof url;
143
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `URL parameter must be a string, got ${actualType}`);
144
- }
145
- if (!url.startsWith('http://') && !url.startsWith('https://')) {
146
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid URL: ${url}. URL must start with "http" or "https".`);
147
- }
148
- const checkDomainRestrictions = async (credentialData, requestUrl, credentialType) => {
149
- if (credentialData.allowedHttpRequestDomains === 'domains') {
150
- const allowedDomains = credentialData.allowedDomains;
151
- if (!allowedDomains || allowedDomains.trim() === '') {
152
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No allowed domains specified. Configure allowed domains or change restriction setting.');
153
- }
154
- if (!isDomainAllowedLocal(requestUrl, { allowedDomains })) {
155
- const credentialInfo = credentialType ? ` (${credentialType})` : '';
156
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Domain not allowed: This credential${credentialInfo} is restricted from accessing ${requestUrl}. ` +
157
- `Only the following domains are allowed: ${allowedDomains}`);
158
- }
159
- }
160
- else if (credentialData.allowedHttpRequestDomains === 'none') {
161
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'This credential is configured to prevent use within an HTTP Request node');
162
- }
163
- };
164
- if (httpBasicAuth)
165
- await checkDomainRestrictions(httpBasicAuth, url);
166
- if (httpBearerAuth)
167
- await checkDomainRestrictions(httpBearerAuth, url);
168
- if (httpDigestAuth)
169
- await checkDomainRestrictions(httpDigestAuth, url);
170
- if (httpHeaderAuth)
171
- await checkDomainRestrictions(httpHeaderAuth, url);
172
- if (httpQueryAuth)
173
- await checkDomainRestrictions(httpQueryAuth, url);
174
- if (httpCustomAuth)
175
- await checkDomainRestrictions(httpCustomAuth, url);
176
- if (oAuth1Api)
177
- await checkDomainRestrictions(oAuth1Api, url);
178
- if (oAuth2Api)
179
- await checkDomainRestrictions(oAuth2Api, url);
180
- if (nodeCredentialType) {
181
- try {
182
- const credentialData = await this.getCredentials(nodeCredentialType, itemIndex);
183
- await checkDomainRestrictions(credentialData, url, nodeCredentialType);
184
- }
185
- catch (error) {
186
- if (((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('Domain not allowed')) ||
187
- ((_b = error.message) === null || _b === void 0 ? void 0 : _b.includes('configured to prevent')) ||
188
- ((_c = error.message) === null || _c === void 0 ? void 0 : _c.includes('No allowed domains specified'))) {
189
- throw error;
190
- }
191
- }
192
- }
114
+ (0, DomainValidator_1.validateUrl)(this.getNode(), url, itemIndex);
193
115
  const provideSslCertificates = this.getNodeParameter('provideSslCertificates', itemIndex, false);
194
116
  if (provideSslCertificates) {
195
117
  sslCertificates = (await this.getCredentials('httpSslAuth', itemIndex));
@@ -210,12 +132,12 @@ class BetterHttpRequest {
210
132
  const specifyHeaders = this.getNodeParameter('specifyHeaders', itemIndex, 'keypair');
211
133
  const jsonHeadersParameter = this.getNodeParameter('jsonHeaders', itemIndex, '');
212
134
  const { redirect, batching, proxy, timeout, allowUnauthorizedCerts, queryParameterArrays, response, lowercaseHeaders, sendCredentialsOnCrossOriginRedirect, } = this.getNodeParameter('options', itemIndex, {});
213
- responseFileName = (_d = response === null || response === void 0 ? void 0 : response.response) === null || _d === void 0 ? void 0 : _d.outputPropertyName;
214
- const responseFormat = ((_e = response === null || response === void 0 ? void 0 : response.response) === null || _e === void 0 ? void 0 : _e.responseFormat) || 'autodetect';
215
- fullResponse = ((_f = response === null || response === void 0 ? void 0 : response.response) === null || _f === void 0 ? void 0 : _f.fullResponse) || false;
135
+ responseFileName = (_a = response === null || response === void 0 ? void 0 : response.response) === null || _a === void 0 ? void 0 : _a.outputPropertyName;
136
+ const responseFormat = ((_b = response === null || response === void 0 ? void 0 : response.response) === null || _b === void 0 ? void 0 : _b.responseFormat) || 'autodetect';
137
+ fullResponse = ((_c = response === null || response === void 0 ? void 0 : response.response) === null || _c === void 0 ? void 0 : _c.fullResponse) || false;
216
138
  autoDetectResponseFormat = responseFormat === 'autodetect';
217
- const batchSize = ((_g = batching === null || batching === void 0 ? void 0 : batching.batch) === null || _g === void 0 ? void 0 : _g.batchSize) > 0 ? (_h = batching === null || batching === void 0 ? void 0 : batching.batch) === null || _h === void 0 ? void 0 : _h.batchSize : 1;
218
- const batchInterval = (_j = batching === null || batching === void 0 ? void 0 : batching.batch) === null || _j === void 0 ? void 0 : _j.batchInterval;
139
+ const batchSize = ((_d = batching === null || batching === void 0 ? void 0 : batching.batch) === null || _d === void 0 ? void 0 : _d.batchSize) > 0 ? (_e = batching === null || batching === void 0 ? void 0 : batching.batch) === null || _e === void 0 ? void 0 : _e.batchSize : 1;
140
+ const batchInterval = (_f = batching === null || batching === void 0 ? void 0 : batching.batch) === null || _f === void 0 ? void 0 : _f.batchInterval;
219
141
  if (itemIndex > 0 && batchSize >= 0 && batchInterval > 0) {
220
142
  if (itemIndex % batchSize === 0) {
221
143
  await (0, n8n_workflow_1.sleep)(batchInterval);
@@ -235,25 +157,20 @@ class BetterHttpRequest {
235
157
  requestOptions = { ...requestOptions, followAllRedirects: false };
236
158
  }
237
159
  const defaultRedirect = redirect === undefined;
238
- if (((_k = redirect === null || redirect === void 0 ? void 0 : redirect.redirect) === null || _k === void 0 ? void 0 : _k.followRedirects) || defaultRedirect) {
160
+ if (((_g = redirect === null || redirect === void 0 ? void 0 : redirect.redirect) === null || _g === void 0 ? void 0 : _g.followRedirects) || defaultRedirect) {
239
161
  requestOptions.followRedirect = true;
240
162
  requestOptions.followAllRedirects = true;
241
163
  }
242
- if (((_l = redirect === null || redirect === void 0 ? void 0 : redirect.redirect) === null || _l === void 0 ? void 0 : _l.maxRedirects) || defaultRedirect) {
243
- requestOptions.maxRedirects = (_m = redirect === null || redirect === void 0 ? void 0 : redirect.redirect) === null || _m === void 0 ? void 0 : _m.maxRedirects;
164
+ if (((_h = redirect === null || redirect === void 0 ? void 0 : redirect.redirect) === null || _h === void 0 ? void 0 : _h.maxRedirects) || defaultRedirect) {
165
+ requestOptions.maxRedirects = (_j = redirect === null || redirect === void 0 ? void 0 : redirect.redirect) === null || _j === void 0 ? void 0 : _j.maxRedirects;
244
166
  }
245
- if ((_o = response === null || response === void 0 ? void 0 : response.response) === null || _o === void 0 ? void 0 : _o.neverError) {
167
+ if ((_k = response === null || response === void 0 ? void 0 : response.response) === null || _k === void 0 ? void 0 : _k.neverError) {
246
168
  requestOptions.simple = false;
247
169
  }
248
170
  if (proxy) {
249
171
  requestOptions.proxy = proxy;
250
172
  }
251
- if (timeout) {
252
- requestOptions.timeout = timeout;
253
- }
254
- else {
255
- requestOptions.timeout = 300000;
256
- }
173
+ requestOptions.timeout = timeout || constants_1.DEFAULT_TIMEOUT_MS;
257
174
  if (sendQuery && queryParameterArrays) {
258
175
  Object.assign(requestOptions, {
259
176
  qsStringifyOptions: { arrayFormat: queryParameterArrays },
@@ -290,7 +207,7 @@ class BetterHttpRequest {
290
207
  else if (specifyBody === 'json') {
291
208
  if (typeof jsonBodyParameter !== 'object' &&
292
209
  jsonBodyParameter !== null) {
293
- requestOptions.body = parseJsonParameter(this.getNode(), jsonBodyParameter, 'JSON Body', itemIndex);
210
+ requestOptions.body = (0, RequestUtils_1.parseJsonParameter)(this.getNode(), jsonBodyParameter, 'JSON Body', itemIndex);
294
211
  }
295
212
  else {
296
213
  requestOptions.body = jsonBodyParameter;
@@ -327,7 +244,7 @@ class BetterHttpRequest {
327
244
  requestOptions.headers = {
328
245
  ...requestOptions.headers,
329
246
  'content-length': contentLength,
330
- 'content-type': (_p = itemBinaryData.mimeType) !== null && _p !== void 0 ? _p : 'application/octet-stream',
247
+ 'content-type': (_l = itemBinaryData.mimeType) !== null && _l !== void 0 ? _l : 'application/octet-stream',
331
248
  };
332
249
  }
333
250
  else if (bodyContentType === 'raw') {
@@ -339,7 +256,7 @@ class BetterHttpRequest {
339
256
  requestOptions.qs = await (0, helpers_1.reduceAsync)(queryParameters, parametersToKeyValue);
340
257
  }
341
258
  else if (specifyQuery === 'json') {
342
- requestOptions.qs = parseJsonParameter(this.getNode(), jsonQueryParameter, 'JSON Query Parameters', itemIndex);
259
+ requestOptions.qs = (0, RequestUtils_1.parseJsonParameter)(this.getNode(), jsonQueryParameter, 'JSON Query Parameters', itemIndex);
343
260
  }
344
261
  }
345
262
  if (sendHeaders && headerParameters) {
@@ -348,7 +265,7 @@ class BetterHttpRequest {
348
265
  additionalHeaders = await (0, helpers_1.reduceAsync)(headerParameters.filter((header) => header.name), parametersToKeyValue);
349
266
  }
350
267
  else if (specifyHeaders === 'json') {
351
- additionalHeaders = parseJsonParameter(this.getNode(), jsonHeadersParameter, 'JSON Headers', itemIndex);
268
+ additionalHeaders = (0, RequestUtils_1.parseJsonParameter)(this.getNode(), jsonHeadersParameter, 'JSON Headers', itemIndex);
352
269
  }
353
270
  requestOptions.headers = {
354
271
  ...requestOptions.headers,
@@ -381,77 +298,28 @@ class BetterHttpRequest {
381
298
  if (requestOptions.agentOptions) {
382
299
  authDataKeys.agentOptions = Object.keys(requestOptions.agentOptions);
383
300
  }
384
- if (httpBasicAuth !== undefined) {
385
- requestOptions.auth = {
386
- user: httpBasicAuth.user,
387
- pass: httpBasicAuth.password,
388
- };
389
- authDataKeys.auth = ['pass'];
390
- }
391
- if (httpBearerAuth !== undefined) {
392
- requestOptions.headers = (_q = requestOptions.headers) !== null && _q !== void 0 ? _q : {};
393
- requestOptions.headers.Authorization = `Bearer ${String(httpBearerAuth.token)}`;
394
- authDataKeys.headers = ['Authorization'];
395
- }
396
- if (httpHeaderAuth !== undefined) {
397
- requestOptions.headers[httpHeaderAuth.name] =
398
- httpHeaderAuth.value;
399
- authDataKeys.headers = [httpHeaderAuth.name];
400
- }
401
- if (httpQueryAuth !== undefined) {
402
- if (!requestOptions.qs) {
403
- requestOptions.qs = {};
404
- }
405
- requestOptions.qs[httpQueryAuth.name] = httpQueryAuth.value;
406
- authDataKeys.qs = [httpQueryAuth.name];
407
- }
408
- if (httpDigestAuth !== undefined) {
409
- requestOptions.auth = {
410
- user: httpDigestAuth.user,
411
- pass: httpDigestAuth.password,
412
- sendImmediately: false,
413
- };
414
- authDataKeys.auth = ['pass'];
415
- }
416
- if (httpCustomAuth !== undefined) {
417
- const customAuth = (0, n8n_workflow_1.jsonParse)(httpCustomAuth.json || '{}', { errorMessage: 'Invalid Custom Auth JSON' });
418
- if (customAuth.headers) {
419
- requestOptions.headers = {
420
- ...requestOptions.headers,
421
- ...customAuth.headers,
422
- };
423
- authDataKeys.headers = Object.keys(customAuth.headers);
424
- }
425
- if (customAuth.body) {
426
- requestOptions.body = {
427
- ...requestOptions.body,
428
- ...customAuth.body,
429
- };
430
- authDataKeys.body = Object.keys(customAuth.body);
431
- }
432
- if (customAuth.qs) {
433
- requestOptions.qs = { ...requestOptions.qs, ...customAuth.qs };
434
- authDataKeys.qs = Object.keys(customAuth.qs);
435
- }
436
- }
301
+ (0, CredentialHandler_1.applyAllCredentials)(requestOptions, {
302
+ httpBasicAuth,
303
+ httpBearerAuth,
304
+ httpHeaderAuth,
305
+ httpQueryAuth,
306
+ httpDigestAuth,
307
+ httpCustomAuth,
308
+ }, authDataKeys);
437
309
  if (requestOptions.headers.accept === undefined) {
438
- if (responseFormat === 'json') {
439
- requestOptions.headers.accept = 'application/json,text/*;q=0.99';
440
- }
441
- else if (responseFormat === 'text') {
442
- requestOptions.headers.accept =
443
- 'application/json,text/html,application/xhtml+xml,application/xml,text/*;q=0.9, */*;q=0.1';
444
- }
445
- else {
446
- requestOptions.headers.accept =
447
- 'application/json,text/html,application/xhtml+xml,application/xml,text/*;q=0.9, image/*;q=0.8, */*;q=0.7';
448
- }
449
- }
450
- requests.push({
451
- options: requestOptions,
310
+ requestOptions.headers.accept =
311
+ responseFormat === 'json'
312
+ ? constants_1.ACCEPT_HEADERS.JSON
313
+ : responseFormat === 'text'
314
+ ? constants_1.ACCEPT_HEADERS.TEXT
315
+ : constants_1.ACCEPT_HEADERS.AUTO;
316
+ }
317
+ const itemRequestOptions = requestOptions;
318
+ requests[itemIndex] = {
319
+ options: itemRequestOptions,
452
320
  authKeys: authDataKeys,
453
321
  credentialType: nodeCredentialType,
454
- });
322
+ };
455
323
  if (pagination && pagination.paginationMode !== 'off') {
456
324
  let continueExpression = '={{false}}';
457
325
  if (pagination.paginationCompleteWhen === 'receiveSpecificStatusCodes') {
@@ -472,7 +340,7 @@ class BetterHttpRequest {
472
340
  const completionExpression = pagination.completeExpression
473
341
  .trim()
474
342
  .slice(3, -2);
475
- if ((_r = response === null || response === void 0 ? void 0 : response.response) === null || _r === void 0 ? void 0 : _r.neverError) {
343
+ if ((_m = response === null || response === void 0 ? void 0 : response.response) === null || _m === void 0 ? void 0 : _m.neverError) {
476
344
  continueExpression = `={{ !(${completionExpression}) }}`;
477
345
  }
478
346
  else {
@@ -517,83 +385,134 @@ class BetterHttpRequest {
517
385
  if (responseFormat === 'file') {
518
386
  paginationData.binaryResult = true;
519
387
  }
520
- const requestPromise = this.helpers.requestWithAuthenticationPaginated
521
- .call(this, requestOptions, itemIndex, paginationData, nodeCredentialType !== null && nodeCredentialType !== void 0 ? nodeCredentialType : genericCredentialType)
522
- .catch((error) => {
523
- if (error instanceof n8n_workflow_1.NodeOperationError &&
524
- error.type === 'invalid_url') {
525
- const urlParameterName = pagination.paginationMode ===
526
- 'responseContainsNextURL'
527
- ? 'Next URL'
528
- : 'URL';
529
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message, {
530
- description: `Make sure the "${urlParameterName}" parameter evaluates to a valid URL.`,
531
- });
532
- }
533
- throw error;
534
- });
535
- requestPromises.push(requestPromise);
388
+ requestExecutors[itemIndex] = async () => {
389
+ return await this.helpers.requestWithAuthenticationPaginated
390
+ .call(this, itemRequestOptions, itemIndex, paginationData, nodeCredentialType !== null && nodeCredentialType !== void 0 ? nodeCredentialType : genericCredentialType)
391
+ .catch((error) => {
392
+ if (error instanceof n8n_workflow_1.NodeOperationError &&
393
+ error.type === 'invalid_url') {
394
+ const urlParameterName = pagination.paginationMode ===
395
+ 'responseContainsNextURL'
396
+ ? 'Next URL'
397
+ : 'URL';
398
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message, {
399
+ description: `Make sure the "${urlParameterName}" parameter evaluates to a valid URL.`,
400
+ });
401
+ }
402
+ throw error;
403
+ });
404
+ };
536
405
  }
537
406
  else if (authentication === 'genericCredentialType' ||
538
407
  authentication === 'none') {
539
408
  if (oAuth1Api) {
540
- const requestOAuth1 = this.helpers.requestOAuth1.call(this, 'oAuth1Api', requestOptions);
541
- requestOAuth1.catch(() => { });
542
- requestPromises.push(requestOAuth1);
409
+ requestExecutors[itemIndex] = async () => await this.helpers.requestOAuth1.call(this, 'oAuth1Api', itemRequestOptions);
543
410
  }
544
411
  else if (oAuth2Api) {
545
- const requestOAuth2 = this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions, { tokenType: 'Bearer' });
546
- requestOAuth2.catch(() => { });
547
- requestPromises.push(requestOAuth2);
412
+ requestExecutors[itemIndex] = async () => await this.helpers.requestOAuth2.call(this, 'oAuth2Api', itemRequestOptions, { tokenType: 'Bearer' });
548
413
  }
549
414
  else {
550
- const request = this.helpers.request(requestOptions);
551
- request.catch(() => { });
552
- requestPromises.push(request);
415
+ requestExecutors[itemIndex] = async () => await this.helpers.request(itemRequestOptions);
553
416
  }
554
417
  }
555
418
  else if (authentication === 'predefinedCredentialType' &&
556
419
  nodeCredentialType) {
557
- const additionalOAuth2Options = (0, helpers_1.getOAuth2AdditionalParameters)(nodeCredentialType);
558
- const requestWithAuthentication = this.helpers.requestWithAuthentication.call(this, nodeCredentialType, requestOptions, additionalOAuth2Options && {
420
+ const credentialType = nodeCredentialType;
421
+ const additionalOAuth2Options = (0, helpers_1.getOAuth2AdditionalParameters)(credentialType);
422
+ requestExecutors[itemIndex] = async () => await this.helpers.requestWithAuthentication.call(this, credentialType, itemRequestOptions, additionalOAuth2Options && {
559
423
  oauth2: additionalOAuth2Options,
560
424
  }, itemIndex);
561
- requestWithAuthentication.catch(() => { });
562
- requestPromises.push(requestWithAuthentication);
563
425
  }
564
426
  }
565
427
  catch (error) {
566
428
  if (!this.continueOnFail())
567
429
  throw error;
568
- requestPromises.push(Promise.reject(error).catch(() => { }));
569
430
  errorItems[itemIndex] = error.message;
431
+ this.logger.warn(`Failed to process item ${itemIndex}`, { error: error.message });
570
432
  continue;
571
433
  }
572
434
  }
573
- const sanitizedRequests = [];
574
- const promisesResponses = await Promise.allSettled(requestPromises.map(async (requestPromise, itemIndex) => await requestPromise
575
- .then((response) => response)
576
- .finally(async () => {
577
- if (errorItems[itemIndex])
578
- return;
435
+ const sanitizedRequests = new Array(items.length);
436
+ const promisesResponses = new Array(items.length);
437
+ const inFlightTasks = new Set();
438
+ let completedCount = 0;
439
+ const totalCount = items.length;
440
+ const reportProgress = () => {
441
+ const percentage = Math.round((completedCount / totalCount) * 100);
442
+ this.sendMessageToUI({
443
+ type: 'progress',
444
+ message: `${percentage}% complete (${completedCount}/${totalCount} items)`,
445
+ percentage,
446
+ completed: completedCount,
447
+ total: totalCount,
448
+ });
449
+ };
450
+ const executeRequestWithTracking = async (itemIndex, executor) => {
451
+ this.logger.debug(`Executing request for item ${itemIndex}`);
579
452
  try {
580
- const { options, authKeys, credentialType } = requests[itemIndex];
581
- let secrets = [];
582
- if (credentialType) {
583
- const properties = this.getCredentialsProperties(credentialType);
584
- const credentials = await this.getCredentials(credentialType, itemIndex);
585
- secrets = (0, helpers_1.getSecrets)(properties, credentials);
453
+ const value = await executor();
454
+ promisesResponses[itemIndex] = {
455
+ status: 'fulfilled',
456
+ value,
457
+ };
458
+ }
459
+ catch (reason) {
460
+ promisesResponses[itemIndex] = {
461
+ status: 'rejected',
462
+ reason,
463
+ };
464
+ this.logger.debug(`Request failed for item ${itemIndex}`, { error: reason });
465
+ }
466
+ finally {
467
+ if (!errorItems[itemIndex]) {
468
+ try {
469
+ const requestData = requests[itemIndex];
470
+ if (requestData) {
471
+ const { options, authKeys, credentialType } = requestData;
472
+ let secrets = [];
473
+ if (credentialType) {
474
+ const properties = this.getCredentialsProperties(credentialType);
475
+ const credentials = await this.getCredentials(credentialType, itemIndex);
476
+ secrets = (0, helpers_1.getSecrets)(properties, credentials);
477
+ }
478
+ const sanitizedRequestOptions = (0, helpers_1.sanitizeUiMessage)(options, authKeys, secrets);
479
+ sanitizedRequests[itemIndex] = sanitizedRequestOptions;
480
+ this.sendMessageToUI(sanitizedRequestOptions);
481
+ }
482
+ }
483
+ catch { }
586
484
  }
587
- const sanitizedRequestOptions = (0, helpers_1.sanitizeUiMessage)(options, authKeys, secrets);
588
- sanitizedRequests.push(sanitizedRequestOptions);
589
- this.sendMessageToUI(sanitizedRequestOptions);
485
+ completedCount++;
486
+ reportProgress();
487
+ }
488
+ };
489
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
490
+ const executor = requestExecutors[itemIndex];
491
+ if (errorItems[itemIndex] || !executor) {
492
+ promisesResponses[itemIndex] = {
493
+ status: 'fulfilled',
494
+ value: undefined,
495
+ };
496
+ completedCount++;
497
+ reportProgress();
498
+ continue;
590
499
  }
591
- catch { }
592
- })));
500
+ while (inFlightTasks.size >= constants_1.MAX_CONCURRENT_REQUESTS) {
501
+ await Promise.race(inFlightTasks);
502
+ }
503
+ let task;
504
+ task = executeRequestWithTracking(itemIndex, executor).finally(() => {
505
+ inFlightTasks.delete(task);
506
+ });
507
+ inFlightTasks.add(task);
508
+ }
509
+ if (inFlightTasks.size > 0) {
510
+ await Promise.all(inFlightTasks);
511
+ }
593
512
  let responseData;
594
513
  for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
595
514
  try {
596
- responseData = promisesResponses.shift();
515
+ responseData = promisesResponses[itemIndex];
597
516
  if (errorItems[itemIndex]) {
598
517
  returnItems.push({
599
518
  json: { error: errorItems[itemIndex] },
@@ -603,8 +522,7 @@ class BetterHttpRequest {
603
522
  }
604
523
  if (responseData.status !== 'fulfilled') {
605
524
  if (responseData.reason.statusCode === 429) {
606
- responseData.reason.message =
607
- "Try spacing your requests out using the batching settings under 'Options'";
525
+ responseData.reason.message = constants_1.UI_MESSAGES.RATE_LIMITED_HINT;
608
526
  }
609
527
  if (!this.continueOnFail()) {
610
528
  if (autoDetectResponseFormat &&
@@ -639,7 +557,7 @@ class BetterHttpRequest {
639
557
  ...(reason.code !== undefined ? { code: reason.code } : {}),
640
558
  ...(reason.description !== undefined ? { description: reason.description } : {}),
641
559
  ...(reason.headers ? { headers: reason.headers } : {}),
642
- ...(((_s = reason.response) === null || _s === void 0 ? void 0 : _s.headers) ? { response: { headers: reason.response.headers } } : {}),
560
+ ...(((_o = reason.response) === null || _o === void 0 ? void 0 : _o.headers) ? { response: { headers: reason.response.headers } } : {}),
643
561
  };
644
562
  }
645
563
  else if (typeof reason === 'object' && reason !== null) {
@@ -661,17 +579,13 @@ class BetterHttpRequest {
661
579
  continue;
662
580
  }
663
581
  }
664
- let responses;
665
- if (Array.isArray(responseData.value)) {
666
- responses = responseData.value;
667
- }
668
- else {
669
- responses = [responseData.value];
670
- }
582
+ const responses = Array.isArray(responseData.value)
583
+ ? responseData.value
584
+ : [responseData.value];
671
585
  let responseFormat = this.getNodeParameter('options.response.response.responseFormat', 0, 'autodetect');
672
586
  fullResponse = this.getNodeParameter('options.response.response.fullResponse', 0, false);
673
587
  for (let [index, response] of Object.entries(responses)) {
674
- if (((_t = response === null || response === void 0 ? void 0 : response.request) === null || _t === void 0 ? void 0 : _t.constructor.name) === 'ClientRequest')
588
+ if (((_p = response === null || response === void 0 ? void 0 : response.request) === null || _p === void 0 ? void 0 : _p.constructor.name) === 'ClientRequest')
675
589
  delete response.request;
676
590
  if (this.getMode() === 'manual' && index === '0') {
677
591
  const nodeContext = this.getContext('node');
@@ -682,7 +596,7 @@ class BetterHttpRequest {
682
596
  nodeContext.response = responseData.value;
683
597
  }
684
598
  }
685
- const responseContentType = (_v = (_u = response.headers) === null || _u === void 0 ? void 0 : _u['content-type']) !== null && _v !== void 0 ? _v : '';
599
+ const responseContentType = (_r = (_q = response.headers) === null || _q === void 0 ? void 0 : _q['content-type']) !== null && _r !== void 0 ? _r : '';
686
600
  if (autoDetectResponseFormat) {
687
601
  if (responseContentType.includes('application/json')) {
688
602
  responseFormat = 'json';
@@ -758,7 +672,7 @@ class BetterHttpRequest {
758
672
  const returnItem = {};
759
673
  for (const property of fullResponseProperties) {
760
674
  if (property === 'body') {
761
- returnItem[outputPropertyName] = toText(response[property]);
675
+ returnItem[outputPropertyName] = (0, RequestUtils_1.toText)(response[property]);
762
676
  continue;
763
677
  }
764
678
  returnItem[property] = response[property];
@@ -771,7 +685,7 @@ class BetterHttpRequest {
771
685
  else {
772
686
  returnItems.push({
773
687
  json: {
774
- [outputPropertyName]: toText(response),
688
+ [outputPropertyName]: (0, RequestUtils_1.toText)(response),
775
689
  },
776
690
  pairedItem: { item: itemIndex },
777
691
  });
@@ -829,7 +743,13 @@ class BetterHttpRequest {
829
743
  if (!this.continueOnFail())
830
744
  throw error;
831
745
  returnItems.push({
832
- json: { error: error.message },
746
+ json: {
747
+ error: {
748
+ message: error.message,
749
+ code: error.code,
750
+ statusCode: error.statusCode,
751
+ },
752
+ },
833
753
  pairedItem: { item: itemIndex },
834
754
  });
835
755
  continue;
@@ -837,13 +757,19 @@ class BetterHttpRequest {
837
757
  }
838
758
  const retryOnFail = this.getNodeParameter('options.retryOnFail', 0, false);
839
759
  if (retryOnFail && this.continueOnFail()) {
840
- const maxRetries = this.getNodeParameter('options.maxRetries', 0, 3);
841
- const retryDelay = this.getNodeParameter('options.retryDelay', 0, 1000);
842
- const retryOnStatusCodesStr = this.getNodeParameter('options.retryOnStatusCodes', 0, '429,500,502,503,504');
760
+ const getOriginalItemIndex = (item, fallback) => item.pairedItem &&
761
+ typeof item.pairedItem === 'object' &&
762
+ !Array.isArray(item.pairedItem)
763
+ ? item.pairedItem.item
764
+ : fallback;
765
+ const maxRetries = this.getNodeParameter('options.maxRetries', 0, constants_1.DEFAULT_MAX_RETRIES);
766
+ const retryDelay = this.getNodeParameter('options.retryDelay', 0, constants_1.DEFAULT_RETRY_DELAY);
767
+ const retryOnStatusCodesStr = this.getNodeParameter('options.retryOnStatusCodes', 0, constants_1.DEFAULT_RETRY_ON_STATUS_CODES);
843
768
  const retryOnStatusCodes = new Set(retryOnStatusCodesStr
844
769
  .split(',')
845
770
  .map((s) => parseInt(s.trim(), 10))
846
771
  .filter((n) => !isNaN(n)));
772
+ const retryOnErrorCodes = new Set(constants_1.RETRYABLE_CONNECTION_ERRORS);
847
773
  for (let attempt = 0; attempt < maxRetries; attempt++) {
848
774
  const failedIndices = [];
849
775
  for (let i = 0; i < returnItems.length; i++) {
@@ -851,25 +777,30 @@ class BetterHttpRequest {
851
777
  if (item.json && item.json.error) {
852
778
  const errObj = item.json.error;
853
779
  let statusCode;
780
+ let errorCode;
854
781
  if (typeof errObj === 'object' && errObj !== null) {
855
782
  statusCode =
856
- (_x = (_w = errObj.statusCode) !== null && _w !== void 0 ? _w : errObj.httpCode) !== null && _x !== void 0 ? _x : errObj.code;
783
+ (_s = errObj.statusCode) !== null && _s !== void 0 ? _s : errObj.httpCode;
784
+ errorCode = errObj.code;
857
785
  }
858
- if (statusCode !== undefined &&
859
- retryOnStatusCodes.has(statusCode)) {
786
+ if ((statusCode !== undefined &&
787
+ retryOnStatusCodes.has(statusCode)) ||
788
+ (errorCode !== undefined &&
789
+ retryOnErrorCodes.has(errorCode))) {
860
790
  failedIndices.push(i);
861
791
  }
862
792
  }
863
793
  }
864
794
  if (failedIndices.length === 0)
865
795
  break;
796
+ this.logger.info(`Retrying ${failedIndices.length} failed items, attempt ${attempt + 1} of ${maxRetries}`);
866
797
  let effectiveDelay = retryDelay;
867
798
  for (const idx of failedIndices) {
868
799
  const errObj = returnItems[idx].json.error;
869
800
  if (typeof errObj === 'object' && errObj !== null) {
870
801
  const sc = errObj.statusCode;
871
802
  if (sc === 429) {
872
- const retryAfterHeader = (_z = (_y = errObj.headers) === null || _y === void 0 ? void 0 : _y['retry-after']) !== null && _z !== void 0 ? _z : (_1 = (_0 = errObj.response) === null || _0 === void 0 ? void 0 : _0.headers) === null || _1 === void 0 ? void 0 : _1['retry-after'];
803
+ const retryAfterHeader = (_u = (_t = errObj.headers) === null || _t === void 0 ? void 0 : _t['retry-after']) !== null && _u !== void 0 ? _u : (_w = (_v = errObj.response) === null || _v === void 0 ? void 0 : _v.headers) === null || _w === void 0 ? void 0 : _w['retry-after'];
873
804
  if (retryAfterHeader) {
874
805
  const retryAfterSeconds = parseInt(retryAfterHeader, 10);
875
806
  if (!isNaN(retryAfterSeconds)) {
@@ -884,12 +815,7 @@ class BetterHttpRequest {
884
815
  }
885
816
  const retryPromises = [];
886
817
  for (const idx of failedIndices) {
887
- const originalItemIndex = returnItems[idx].pairedItem &&
888
- typeof returnItems[idx].pairedItem === 'object' &&
889
- !Array.isArray(returnItems[idx].pairedItem)
890
- ? returnItems[idx].pairedItem
891
- .item
892
- : idx;
818
+ const originalItemIndex = getOriginalItemIndex(returnItems[idx], idx);
893
819
  if (requests[originalItemIndex]) {
894
820
  const { options } = requests[originalItemIndex];
895
821
  const retryRequest = this.helpers
@@ -905,12 +831,7 @@ class BetterHttpRequest {
905
831
  for (let ri = 0; ri < retryResults.length; ri++) {
906
832
  const result = retryResults[ri];
907
833
  const idx = retryPromises[ri].index;
908
- const originalItemIndex = returnItems[idx].pairedItem &&
909
- typeof returnItems[idx].pairedItem === 'object' &&
910
- !Array.isArray(returnItems[idx].pairedItem)
911
- ? returnItems[idx].pairedItem
912
- .item
913
- : idx;
834
+ const originalItemIndex = getOriginalItemIndex(returnItems[idx], idx);
914
835
  if (result.status === 'fulfilled' && result.value != null) {
915
836
  const response = result.value;
916
837
  if (typeof response === 'object' &&
@@ -919,7 +840,7 @@ class BetterHttpRequest {
919
840
  let responseFormat = this.getNodeParameter('options.response.response.responseFormat', 0, 'autodetect');
920
841
  const currentFullResponse = this.getNodeParameter('options.response.response.fullResponse', 0, false);
921
842
  if (responseFormat === 'autodetect') {
922
- const ct = (_3 = (_2 = response.headers) === null || _2 === void 0 ? void 0 : _2['content-type']) !== null && _3 !== void 0 ? _3 : '';
843
+ const ct = (_y = (_x = response.headers) === null || _x === void 0 ? void 0 : _x['content-type']) !== null && _y !== void 0 ? _y : '';
923
844
  if (ct.includes('application/json')) {
924
845
  responseFormat = 'json';
925
846
  }
@@ -963,12 +884,22 @@ class BetterHttpRequest {
963
884
  }
964
885
  if (typeof bodyData === 'object' &&
965
886
  bodyData !== null) {
966
- returnItems[idx] = {
967
- json: bodyData,
968
- pairedItem: {
969
- item: originalItemIndex,
970
- },
971
- };
887
+ if (Object.keys(bodyData).length === 0) {
888
+ returnItems[idx] = {
889
+ json: { item: originalItemIndex },
890
+ pairedItem: {
891
+ item: originalItemIndex,
892
+ },
893
+ };
894
+ }
895
+ else {
896
+ returnItems[idx] = {
897
+ json: bodyData,
898
+ pairedItem: {
899
+ item: originalItemIndex,
900
+ },
901
+ };
902
+ }
972
903
  }
973
904
  else {
974
905
  returnItems[idx] = {
@@ -982,10 +913,18 @@ class BetterHttpRequest {
982
913
  }
983
914
  else if (typeof response === 'object' &&
984
915
  response !== null) {
985
- returnItems[idx] = {
986
- json: response,
987
- pairedItem: { item: originalItemIndex },
988
- };
916
+ if (Object.keys(response).length === 0) {
917
+ returnItems[idx] = {
918
+ json: { item: originalItemIndex },
919
+ pairedItem: { item: originalItemIndex },
920
+ };
921
+ }
922
+ else {
923
+ returnItems[idx] = {
924
+ json: response,
925
+ pairedItem: { item: originalItemIndex },
926
+ };
927
+ }
989
928
  }
990
929
  else {
991
930
  try {
@@ -1011,10 +950,11 @@ class BetterHttpRequest {
1011
950
  }
1012
951
  }
1013
952
  returnItems = returnItems.map(helpers_1.replaceNullValues);
953
+ this.logger.debug('Better HTTP Request node execution finished', { returnItemsLength: returnItems.length });
1014
954
  if (returnItems.length === 1 &&
1015
955
  returnItems[0].json.data &&
1016
956
  Array.isArray(returnItems[0].json.data)) {
1017
- const message = "To split the contents of 'data' into separate items for easier processing, add a 'Split Out' node after this one";
957
+ const message = constants_1.UI_MESSAGES.SPLIT_OUT_HINT;
1018
958
  if (this.addExecutionHints) {
1019
959
  this.addExecutionHints({
1020
960
  message,