@unito/integration-sdk 2.3.14 → 2.3.15
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.
package/dist/src/index.cjs
CHANGED
|
@@ -1369,6 +1369,21 @@ class Provider {
|
|
|
1369
1369
|
request.on('error', error => {
|
|
1370
1370
|
reject(this.handleError(400, `Error while calling the provider: "${error}"`, options));
|
|
1371
1371
|
});
|
|
1372
|
+
if (options.signal) {
|
|
1373
|
+
const abortHandler = () => {
|
|
1374
|
+
request.destroy();
|
|
1375
|
+
reject(this.handleError(408, 'Timeout', options));
|
|
1376
|
+
};
|
|
1377
|
+
if (options.signal.aborted) {
|
|
1378
|
+
abortHandler();
|
|
1379
|
+
}
|
|
1380
|
+
options.signal.addEventListener('abort', abortHandler);
|
|
1381
|
+
request.on('close', () => {
|
|
1382
|
+
if (options.signal) {
|
|
1383
|
+
options.signal.removeEventListener('abort', abortHandler);
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1372
1387
|
form.pipe(request);
|
|
1373
1388
|
}
|
|
1374
1389
|
catch (error) {
|
|
@@ -1423,6 +1438,7 @@ class Provider {
|
|
|
1423
1438
|
path: urlObj.pathname + urlObj.search,
|
|
1424
1439
|
method: 'POST',
|
|
1425
1440
|
headers,
|
|
1441
|
+
timeout: 0,
|
|
1426
1442
|
};
|
|
1427
1443
|
const request = https.request(requestOptions, response => {
|
|
1428
1444
|
response.setEncoding('utf8');
|
|
@@ -1462,6 +1478,21 @@ class Provider {
|
|
|
1462
1478
|
request.destroy();
|
|
1463
1479
|
safeReject(this.handleError(500, `Stream error: "${error}"`, options));
|
|
1464
1480
|
});
|
|
1481
|
+
if (options.signal) {
|
|
1482
|
+
const abortHandler = () => {
|
|
1483
|
+
request.destroy();
|
|
1484
|
+
safeReject(this.handleError(408, 'Timeout', options));
|
|
1485
|
+
};
|
|
1486
|
+
if (options.signal.aborted) {
|
|
1487
|
+
abortHandler();
|
|
1488
|
+
}
|
|
1489
|
+
options.signal.addEventListener('abort', abortHandler);
|
|
1490
|
+
request.on('close', () => {
|
|
1491
|
+
if (options.signal) {
|
|
1492
|
+
options.signal.removeEventListener('abort', abortHandler);
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1465
1496
|
// Stream the data directly without buffering
|
|
1466
1497
|
stream.pipe(request);
|
|
1467
1498
|
}
|
|
@@ -150,6 +150,21 @@ export class Provider {
|
|
|
150
150
|
request.on('error', error => {
|
|
151
151
|
reject(this.handleError(400, `Error while calling the provider: "${error}"`, options));
|
|
152
152
|
});
|
|
153
|
+
if (options.signal) {
|
|
154
|
+
const abortHandler = () => {
|
|
155
|
+
request.destroy();
|
|
156
|
+
reject(this.handleError(408, 'Timeout', options));
|
|
157
|
+
};
|
|
158
|
+
if (options.signal.aborted) {
|
|
159
|
+
abortHandler();
|
|
160
|
+
}
|
|
161
|
+
options.signal.addEventListener('abort', abortHandler);
|
|
162
|
+
request.on('close', () => {
|
|
163
|
+
if (options.signal) {
|
|
164
|
+
options.signal.removeEventListener('abort', abortHandler);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
153
168
|
form.pipe(request);
|
|
154
169
|
}
|
|
155
170
|
catch (error) {
|
|
@@ -204,6 +219,7 @@ export class Provider {
|
|
|
204
219
|
path: urlObj.pathname + urlObj.search,
|
|
205
220
|
method: 'POST',
|
|
206
221
|
headers,
|
|
222
|
+
timeout: 0,
|
|
207
223
|
};
|
|
208
224
|
const request = https.request(requestOptions, response => {
|
|
209
225
|
response.setEncoding('utf8');
|
|
@@ -243,6 +259,21 @@ export class Provider {
|
|
|
243
259
|
request.destroy();
|
|
244
260
|
safeReject(this.handleError(500, `Stream error: "${error}"`, options));
|
|
245
261
|
});
|
|
262
|
+
if (options.signal) {
|
|
263
|
+
const abortHandler = () => {
|
|
264
|
+
request.destroy();
|
|
265
|
+
safeReject(this.handleError(408, 'Timeout', options));
|
|
266
|
+
};
|
|
267
|
+
if (options.signal.aborted) {
|
|
268
|
+
abortHandler();
|
|
269
|
+
}
|
|
270
|
+
options.signal.addEventListener('abort', abortHandler);
|
|
271
|
+
request.on('close', () => {
|
|
272
|
+
if (options.signal) {
|
|
273
|
+
options.signal.removeEventListener('abort', abortHandler);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
246
277
|
// Stream the data directly without buffering
|
|
247
278
|
stream.pipe(request);
|
|
248
279
|
}
|
|
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
|
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import { Readable } from 'stream';
|
|
4
4
|
import nock from 'nock';
|
|
5
|
+
import https from 'https';
|
|
5
6
|
import { Provider } from '../../src/resources/provider.js';
|
|
6
7
|
import * as HttpErrors from '../../src/httpErrors.js';
|
|
7
8
|
import Logger from '../../src/resources/logger.js';
|
|
@@ -23,6 +24,21 @@ describe('Provider', () => {
|
|
|
23
24
|
},
|
|
24
25
|
});
|
|
25
26
|
const logger = new Logger();
|
|
27
|
+
// Helper to spy on https.request and capture options
|
|
28
|
+
const spyOnHttpsRequest = () => {
|
|
29
|
+
let capturedOptions;
|
|
30
|
+
const originalRequest = https.request;
|
|
31
|
+
https.request = function (options, callback) {
|
|
32
|
+
capturedOptions = options;
|
|
33
|
+
return originalRequest.call(https, options, callback);
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
getCapturedOptions: () => capturedOptions,
|
|
37
|
+
restore: () => {
|
|
38
|
+
https.request = originalRequest;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
};
|
|
26
42
|
it('get', async (context) => {
|
|
27
43
|
const response = new Response('{"data": "value"}', {
|
|
28
44
|
status: 200,
|
|
@@ -941,4 +957,212 @@ describe('Provider', () => {
|
|
|
941
957
|
unitoCredentialId: '123',
|
|
942
958
|
});
|
|
943
959
|
});
|
|
960
|
+
it('postStream sets timeout to 0 (no timeout)', async () => {
|
|
961
|
+
const streamProvider = new Provider({
|
|
962
|
+
prepareRequest: requestOptions => ({
|
|
963
|
+
url: `https://www.myApi.com`,
|
|
964
|
+
headers: {
|
|
965
|
+
'X-Custom-Provider-Header': 'value',
|
|
966
|
+
'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
|
|
967
|
+
},
|
|
968
|
+
}),
|
|
969
|
+
});
|
|
970
|
+
const testData = 'timeout test data';
|
|
971
|
+
const stream = Readable.from([testData]);
|
|
972
|
+
const spy = spyOnHttpsRequest();
|
|
973
|
+
const scope = nock('https://www.myApi.com').post('/upload', testData).reply(201, { success: true });
|
|
974
|
+
try {
|
|
975
|
+
await streamProvider.postStream('/upload', stream, {
|
|
976
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
977
|
+
logger,
|
|
978
|
+
});
|
|
979
|
+
assert.ok(scope.isDone());
|
|
980
|
+
assert.equal(spy.getCapturedOptions().timeout, 0, 'Timeout should be set to 0 (no timeout)');
|
|
981
|
+
}
|
|
982
|
+
finally {
|
|
983
|
+
spy.restore();
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
it('postStream handles AbortSignal for request cancellation', async () => {
|
|
987
|
+
const streamProvider = new Provider({
|
|
988
|
+
prepareRequest: requestOptions => ({
|
|
989
|
+
url: `https://www.myApi.com`,
|
|
990
|
+
headers: {
|
|
991
|
+
'X-Custom-Provider-Header': 'value',
|
|
992
|
+
'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
|
|
993
|
+
},
|
|
994
|
+
}),
|
|
995
|
+
});
|
|
996
|
+
const stream = Readable.from(['abort signal test']);
|
|
997
|
+
const abortController = new AbortController();
|
|
998
|
+
// Simulate aborting the request immediately
|
|
999
|
+
abortController.abort();
|
|
1000
|
+
let error;
|
|
1001
|
+
try {
|
|
1002
|
+
await streamProvider.postStream('/upload', stream, {
|
|
1003
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
1004
|
+
logger,
|
|
1005
|
+
signal: abortController.signal,
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
catch (e) {
|
|
1009
|
+
error = e;
|
|
1010
|
+
}
|
|
1011
|
+
assert.ok(error instanceof HttpErrors.TimeoutError);
|
|
1012
|
+
assert.equal(error.message, 'Timeout');
|
|
1013
|
+
});
|
|
1014
|
+
it('postStream handles AbortSignal timeout during request', async () => {
|
|
1015
|
+
const streamProvider = new Provider({
|
|
1016
|
+
prepareRequest: requestOptions => ({
|
|
1017
|
+
url: `https://www.myApi.com`,
|
|
1018
|
+
headers: {
|
|
1019
|
+
'X-Custom-Provider-Header': 'value',
|
|
1020
|
+
'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
|
|
1021
|
+
},
|
|
1022
|
+
}),
|
|
1023
|
+
});
|
|
1024
|
+
const stream = Readable.from(['timeout during request test']);
|
|
1025
|
+
const abortController = new AbortController();
|
|
1026
|
+
// Delay response to simulate a slow server
|
|
1027
|
+
nock('https://www.myApi.com').post('/upload').delayConnection(100).reply(201, { success: true });
|
|
1028
|
+
// Abort after 50ms
|
|
1029
|
+
setTimeout(() => abortController.abort(), 50);
|
|
1030
|
+
let error;
|
|
1031
|
+
try {
|
|
1032
|
+
await streamProvider.postStream('/upload', stream, {
|
|
1033
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
1034
|
+
logger,
|
|
1035
|
+
signal: abortController.signal,
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
catch (e) {
|
|
1039
|
+
error = e;
|
|
1040
|
+
}
|
|
1041
|
+
assert.ok(error instanceof HttpErrors.TimeoutError);
|
|
1042
|
+
assert.equal(error.message, 'Timeout');
|
|
1043
|
+
});
|
|
1044
|
+
it('postStream cleans up AbortSignal listener on success', async () => {
|
|
1045
|
+
const streamProvider = new Provider({
|
|
1046
|
+
prepareRequest: requestOptions => ({
|
|
1047
|
+
url: `https://www.myApi.com`,
|
|
1048
|
+
headers: {
|
|
1049
|
+
'X-Custom-Provider-Header': 'value',
|
|
1050
|
+
'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
|
|
1051
|
+
},
|
|
1052
|
+
}),
|
|
1053
|
+
});
|
|
1054
|
+
const testData = 'cleanup test';
|
|
1055
|
+
const stream = Readable.from([testData]);
|
|
1056
|
+
const abortController = new AbortController();
|
|
1057
|
+
const scope = nock('https://www.myApi.com').post('/upload', testData).reply(201, { success: true });
|
|
1058
|
+
const response = await streamProvider.postStream('/upload', stream, {
|
|
1059
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
1060
|
+
logger,
|
|
1061
|
+
signal: abortController.signal,
|
|
1062
|
+
});
|
|
1063
|
+
assert.ok(scope.isDone());
|
|
1064
|
+
assert.equal(response.status, 201);
|
|
1065
|
+
// Verify the listener was removed by checking that aborting after completion
|
|
1066
|
+
// doesn't cause any side effects (if listener wasn't removed, this could cause issues)
|
|
1067
|
+
assert.doesNotThrow(() => {
|
|
1068
|
+
abortController.abort();
|
|
1069
|
+
}, 'Aborting after completion should not throw or cause issues');
|
|
1070
|
+
// Verify the signal is indeed aborted
|
|
1071
|
+
assert.ok(abortController.signal.aborted, 'Signal should be aborted');
|
|
1072
|
+
});
|
|
1073
|
+
it('postForm handles AbortSignal for request cancellation', async () => {
|
|
1074
|
+
const FormData = (await import('form-data')).default;
|
|
1075
|
+
const formProvider = new Provider({
|
|
1076
|
+
prepareRequest: requestOptions => ({
|
|
1077
|
+
url: `https://www.myApi.com`,
|
|
1078
|
+
headers: {
|
|
1079
|
+
'X-Custom-Provider-Header': 'value',
|
|
1080
|
+
'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
|
|
1081
|
+
},
|
|
1082
|
+
}),
|
|
1083
|
+
});
|
|
1084
|
+
const form = new FormData();
|
|
1085
|
+
form.append('field1', 'value1');
|
|
1086
|
+
form.append('field2', 'value2');
|
|
1087
|
+
const abortController = new AbortController();
|
|
1088
|
+
// Simulate aborting the request immediately
|
|
1089
|
+
abortController.abort();
|
|
1090
|
+
let error;
|
|
1091
|
+
try {
|
|
1092
|
+
await formProvider.postForm('/upload-form', form, {
|
|
1093
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
1094
|
+
logger,
|
|
1095
|
+
signal: abortController.signal,
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
catch (e) {
|
|
1099
|
+
error = e;
|
|
1100
|
+
}
|
|
1101
|
+
assert.ok(error instanceof HttpErrors.TimeoutError);
|
|
1102
|
+
assert.equal(error.message, 'Timeout');
|
|
1103
|
+
});
|
|
1104
|
+
it('postForm handles AbortSignal timeout during request', async () => {
|
|
1105
|
+
const FormData = (await import('form-data')).default;
|
|
1106
|
+
const formProvider = new Provider({
|
|
1107
|
+
prepareRequest: requestOptions => ({
|
|
1108
|
+
url: `https://www.myApi.com`,
|
|
1109
|
+
headers: {
|
|
1110
|
+
'X-Custom-Provider-Header': 'value',
|
|
1111
|
+
'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
|
|
1112
|
+
},
|
|
1113
|
+
}),
|
|
1114
|
+
});
|
|
1115
|
+
const form = new FormData();
|
|
1116
|
+
form.append('field1', 'value1');
|
|
1117
|
+
const abortController = new AbortController();
|
|
1118
|
+
// Mock a delayed response
|
|
1119
|
+
nock('https://www.myApi.com').post('/upload-form').delayConnection(100).reply(201, { success: true, id: '12345' });
|
|
1120
|
+
// Abort after 50ms
|
|
1121
|
+
setTimeout(() => abortController.abort(), 50);
|
|
1122
|
+
let error;
|
|
1123
|
+
try {
|
|
1124
|
+
await formProvider.postForm('/upload-form', form, {
|
|
1125
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
1126
|
+
logger,
|
|
1127
|
+
signal: abortController.signal,
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
catch (e) {
|
|
1131
|
+
error = e;
|
|
1132
|
+
}
|
|
1133
|
+
assert.ok(error instanceof HttpErrors.TimeoutError);
|
|
1134
|
+
assert.equal(error.message, 'Timeout');
|
|
1135
|
+
});
|
|
1136
|
+
it('postForm successfully completes with AbortSignal provided', async () => {
|
|
1137
|
+
const FormData = (await import('form-data')).default;
|
|
1138
|
+
const formProvider = new Provider({
|
|
1139
|
+
prepareRequest: requestOptions => ({
|
|
1140
|
+
url: `https://www.myApi.com`,
|
|
1141
|
+
headers: {
|
|
1142
|
+
'X-Custom-Provider-Header': 'value',
|
|
1143
|
+
'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
|
|
1144
|
+
},
|
|
1145
|
+
}),
|
|
1146
|
+
});
|
|
1147
|
+
const form = new FormData();
|
|
1148
|
+
form.append('field1', 'value1');
|
|
1149
|
+
form.append('field2', 'value2');
|
|
1150
|
+
const abortController = new AbortController();
|
|
1151
|
+
const scope = nock('https://www.myApi.com').post('/upload-form').reply(201, { success: true, id: '12345' });
|
|
1152
|
+
const response = await formProvider.postForm('/upload-form', form, {
|
|
1153
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
1154
|
+
logger,
|
|
1155
|
+
signal: abortController.signal,
|
|
1156
|
+
});
|
|
1157
|
+
assert.ok(scope.isDone());
|
|
1158
|
+
assert.equal(response.status, 201);
|
|
1159
|
+
assert.deepEqual(response.body, { success: true, id: '12345' });
|
|
1160
|
+
// Verify the listener was removed by checking that aborting after completion
|
|
1161
|
+
// doesn't cause any side effects
|
|
1162
|
+
assert.doesNotThrow(() => {
|
|
1163
|
+
abortController.abort();
|
|
1164
|
+
}, 'Aborting after completion should not throw or cause issues');
|
|
1165
|
+
// Verify the signal is indeed aborted
|
|
1166
|
+
assert.ok(abortController.signal.aborted, 'Signal should be aborted');
|
|
1167
|
+
});
|
|
944
1168
|
});
|
package/package.json
CHANGED
|
@@ -243,6 +243,25 @@ export class Provider {
|
|
|
243
243
|
reject(this.handleError(400, `Error while calling the provider: "${error}"`, options));
|
|
244
244
|
});
|
|
245
245
|
|
|
246
|
+
if (options.signal) {
|
|
247
|
+
const abortHandler = () => {
|
|
248
|
+
request.destroy();
|
|
249
|
+
reject(this.handleError(408, 'Timeout', options));
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
if (options.signal.aborted) {
|
|
253
|
+
abortHandler();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
options.signal.addEventListener('abort', abortHandler);
|
|
257
|
+
|
|
258
|
+
request.on('close', () => {
|
|
259
|
+
if (options.signal) {
|
|
260
|
+
options.signal.removeEventListener('abort', abortHandler);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
246
265
|
form.pipe(request);
|
|
247
266
|
} catch (error) {
|
|
248
267
|
reject(this.handleError(500, `Unexpected error while calling the provider: "${error}"`, options));
|
|
@@ -303,6 +322,7 @@ export class Provider {
|
|
|
303
322
|
path: urlObj.pathname + urlObj.search,
|
|
304
323
|
method: 'POST',
|
|
305
324
|
headers,
|
|
325
|
+
timeout: 0,
|
|
306
326
|
};
|
|
307
327
|
|
|
308
328
|
const request = https.request(requestOptions, response => {
|
|
@@ -350,6 +370,25 @@ export class Provider {
|
|
|
350
370
|
safeReject(this.handleError(500, `Stream error: "${error}"`, options));
|
|
351
371
|
});
|
|
352
372
|
|
|
373
|
+
if (options.signal) {
|
|
374
|
+
const abortHandler = () => {
|
|
375
|
+
request.destroy();
|
|
376
|
+
safeReject(this.handleError(408, 'Timeout', options));
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
if (options.signal.aborted) {
|
|
380
|
+
abortHandler();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
options.signal.addEventListener('abort', abortHandler);
|
|
384
|
+
|
|
385
|
+
request.on('close', () => {
|
|
386
|
+
if (options.signal) {
|
|
387
|
+
options.signal.removeEventListener('abort', abortHandler);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
353
392
|
// Stream the data directly without buffering
|
|
354
393
|
stream.pipe(request);
|
|
355
394
|
} catch (error) {
|
|
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
|
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import { Readable } from 'stream';
|
|
4
4
|
import nock from 'nock';
|
|
5
|
+
import https from 'https';
|
|
5
6
|
|
|
6
7
|
import { Provider } from '../../src/resources/provider.js';
|
|
7
8
|
import * as HttpErrors from '../../src/httpErrors.js';
|
|
@@ -28,6 +29,24 @@ describe('Provider', () => {
|
|
|
28
29
|
|
|
29
30
|
const logger = new Logger();
|
|
30
31
|
|
|
32
|
+
// Helper to spy on https.request and capture options
|
|
33
|
+
const spyOnHttpsRequest = (): { getCapturedOptions: () => any; restore: () => void } => {
|
|
34
|
+
let capturedOptions: any;
|
|
35
|
+
const originalRequest = https.request;
|
|
36
|
+
|
|
37
|
+
(https as any).request = function (options: any, callback: any) {
|
|
38
|
+
capturedOptions = options;
|
|
39
|
+
return originalRequest.call(https, options, callback);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
getCapturedOptions: () => capturedOptions,
|
|
44
|
+
restore: () => {
|
|
45
|
+
(https as any).request = originalRequest;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
31
50
|
it('get', async context => {
|
|
32
51
|
const response = new Response('{"data": "value"}', {
|
|
33
52
|
status: 200,
|
|
@@ -1132,4 +1151,254 @@ describe('Provider', () => {
|
|
|
1132
1151
|
unitoCredentialId: '123',
|
|
1133
1152
|
});
|
|
1134
1153
|
});
|
|
1154
|
+
|
|
1155
|
+
it('postStream sets timeout to 0 (no timeout)', async () => {
|
|
1156
|
+
const streamProvider = new Provider({
|
|
1157
|
+
prepareRequest: requestOptions => ({
|
|
1158
|
+
url: `https://www.myApi.com`,
|
|
1159
|
+
headers: {
|
|
1160
|
+
'X-Custom-Provider-Header': 'value',
|
|
1161
|
+
'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
|
|
1162
|
+
},
|
|
1163
|
+
}),
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
const testData = 'timeout test data';
|
|
1167
|
+
const stream = Readable.from([testData]);
|
|
1168
|
+
|
|
1169
|
+
const spy = spyOnHttpsRequest();
|
|
1170
|
+
const scope = nock('https://www.myApi.com').post('/upload', testData).reply(201, { success: true });
|
|
1171
|
+
|
|
1172
|
+
try {
|
|
1173
|
+
await streamProvider.postStream('/upload', stream, {
|
|
1174
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
1175
|
+
logger,
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
assert.ok(scope.isDone());
|
|
1179
|
+
assert.equal(spy.getCapturedOptions().timeout, 0, 'Timeout should be set to 0 (no timeout)');
|
|
1180
|
+
} finally {
|
|
1181
|
+
spy.restore();
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
it('postStream handles AbortSignal for request cancellation', async () => {
|
|
1186
|
+
const streamProvider = new Provider({
|
|
1187
|
+
prepareRequest: requestOptions => ({
|
|
1188
|
+
url: `https://www.myApi.com`,
|
|
1189
|
+
headers: {
|
|
1190
|
+
'X-Custom-Provider-Header': 'value',
|
|
1191
|
+
'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
|
|
1192
|
+
},
|
|
1193
|
+
}),
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
const stream = Readable.from(['abort signal test']);
|
|
1197
|
+
const abortController = new AbortController();
|
|
1198
|
+
|
|
1199
|
+
// Simulate aborting the request immediately
|
|
1200
|
+
abortController.abort();
|
|
1201
|
+
|
|
1202
|
+
let error;
|
|
1203
|
+
try {
|
|
1204
|
+
await streamProvider.postStream('/upload', stream, {
|
|
1205
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
1206
|
+
logger,
|
|
1207
|
+
signal: abortController.signal,
|
|
1208
|
+
});
|
|
1209
|
+
} catch (e) {
|
|
1210
|
+
error = e;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
assert.ok(error instanceof HttpErrors.TimeoutError);
|
|
1214
|
+
assert.equal(error.message, 'Timeout');
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
it('postStream handles AbortSignal timeout during request', async () => {
|
|
1218
|
+
const streamProvider = new Provider({
|
|
1219
|
+
prepareRequest: requestOptions => ({
|
|
1220
|
+
url: `https://www.myApi.com`,
|
|
1221
|
+
headers: {
|
|
1222
|
+
'X-Custom-Provider-Header': 'value',
|
|
1223
|
+
'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
|
|
1224
|
+
},
|
|
1225
|
+
}),
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
const stream = Readable.from(['timeout during request test']);
|
|
1229
|
+
const abortController = new AbortController();
|
|
1230
|
+
|
|
1231
|
+
// Delay response to simulate a slow server
|
|
1232
|
+
nock('https://www.myApi.com').post('/upload').delayConnection(100).reply(201, { success: true });
|
|
1233
|
+
|
|
1234
|
+
// Abort after 50ms
|
|
1235
|
+
setTimeout(() => abortController.abort(), 50);
|
|
1236
|
+
|
|
1237
|
+
let error;
|
|
1238
|
+
try {
|
|
1239
|
+
await streamProvider.postStream('/upload', stream, {
|
|
1240
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
1241
|
+
logger,
|
|
1242
|
+
signal: abortController.signal,
|
|
1243
|
+
});
|
|
1244
|
+
} catch (e) {
|
|
1245
|
+
error = e;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
assert.ok(error instanceof HttpErrors.TimeoutError);
|
|
1249
|
+
assert.equal(error.message, 'Timeout');
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
it('postStream cleans up AbortSignal listener on success', async () => {
|
|
1253
|
+
const streamProvider = new Provider({
|
|
1254
|
+
prepareRequest: requestOptions => ({
|
|
1255
|
+
url: `https://www.myApi.com`,
|
|
1256
|
+
headers: {
|
|
1257
|
+
'X-Custom-Provider-Header': 'value',
|
|
1258
|
+
'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
|
|
1259
|
+
},
|
|
1260
|
+
}),
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
const testData = 'cleanup test';
|
|
1264
|
+
const stream = Readable.from([testData]);
|
|
1265
|
+
const abortController = new AbortController();
|
|
1266
|
+
|
|
1267
|
+
const scope = nock('https://www.myApi.com').post('/upload', testData).reply(201, { success: true });
|
|
1268
|
+
|
|
1269
|
+
const response = await streamProvider.postStream('/upload', stream, {
|
|
1270
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
1271
|
+
logger,
|
|
1272
|
+
signal: abortController.signal,
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
assert.ok(scope.isDone());
|
|
1276
|
+
assert.equal(response.status, 201);
|
|
1277
|
+
|
|
1278
|
+
// Verify the listener was removed by checking that aborting after completion
|
|
1279
|
+
// doesn't cause any side effects (if listener wasn't removed, this could cause issues)
|
|
1280
|
+
assert.doesNotThrow(() => {
|
|
1281
|
+
abortController.abort();
|
|
1282
|
+
}, 'Aborting after completion should not throw or cause issues');
|
|
1283
|
+
|
|
1284
|
+
// Verify the signal is indeed aborted
|
|
1285
|
+
assert.ok(abortController.signal.aborted, 'Signal should be aborted');
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
it('postForm handles AbortSignal for request cancellation', async () => {
|
|
1289
|
+
const FormData = (await import('form-data')).default;
|
|
1290
|
+
|
|
1291
|
+
const formProvider = new Provider({
|
|
1292
|
+
prepareRequest: requestOptions => ({
|
|
1293
|
+
url: `https://www.myApi.com`,
|
|
1294
|
+
headers: {
|
|
1295
|
+
'X-Custom-Provider-Header': 'value',
|
|
1296
|
+
'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
|
|
1297
|
+
},
|
|
1298
|
+
}),
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
const form = new FormData();
|
|
1302
|
+
form.append('field1', 'value1');
|
|
1303
|
+
form.append('field2', 'value2');
|
|
1304
|
+
|
|
1305
|
+
const abortController = new AbortController();
|
|
1306
|
+
|
|
1307
|
+
// Simulate aborting the request immediately
|
|
1308
|
+
abortController.abort();
|
|
1309
|
+
|
|
1310
|
+
let error;
|
|
1311
|
+
try {
|
|
1312
|
+
await formProvider.postForm('/upload-form', form, {
|
|
1313
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
1314
|
+
logger,
|
|
1315
|
+
signal: abortController.signal,
|
|
1316
|
+
});
|
|
1317
|
+
} catch (e) {
|
|
1318
|
+
error = e;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
assert.ok(error instanceof HttpErrors.TimeoutError);
|
|
1322
|
+
assert.equal(error.message, 'Timeout');
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
it('postForm handles AbortSignal timeout during request', async () => {
|
|
1326
|
+
const FormData = (await import('form-data')).default;
|
|
1327
|
+
|
|
1328
|
+
const formProvider = new Provider({
|
|
1329
|
+
prepareRequest: requestOptions => ({
|
|
1330
|
+
url: `https://www.myApi.com`,
|
|
1331
|
+
headers: {
|
|
1332
|
+
'X-Custom-Provider-Header': 'value',
|
|
1333
|
+
'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
|
|
1334
|
+
},
|
|
1335
|
+
}),
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
const form = new FormData();
|
|
1339
|
+
form.append('field1', 'value1');
|
|
1340
|
+
|
|
1341
|
+
const abortController = new AbortController();
|
|
1342
|
+
|
|
1343
|
+
// Mock a delayed response
|
|
1344
|
+
nock('https://www.myApi.com').post('/upload-form').delayConnection(100).reply(201, { success: true, id: '12345' });
|
|
1345
|
+
|
|
1346
|
+
// Abort after 50ms
|
|
1347
|
+
setTimeout(() => abortController.abort(), 50);
|
|
1348
|
+
|
|
1349
|
+
let error;
|
|
1350
|
+
try {
|
|
1351
|
+
await formProvider.postForm('/upload-form', form, {
|
|
1352
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
1353
|
+
logger,
|
|
1354
|
+
signal: abortController.signal,
|
|
1355
|
+
});
|
|
1356
|
+
} catch (e) {
|
|
1357
|
+
error = e;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
assert.ok(error instanceof HttpErrors.TimeoutError);
|
|
1361
|
+
assert.equal(error.message, 'Timeout');
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
it('postForm successfully completes with AbortSignal provided', async () => {
|
|
1365
|
+
const FormData = (await import('form-data')).default;
|
|
1366
|
+
|
|
1367
|
+
const formProvider = new Provider({
|
|
1368
|
+
prepareRequest: requestOptions => ({
|
|
1369
|
+
url: `https://www.myApi.com`,
|
|
1370
|
+
headers: {
|
|
1371
|
+
'X-Custom-Provider-Header': 'value',
|
|
1372
|
+
'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
|
|
1373
|
+
},
|
|
1374
|
+
}),
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
const form = new FormData();
|
|
1378
|
+
form.append('field1', 'value1');
|
|
1379
|
+
form.append('field2', 'value2');
|
|
1380
|
+
|
|
1381
|
+
const abortController = new AbortController();
|
|
1382
|
+
|
|
1383
|
+
const scope = nock('https://www.myApi.com').post('/upload-form').reply(201, { success: true, id: '12345' });
|
|
1384
|
+
|
|
1385
|
+
const response = await formProvider.postForm('/upload-form', form, {
|
|
1386
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
1387
|
+
logger,
|
|
1388
|
+
signal: abortController.signal,
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
assert.ok(scope.isDone());
|
|
1392
|
+
assert.equal(response.status, 201);
|
|
1393
|
+
assert.deepEqual(response.body, { success: true, id: '12345' });
|
|
1394
|
+
|
|
1395
|
+
// Verify the listener was removed by checking that aborting after completion
|
|
1396
|
+
// doesn't cause any side effects
|
|
1397
|
+
assert.doesNotThrow(() => {
|
|
1398
|
+
abortController.abort();
|
|
1399
|
+
}, 'Aborting after completion should not throw or cause issues');
|
|
1400
|
+
|
|
1401
|
+
// Verify the signal is indeed aborted
|
|
1402
|
+
assert.ok(abortController.signal.aborted, 'Signal should be aborted');
|
|
1403
|
+
});
|
|
1135
1404
|
});
|