n8n-nodes-better-http-request 0.1.0 → 0.2.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,132 @@ 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);
586
- }
587
- const sanitizedRequestOptions = (0, helpers_1.sanitizeUiMessage)(options, authKeys, secrets);
588
- sanitizedRequests.push(sanitizedRequestOptions);
589
- this.sendMessageToUI(sanitizedRequestOptions);
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
+ return;
469
+ try {
470
+ const requestData = requests[itemIndex];
471
+ if (!requestData)
472
+ return;
473
+ const { options, authKeys, credentialType } = requestData;
474
+ let secrets = [];
475
+ if (credentialType) {
476
+ const properties = this.getCredentialsProperties(credentialType);
477
+ const credentials = await this.getCredentials(credentialType, itemIndex);
478
+ secrets = (0, helpers_1.getSecrets)(properties, credentials);
479
+ }
480
+ const sanitizedRequestOptions = (0, helpers_1.sanitizeUiMessage)(options, authKeys, secrets);
481
+ sanitizedRequests[itemIndex] = sanitizedRequestOptions;
482
+ this.sendMessageToUI(sanitizedRequestOptions);
483
+ }
484
+ catch { }
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
+ continue;
497
+ }
498
+ while (inFlightTasks.size >= constants_1.MAX_CONCURRENT_REQUESTS) {
499
+ await Promise.race(inFlightTasks);
590
500
  }
591
- catch { }
592
- })));
501
+ let task;
502
+ task = executeRequestWithTracking(itemIndex, executor).finally(() => {
503
+ inFlightTasks.delete(task);
504
+ });
505
+ inFlightTasks.add(task);
506
+ }
507
+ if (inFlightTasks.size > 0) {
508
+ await Promise.all(inFlightTasks);
509
+ }
593
510
  let responseData;
594
511
  for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
595
512
  try {
596
- responseData = promisesResponses.shift();
513
+ responseData = promisesResponses[itemIndex];
597
514
  if (errorItems[itemIndex]) {
598
515
  returnItems.push({
599
516
  json: { error: errorItems[itemIndex] },
@@ -603,8 +520,7 @@ class BetterHttpRequest {
603
520
  }
604
521
  if (responseData.status !== 'fulfilled') {
605
522
  if (responseData.reason.statusCode === 429) {
606
- responseData.reason.message =
607
- "Try spacing your requests out using the batching settings under 'Options'";
523
+ responseData.reason.message = constants_1.UI_MESSAGES.RATE_LIMITED_HINT;
608
524
  }
609
525
  if (!this.continueOnFail()) {
610
526
  if (autoDetectResponseFormat &&
@@ -639,7 +555,7 @@ class BetterHttpRequest {
639
555
  ...(reason.code !== undefined ? { code: reason.code } : {}),
640
556
  ...(reason.description !== undefined ? { description: reason.description } : {}),
641
557
  ...(reason.headers ? { headers: reason.headers } : {}),
642
- ...(((_s = reason.response) === null || _s === void 0 ? void 0 : _s.headers) ? { response: { headers: reason.response.headers } } : {}),
558
+ ...(((_o = reason.response) === null || _o === void 0 ? void 0 : _o.headers) ? { response: { headers: reason.response.headers } } : {}),
643
559
  };
644
560
  }
645
561
  else if (typeof reason === 'object' && reason !== null) {
@@ -661,17 +577,13 @@ class BetterHttpRequest {
661
577
  continue;
662
578
  }
663
579
  }
664
- let responses;
665
- if (Array.isArray(responseData.value)) {
666
- responses = responseData.value;
667
- }
668
- else {
669
- responses = [responseData.value];
670
- }
580
+ const responses = Array.isArray(responseData.value)
581
+ ? responseData.value
582
+ : [responseData.value];
671
583
  let responseFormat = this.getNodeParameter('options.response.response.responseFormat', 0, 'autodetect');
672
584
  fullResponse = this.getNodeParameter('options.response.response.fullResponse', 0, false);
673
585
  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')
586
+ if (((_p = response === null || response === void 0 ? void 0 : response.request) === null || _p === void 0 ? void 0 : _p.constructor.name) === 'ClientRequest')
675
587
  delete response.request;
676
588
  if (this.getMode() === 'manual' && index === '0') {
677
589
  const nodeContext = this.getContext('node');
@@ -682,7 +594,7 @@ class BetterHttpRequest {
682
594
  nodeContext.response = responseData.value;
683
595
  }
684
596
  }
685
- const responseContentType = (_v = (_u = response.headers) === null || _u === void 0 ? void 0 : _u['content-type']) !== null && _v !== void 0 ? _v : '';
597
+ const responseContentType = (_r = (_q = response.headers) === null || _q === void 0 ? void 0 : _q['content-type']) !== null && _r !== void 0 ? _r : '';
686
598
  if (autoDetectResponseFormat) {
687
599
  if (responseContentType.includes('application/json')) {
688
600
  responseFormat = 'json';
@@ -758,7 +670,7 @@ class BetterHttpRequest {
758
670
  const returnItem = {};
759
671
  for (const property of fullResponseProperties) {
760
672
  if (property === 'body') {
761
- returnItem[outputPropertyName] = toText(response[property]);
673
+ returnItem[outputPropertyName] = (0, RequestUtils_1.toText)(response[property]);
762
674
  continue;
763
675
  }
764
676
  returnItem[property] = response[property];
@@ -771,7 +683,7 @@ class BetterHttpRequest {
771
683
  else {
772
684
  returnItems.push({
773
685
  json: {
774
- [outputPropertyName]: toText(response),
686
+ [outputPropertyName]: (0, RequestUtils_1.toText)(response),
775
687
  },
776
688
  pairedItem: { item: itemIndex },
777
689
  });
@@ -829,7 +741,13 @@ class BetterHttpRequest {
829
741
  if (!this.continueOnFail())
830
742
  throw error;
831
743
  returnItems.push({
832
- json: { error: error.message },
744
+ json: {
745
+ error: {
746
+ message: error.message,
747
+ code: error.code,
748
+ statusCode: error.statusCode,
749
+ },
750
+ },
833
751
  pairedItem: { item: itemIndex },
834
752
  });
835
753
  continue;
@@ -837,13 +755,19 @@ class BetterHttpRequest {
837
755
  }
838
756
  const retryOnFail = this.getNodeParameter('options.retryOnFail', 0, false);
839
757
  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');
758
+ const getOriginalItemIndex = (item, fallback) => item.pairedItem &&
759
+ typeof item.pairedItem === 'object' &&
760
+ !Array.isArray(item.pairedItem)
761
+ ? item.pairedItem.item
762
+ : fallback;
763
+ const maxRetries = this.getNodeParameter('options.maxRetries', 0, constants_1.DEFAULT_MAX_RETRIES);
764
+ const retryDelay = this.getNodeParameter('options.retryDelay', 0, constants_1.DEFAULT_RETRY_DELAY);
765
+ const retryOnStatusCodesStr = this.getNodeParameter('options.retryOnStatusCodes', 0, constants_1.DEFAULT_RETRY_ON_STATUS_CODES);
843
766
  const retryOnStatusCodes = new Set(retryOnStatusCodesStr
844
767
  .split(',')
845
768
  .map((s) => parseInt(s.trim(), 10))
846
769
  .filter((n) => !isNaN(n)));
770
+ const retryOnErrorCodes = new Set(constants_1.RETRYABLE_CONNECTION_ERRORS);
847
771
  for (let attempt = 0; attempt < maxRetries; attempt++) {
848
772
  const failedIndices = [];
849
773
  for (let i = 0; i < returnItems.length; i++) {
@@ -851,25 +775,30 @@ class BetterHttpRequest {
851
775
  if (item.json && item.json.error) {
852
776
  const errObj = item.json.error;
853
777
  let statusCode;
778
+ let errorCode;
854
779
  if (typeof errObj === 'object' && errObj !== null) {
855
780
  statusCode =
856
- (_x = (_w = errObj.statusCode) !== null && _w !== void 0 ? _w : errObj.httpCode) !== null && _x !== void 0 ? _x : errObj.code;
781
+ (_s = errObj.statusCode) !== null && _s !== void 0 ? _s : errObj.httpCode;
782
+ errorCode = errObj.code;
857
783
  }
858
- if (statusCode !== undefined &&
859
- retryOnStatusCodes.has(statusCode)) {
784
+ if ((statusCode !== undefined &&
785
+ retryOnStatusCodes.has(statusCode)) ||
786
+ (errorCode !== undefined &&
787
+ retryOnErrorCodes.has(errorCode))) {
860
788
  failedIndices.push(i);
861
789
  }
862
790
  }
863
791
  }
864
792
  if (failedIndices.length === 0)
865
793
  break;
794
+ this.logger.info(`Retrying ${failedIndices.length} failed items, attempt ${attempt + 1} of ${maxRetries}`);
866
795
  let effectiveDelay = retryDelay;
867
796
  for (const idx of failedIndices) {
868
797
  const errObj = returnItems[idx].json.error;
869
798
  if (typeof errObj === 'object' && errObj !== null) {
870
799
  const sc = errObj.statusCode;
871
800
  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'];
801
+ 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
802
  if (retryAfterHeader) {
874
803
  const retryAfterSeconds = parseInt(retryAfterHeader, 10);
875
804
  if (!isNaN(retryAfterSeconds)) {
@@ -884,12 +813,7 @@ class BetterHttpRequest {
884
813
  }
885
814
  const retryPromises = [];
886
815
  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;
816
+ const originalItemIndex = getOriginalItemIndex(returnItems[idx], idx);
893
817
  if (requests[originalItemIndex]) {
894
818
  const { options } = requests[originalItemIndex];
895
819
  const retryRequest = this.helpers
@@ -905,12 +829,7 @@ class BetterHttpRequest {
905
829
  for (let ri = 0; ri < retryResults.length; ri++) {
906
830
  const result = retryResults[ri];
907
831
  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;
832
+ const originalItemIndex = getOriginalItemIndex(returnItems[idx], idx);
914
833
  if (result.status === 'fulfilled' && result.value != null) {
915
834
  const response = result.value;
916
835
  if (typeof response === 'object' &&
@@ -919,7 +838,7 @@ class BetterHttpRequest {
919
838
  let responseFormat = this.getNodeParameter('options.response.response.responseFormat', 0, 'autodetect');
920
839
  const currentFullResponse = this.getNodeParameter('options.response.response.fullResponse', 0, false);
921
840
  if (responseFormat === 'autodetect') {
922
- const ct = (_3 = (_2 = response.headers) === null || _2 === void 0 ? void 0 : _2['content-type']) !== null && _3 !== void 0 ? _3 : '';
841
+ const ct = (_y = (_x = response.headers) === null || _x === void 0 ? void 0 : _x['content-type']) !== null && _y !== void 0 ? _y : '';
923
842
  if (ct.includes('application/json')) {
924
843
  responseFormat = 'json';
925
844
  }
@@ -963,12 +882,22 @@ class BetterHttpRequest {
963
882
  }
964
883
  if (typeof bodyData === 'object' &&
965
884
  bodyData !== null) {
966
- returnItems[idx] = {
967
- json: bodyData,
968
- pairedItem: {
969
- item: originalItemIndex,
970
- },
971
- };
885
+ if (Object.keys(bodyData).length === 0) {
886
+ returnItems[idx] = {
887
+ json: { item: originalItemIndex },
888
+ pairedItem: {
889
+ item: originalItemIndex,
890
+ },
891
+ };
892
+ }
893
+ else {
894
+ returnItems[idx] = {
895
+ json: bodyData,
896
+ pairedItem: {
897
+ item: originalItemIndex,
898
+ },
899
+ };
900
+ }
972
901
  }
973
902
  else {
974
903
  returnItems[idx] = {
@@ -982,10 +911,18 @@ class BetterHttpRequest {
982
911
  }
983
912
  else if (typeof response === 'object' &&
984
913
  response !== null) {
985
- returnItems[idx] = {
986
- json: response,
987
- pairedItem: { item: originalItemIndex },
988
- };
914
+ if (Object.keys(response).length === 0) {
915
+ returnItems[idx] = {
916
+ json: { item: originalItemIndex },
917
+ pairedItem: { item: originalItemIndex },
918
+ };
919
+ }
920
+ else {
921
+ returnItems[idx] = {
922
+ json: response,
923
+ pairedItem: { item: originalItemIndex },
924
+ };
925
+ }
989
926
  }
990
927
  else {
991
928
  try {
@@ -1011,10 +948,11 @@ class BetterHttpRequest {
1011
948
  }
1012
949
  }
1013
950
  returnItems = returnItems.map(helpers_1.replaceNullValues);
951
+ this.logger.debug('Better HTTP Request node execution finished', { returnItemsLength: returnItems.length });
1014
952
  if (returnItems.length === 1 &&
1015
953
  returnItems[0].json.data &&
1016
954
  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";
955
+ const message = constants_1.UI_MESSAGES.SPLIT_OUT_HINT;
1018
956
  if (this.addExecutionHints) {
1019
957
  this.addExecutionHints({
1020
958
  message,