@63klabs/cache-data 1.3.7 → 1.3.9
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/AGENTS.md +1107 -0
- package/CHANGELOG.md +121 -2
- package/CONTRIBUTING.md +4 -5
- package/README.md +50 -25
- package/eslint.config.js +53 -0
- package/package.json +26 -14
- package/src/lib/dao-cache.js +30 -14
- package/src/lib/tools/APIRequest.class.js +948 -91
- package/src/lib/tools/AWS.classes.js +58 -5
- package/src/lib/tools/CachedParametersSecrets.classes.js +1 -0
- package/src/lib/tools/ClientRequest.class.js +858 -31
- package/src/lib/tools/Connections.classes.js +40 -3
- package/src/lib/tools/Response.class.js +11 -0
- package/src/lib/tools/generic.response.html.js +33 -0
- package/src/lib/tools/generic.response.json.js +40 -1
- package/src/lib/tools/generic.response.rss.js +33 -0
- package/src/lib/tools/generic.response.text.js +34 -1
- package/src/lib/tools/generic.response.xml.js +39 -0
- package/src/lib/tools/index.js +211 -50
- package/src/lib/utils/ValidationExecutor.class.js +66 -0
- package/src/lib/utils/ValidationMatcher.class.js +405 -0
|
@@ -4,6 +4,126 @@ const https = require('https');
|
|
|
4
4
|
const {AWS, AWSXRay} = require('./AWS.classes.js');
|
|
5
5
|
const DebugAndLog = require('./DebugAndLog.class.js');
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} ResponseMetadata
|
|
9
|
+
* @description Metadata about pagination and retry operations. This object is only
|
|
10
|
+
* present in the response when pagination or retry features are used and actually occur.
|
|
11
|
+
*
|
|
12
|
+
* @property {object} [retries] - Retry metadata (present only if retries occurred)
|
|
13
|
+
* @property {boolean} retries.occurred - Whether retry attempts were made
|
|
14
|
+
* @property {number} retries.attempts - Total number of attempts made (including initial attempt)
|
|
15
|
+
* @property {number} retries.finalAttempt - Which attempt number succeeded (1-indexed)
|
|
16
|
+
*
|
|
17
|
+
* @property {object} [pagination] - Pagination metadata (present only if pagination occurred)
|
|
18
|
+
* @property {boolean} pagination.occurred - Whether pagination was performed
|
|
19
|
+
* @property {number} pagination.totalPages - Total number of pages retrieved
|
|
20
|
+
* @property {number} pagination.totalItems - Total number of items returned across all pages
|
|
21
|
+
* @property {boolean} pagination.incomplete - Whether pagination failed to complete (some pages failed)
|
|
22
|
+
* @property {string|null} pagination.error - Error message if pagination was incomplete, null otherwise
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // Response with retry metadata only
|
|
26
|
+
* {
|
|
27
|
+
* success: true,
|
|
28
|
+
* statusCode: 200,
|
|
29
|
+
* headers: {...},
|
|
30
|
+
* body: "...",
|
|
31
|
+
* message: "SUCCESS",
|
|
32
|
+
* metadata: {
|
|
33
|
+
* retries: {
|
|
34
|
+
* occurred: true,
|
|
35
|
+
* attempts: 2,
|
|
36
|
+
* finalAttempt: 2
|
|
37
|
+
* }
|
|
38
|
+
* }
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // Response with pagination metadata only
|
|
43
|
+
* {
|
|
44
|
+
* success: true,
|
|
45
|
+
* statusCode: 200,
|
|
46
|
+
* headers: {...},
|
|
47
|
+
* body: "{\"items\": [...], \"returnedItemCount\": 1000}",
|
|
48
|
+
* message: "SUCCESS",
|
|
49
|
+
* metadata: {
|
|
50
|
+
* pagination: {
|
|
51
|
+
* occurred: true,
|
|
52
|
+
* totalPages: 5,
|
|
53
|
+
* totalItems: 1000,
|
|
54
|
+
* incomplete: false,
|
|
55
|
+
* error: null
|
|
56
|
+
* }
|
|
57
|
+
* }
|
|
58
|
+
* }
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* // Response with both retry and pagination metadata
|
|
62
|
+
* {
|
|
63
|
+
* success: true,
|
|
64
|
+
* statusCode: 200,
|
|
65
|
+
* headers: {...},
|
|
66
|
+
* body: "{\"items\": [...], \"returnedItemCount\": 1000}",
|
|
67
|
+
* message: "SUCCESS",
|
|
68
|
+
* metadata: {
|
|
69
|
+
* retries: {
|
|
70
|
+
* occurred: true,
|
|
71
|
+
* attempts: 2,
|
|
72
|
+
* finalAttempt: 2
|
|
73
|
+
* },
|
|
74
|
+
* pagination: {
|
|
75
|
+
* occurred: true,
|
|
76
|
+
* totalPages: 5,
|
|
77
|
+
* totalItems: 1000,
|
|
78
|
+
* incomplete: false,
|
|
79
|
+
* error: null
|
|
80
|
+
* }
|
|
81
|
+
* }
|
|
82
|
+
* }
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* // Response with incomplete pagination
|
|
86
|
+
* {
|
|
87
|
+
* success: true,
|
|
88
|
+
* statusCode: 200,
|
|
89
|
+
* headers: {...},
|
|
90
|
+
* body: "{\"items\": [...], \"returnedItemCount\": 600}",
|
|
91
|
+
* message: "SUCCESS",
|
|
92
|
+
* metadata: {
|
|
93
|
+
* pagination: {
|
|
94
|
+
* occurred: true,
|
|
95
|
+
* totalPages: 3,
|
|
96
|
+
* totalItems: 600,
|
|
97
|
+
* incomplete: true,
|
|
98
|
+
* error: "Network error fetching page at offset 600"
|
|
99
|
+
* }
|
|
100
|
+
* }
|
|
101
|
+
* }
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* // Response without metadata (no pagination or retry occurred)
|
|
105
|
+
* {
|
|
106
|
+
* success: true,
|
|
107
|
+
* statusCode: 200,
|
|
108
|
+
* headers: {...},
|
|
109
|
+
* body: "...",
|
|
110
|
+
* message: "SUCCESS"
|
|
111
|
+
* // No metadata field present
|
|
112
|
+
* }
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @typedef {object} APIResponse
|
|
117
|
+
* @description Standard response object returned by APIRequest.send()
|
|
118
|
+
*
|
|
119
|
+
* @property {boolean} success - Whether the request was successful (statusCode < 400)
|
|
120
|
+
* @property {number} statusCode - HTTP status code
|
|
121
|
+
* @property {object|null} headers - Response headers
|
|
122
|
+
* @property {string|null} body - Response body
|
|
123
|
+
* @property {string|null} message - Response message
|
|
124
|
+
* @property {ResponseMetadata} [metadata] - Optional metadata (present only if pagination or retry occurred)
|
|
125
|
+
*/
|
|
126
|
+
|
|
7
127
|
/* Either return an XRay segment or mock one up so we don't need much logic if xray isn't used */
|
|
8
128
|
const xRayProxyFunc = {
|
|
9
129
|
addMetadata: (mockParam, mockObj) => { DebugAndLog.debug(`Mocking XRay addMetadata: ${mockParam} | ${mockObj}`); },
|
|
@@ -254,24 +374,69 @@ const _httpGetExecute = async function (options, requestObject, xRaySegment = xR
|
|
|
254
374
|
/**
|
|
255
375
|
* Submit GET and POST requests and handle responses.
|
|
256
376
|
* This class can be used in a DAO class object within its call() method.
|
|
257
|
-
* @example
|
|
258
|
-
*
|
|
259
|
-
* async call() {
|
|
260
377
|
*
|
|
261
|
-
*
|
|
378
|
+
* Features:
|
|
379
|
+
* - Automatic redirect handling (up to MAX_REDIRECTS)
|
|
380
|
+
* - Optional automatic pagination for paginated APIs
|
|
381
|
+
* - Optional automatic retry logic for transient failures
|
|
382
|
+
* - AWS X-Ray distributed tracing with unique subsegments
|
|
383
|
+
* - Response metadata for pagination and retry details
|
|
262
384
|
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
385
|
+
* @example
|
|
386
|
+
* // Basic usage in a DAO class
|
|
387
|
+
* async call() {
|
|
388
|
+
* var response = null;
|
|
389
|
+
* try {
|
|
390
|
+
* var apiRequest = new tools.APIRequest(this.request);
|
|
391
|
+
* response = await apiRequest.send();
|
|
392
|
+
* } catch (error) {
|
|
393
|
+
* DebugAndLog.error(`Error in call: ${error.message}`, error.stack);
|
|
394
|
+
* response = tools.APIRequest.responseFormat(false, 500, "Error in call()");
|
|
395
|
+
* }
|
|
396
|
+
* return response;
|
|
397
|
+
* }
|
|
266
398
|
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
399
|
+
* @example
|
|
400
|
+
* // Using pagination feature
|
|
401
|
+
* const apiRequest = new tools.APIRequest({
|
|
402
|
+
* host: 'api.example.com',
|
|
403
|
+
* path: '/data',
|
|
404
|
+
* pagination: {
|
|
405
|
+
* enabled: true,
|
|
406
|
+
* totalItemsLabel: 'total',
|
|
407
|
+
* itemsLabel: 'results'
|
|
408
|
+
* }
|
|
409
|
+
* });
|
|
410
|
+
* const response = await apiRequest.send();
|
|
411
|
+
* // All pages automatically retrieved and combined
|
|
271
412
|
*
|
|
272
|
-
*
|
|
413
|
+
* @example
|
|
414
|
+
* // Using retry feature
|
|
415
|
+
* const apiRequest = new tools.APIRequest({
|
|
416
|
+
* host: 'api.example.com',
|
|
417
|
+
* path: '/data',
|
|
418
|
+
* retry: {
|
|
419
|
+
* enabled: true,
|
|
420
|
+
* maxRetries: 3
|
|
421
|
+
* }
|
|
422
|
+
* });
|
|
423
|
+
* const response = await apiRequest.send();
|
|
424
|
+
* // Automatically retries on transient failures
|
|
273
425
|
*
|
|
274
|
-
*
|
|
426
|
+
* @example
|
|
427
|
+
* // Using both pagination and retry
|
|
428
|
+
* const apiRequest = new tools.APIRequest({
|
|
429
|
+
* host: 'api.example.com',
|
|
430
|
+
* path: '/data',
|
|
431
|
+
* pagination: { enabled: true },
|
|
432
|
+
* retry: { enabled: true, maxRetries: 2 }
|
|
433
|
+
* });
|
|
434
|
+
* const response = await apiRequest.send();
|
|
435
|
+
* // Response includes metadata about both features
|
|
436
|
+
* if (response.metadata) {
|
|
437
|
+
* console.log('Retries:', response.metadata.retries);
|
|
438
|
+
* console.log('Pagination:', response.metadata.pagination);
|
|
439
|
+
* }
|
|
275
440
|
*/
|
|
276
441
|
class APIRequest {
|
|
277
442
|
|
|
@@ -281,12 +446,119 @@ class APIRequest {
|
|
|
281
446
|
#requestComplete = false;
|
|
282
447
|
#response = null;
|
|
283
448
|
#request = null;
|
|
449
|
+
#responseMetadata = null;
|
|
284
450
|
|
|
285
451
|
/**
|
|
286
452
|
* Function used to make an API request utilized directly or from within
|
|
287
453
|
* a data access object.
|
|
288
454
|
*
|
|
289
|
-
* @param {
|
|
455
|
+
* @param {object} request - Request configuration object
|
|
456
|
+
* @param {string} [request.method="GET"] - HTTP method (GET, POST)
|
|
457
|
+
* @param {string} [request.protocol="https"] - Protocol (https or http)
|
|
458
|
+
* @param {string} request.host - Host domain for the request
|
|
459
|
+
* @param {string} [request.path=""] - Path for the request
|
|
460
|
+
* @param {string} [request.uri] - Complete URI (overrides host/path if provided)
|
|
461
|
+
* @param {object} [request.parameters={}] - Query string parameters
|
|
462
|
+
* @param {object} [request.headers={}] - HTTP headers
|
|
463
|
+
* @param {string|null} [request.body=null] - Request body for POST requests
|
|
464
|
+
* @param {string} [request.note=""] - Note for troubleshooting and tracing
|
|
465
|
+
* @param {object} [request.options] - Request options
|
|
466
|
+
* @param {number} [request.options.timeout=8000] - Request timeout in milliseconds
|
|
467
|
+
* @param {boolean} [request.options.separateDuplicateParameters=false] - Whether to separate array parameters into multiple key/value pairs
|
|
468
|
+
* @param {string} [request.options.separateDuplicateParametersAppendToKey=""] - String to append to key for separated parameters ("", "[]", "0++", "1++")
|
|
469
|
+
* @param {string} [request.options.combinedDuplicateParameterDelimiter=","] - Delimiter for combined array parameters
|
|
470
|
+
* @param {object} [request.pagination] - Pagination configuration (opt-in)
|
|
471
|
+
* @param {boolean} [request.pagination.enabled=false] - Enable automatic pagination
|
|
472
|
+
* @param {string} [request.pagination.totalItemsLabel="totalItems"] - Response field name for total items count
|
|
473
|
+
* @param {string} [request.pagination.itemsLabel="items"] - Response field name for items array
|
|
474
|
+
* @param {string} [request.pagination.offsetLabel="offset"] - Parameter name for offset
|
|
475
|
+
* @param {string} [request.pagination.limitLabel="limit"] - Parameter name for limit
|
|
476
|
+
* @param {string|null} [request.pagination.continuationTokenLabel=null] - Parameter name for continuation token (for token-based pagination)
|
|
477
|
+
* @param {string} [request.pagination.responseReturnCountLabel="returnedItemCount"] - Response field name for returned item count
|
|
478
|
+
* @param {number} [request.pagination.defaultLimit=200] - Default limit per page
|
|
479
|
+
* @param {number} [request.pagination.batchSize=5] - Number of pages to fetch concurrently
|
|
480
|
+
* @param {object} [request.retry] - Retry configuration (opt-in)
|
|
481
|
+
* @param {boolean} [request.retry.enabled=false] - Enable automatic retries
|
|
482
|
+
* @param {number} [request.retry.maxRetries=1] - Maximum number of retry attempts after initial attempt (total attempts = maxRetries + 1)
|
|
483
|
+
* @param {object} [request.retry.retryOn] - Conditions for retrying requests
|
|
484
|
+
* @param {boolean} [request.retry.retryOn.networkError=true] - Retry on network errors
|
|
485
|
+
* @param {boolean} [request.retry.retryOn.emptyResponse=true] - Retry on empty or null response body
|
|
486
|
+
* @param {boolean} [request.retry.retryOn.parseError=true] - Retry on JSON parse errors
|
|
487
|
+
* @param {boolean} [request.retry.retryOn.serverError=true] - Retry on 5xx status codes
|
|
488
|
+
* @param {boolean} [request.retry.retryOn.clientError=false] - Retry on 4xx status codes (default: false)
|
|
489
|
+
*
|
|
490
|
+
* @example
|
|
491
|
+
* // Basic request without pagination or retry
|
|
492
|
+
* const request = new APIRequest({
|
|
493
|
+
* host: 'api.example.com',
|
|
494
|
+
* path: '/users',
|
|
495
|
+
* parameters: { limit: 10 }
|
|
496
|
+
* });
|
|
497
|
+
* const response = await request.send();
|
|
498
|
+
*
|
|
499
|
+
* @example
|
|
500
|
+
* // Request with minimal pagination configuration (uses all defaults)
|
|
501
|
+
* const request = new APIRequest({
|
|
502
|
+
* host: 'api.example.com',
|
|
503
|
+
* path: '/data',
|
|
504
|
+
* pagination: { enabled: true }
|
|
505
|
+
* });
|
|
506
|
+
* const response = await request.send();
|
|
507
|
+
* // Response will include metadata.pagination with details
|
|
508
|
+
*
|
|
509
|
+
* @example
|
|
510
|
+
* // Request with custom pagination labels
|
|
511
|
+
* const request = new APIRequest({
|
|
512
|
+
* host: 'api.example.com',
|
|
513
|
+
* path: '/data',
|
|
514
|
+
* pagination: {
|
|
515
|
+
* enabled: true,
|
|
516
|
+
* totalItemsLabel: 'total',
|
|
517
|
+
* itemsLabel: 'results',
|
|
518
|
+
* offsetLabel: 'skip',
|
|
519
|
+
* limitLabel: 'take',
|
|
520
|
+
* batchSize: 3
|
|
521
|
+
* }
|
|
522
|
+
* });
|
|
523
|
+
* const response = await request.send();
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* // Request with minimal retry configuration (uses all defaults)
|
|
527
|
+
* const request = new APIRequest({
|
|
528
|
+
* host: 'api.example.com',
|
|
529
|
+
* path: '/data',
|
|
530
|
+
* retry: { enabled: true }
|
|
531
|
+
* });
|
|
532
|
+
* const response = await request.send();
|
|
533
|
+
* // Response will include metadata.retries if retries occurred
|
|
534
|
+
*
|
|
535
|
+
* @example
|
|
536
|
+
* // Request with custom retry configuration
|
|
537
|
+
* const request = new APIRequest({
|
|
538
|
+
* host: 'api.example.com',
|
|
539
|
+
* path: '/data',
|
|
540
|
+
* retry: {
|
|
541
|
+
* enabled: true,
|
|
542
|
+
* maxRetries: 3,
|
|
543
|
+
* retryOn: {
|
|
544
|
+
* clientError: true, // Also retry on 4xx errors
|
|
545
|
+
* serverError: true,
|
|
546
|
+
* networkError: true
|
|
547
|
+
* }
|
|
548
|
+
* }
|
|
549
|
+
* });
|
|
550
|
+
* const response = await request.send();
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* // Request with both pagination and retry
|
|
554
|
+
* const request = new APIRequest({
|
|
555
|
+
* host: 'api.example.com',
|
|
556
|
+
* path: '/data',
|
|
557
|
+
* pagination: { enabled: true },
|
|
558
|
+
* retry: { enabled: true, maxRetries: 2 }
|
|
559
|
+
* });
|
|
560
|
+
* const response = await request.send();
|
|
561
|
+
* // Response may include both metadata.pagination and metadata.retries
|
|
290
562
|
*/
|
|
291
563
|
constructor(request) {
|
|
292
564
|
this.resetRequest();
|
|
@@ -296,6 +568,32 @@ class APIRequest {
|
|
|
296
568
|
|
|
297
569
|
let timeOutInMilliseconds = 8000;
|
|
298
570
|
|
|
571
|
+
/* Default pagination configuration */
|
|
572
|
+
const defaultPaginationConfig = {
|
|
573
|
+
enabled: false,
|
|
574
|
+
totalItemsLabel: 'totalItems',
|
|
575
|
+
itemsLabel: 'items',
|
|
576
|
+
offsetLabel: 'offset',
|
|
577
|
+
limitLabel: 'limit',
|
|
578
|
+
continuationTokenLabel: null,
|
|
579
|
+
responseReturnCountLabel: 'returnedItemCount',
|
|
580
|
+
defaultLimit: 200,
|
|
581
|
+
batchSize: 5
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
/* Default retry configuration */
|
|
585
|
+
const defaultRetryConfig = {
|
|
586
|
+
enabled: false,
|
|
587
|
+
maxRetries: 1,
|
|
588
|
+
retryOn: {
|
|
589
|
+
networkError: true,
|
|
590
|
+
emptyResponse: true,
|
|
591
|
+
parseError: true,
|
|
592
|
+
serverError: true,
|
|
593
|
+
clientError: false
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
|
|
299
597
|
/* Default values */
|
|
300
598
|
let req = {
|
|
301
599
|
method: "GET",
|
|
@@ -312,7 +610,9 @@ class APIRequest {
|
|
|
312
610
|
separateDuplicateParameters: false,
|
|
313
611
|
separateDuplicateParametersAppendToKey: "", // "" "[]", or "0++", "1++"
|
|
314
612
|
combinedDuplicateParameterDelimiter: ',' // "," or "|" or " "
|
|
315
|
-
}
|
|
613
|
+
},
|
|
614
|
+
pagination: defaultPaginationConfig,
|
|
615
|
+
retry: defaultRetryConfig
|
|
316
616
|
};
|
|
317
617
|
|
|
318
618
|
/* if we have a method or protocol passed to us, set them */
|
|
@@ -326,6 +626,20 @@ class APIRequest {
|
|
|
326
626
|
// With options we want to keep our defaults so we'll use Object.assign
|
|
327
627
|
if ("options" in request && request.options !== null) { req.options = Object.assign(req.options, request.options); }
|
|
328
628
|
|
|
629
|
+
// Merge pagination configuration with defaults
|
|
630
|
+
if ("pagination" in request && request.pagination !== null) {
|
|
631
|
+
req.pagination = Object.assign({}, defaultPaginationConfig, request.pagination);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Merge retry configuration with defaults, including nested retryOn object
|
|
635
|
+
if ("retry" in request && request.retry !== null && request.retry !== undefined) {
|
|
636
|
+
req.retry = Object.assign({}, defaultRetryConfig, request.retry);
|
|
637
|
+
// Ensure nested retryOn object is also merged properly
|
|
638
|
+
if ("retryOn" in request.retry && request.retry.retryOn !== null) {
|
|
639
|
+
req.retry.retryOn = Object.assign({}, defaultRetryConfig.retryOn, request.retry.retryOn);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
329
643
|
/* if there is no timeout set, or if it is less than 1, then set to default */
|
|
330
644
|
if ( !("timeout" in req.options && req.options.timeout > 0) ) {
|
|
331
645
|
req.options.timeout = timeOutInMilliseconds;
|
|
@@ -394,6 +708,398 @@ class APIRequest {
|
|
|
394
708
|
return (qString.length > 0) ? '?'+qString.join("&") : "";
|
|
395
709
|
}
|
|
396
710
|
|
|
711
|
+
/**
|
|
712
|
+
* Handle request with retry logic. This private method wraps the HTTP request
|
|
713
|
+
* execution with automatic retry functionality based on the retry configuration.
|
|
714
|
+
*
|
|
715
|
+
* @private
|
|
716
|
+
* @param {object} options - HTTPS request options for the underlying https.request call
|
|
717
|
+
* @param {object} xRaySegment - X-Ray subsegment for tracking request attempts
|
|
718
|
+
* @returns {Promise<{response: object, metadata: object}>} Promise resolving to response and retry metadata
|
|
719
|
+
* @returns {object} return.response - The final response object from the request
|
|
720
|
+
* @returns {boolean} return.response.success - Whether the request was successful
|
|
721
|
+
* @returns {number} return.response.statusCode - HTTP status code
|
|
722
|
+
* @returns {object} return.response.headers - Response headers
|
|
723
|
+
* @returns {string} return.response.body - Response body
|
|
724
|
+
* @returns {string} return.response.message - Response message
|
|
725
|
+
* @returns {object} return.metadata - Metadata about retry attempts
|
|
726
|
+
* @returns {object} return.metadata.retries - Retry information
|
|
727
|
+
* @returns {boolean} return.metadata.retries.occurred - Whether retries occurred
|
|
728
|
+
* @returns {number} return.metadata.retries.attempts - Total number of attempts made (including initial)
|
|
729
|
+
* @returns {number} return.metadata.retries.finalAttempt - Which attempt succeeded
|
|
730
|
+
*
|
|
731
|
+
* @example
|
|
732
|
+
* // Internal usage within send_get()
|
|
733
|
+
* const { response, metadata } = await this._handleRetries(options, xRaySegment);
|
|
734
|
+
* if (metadata.retries.occurred) {
|
|
735
|
+
* console.log(`Request succeeded after ${metadata.retries.attempts} attempts`);
|
|
736
|
+
* }
|
|
737
|
+
*/
|
|
738
|
+
async _handleRetries(options, xRaySegment) {
|
|
739
|
+
const retryConfig = this.#request.retry || { enabled: false };
|
|
740
|
+
const maxRetries = retryConfig.enabled ? (retryConfig.maxRetries || 1) : 0;
|
|
741
|
+
|
|
742
|
+
let lastResponse = null;
|
|
743
|
+
let attempts = 0;
|
|
744
|
+
|
|
745
|
+
// Loop: initial attempt + retry attempts
|
|
746
|
+
// If maxRetries = 1, this loops twice (attempt 0 and attempt 1)
|
|
747
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
748
|
+
attempts++;
|
|
749
|
+
|
|
750
|
+
if (attempt > 0) {
|
|
751
|
+
this.#request.note += ` [Retry ${attempt}/${maxRetries}]`;
|
|
752
|
+
DebugAndLog.warn(`Retrying request (${this.#request.note})`);
|
|
753
|
+
// Reset request state for retry
|
|
754
|
+
this.#requestComplete = false;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Perform the request (handles redirects internally)
|
|
758
|
+
while (!this.#requestComplete) {
|
|
759
|
+
await _httpGetExecute(options, this, xRaySegment);
|
|
760
|
+
}
|
|
761
|
+
lastResponse = this.#response;
|
|
762
|
+
|
|
763
|
+
// Check if we should retry
|
|
764
|
+
const shouldRetry = this._shouldRetry(lastResponse, retryConfig, attempt, maxRetries);
|
|
765
|
+
|
|
766
|
+
if (!shouldRetry) {
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return {
|
|
772
|
+
response: lastResponse,
|
|
773
|
+
metadata: {
|
|
774
|
+
retries: {
|
|
775
|
+
occurred: attempts > 1,
|
|
776
|
+
attempts: attempts,
|
|
777
|
+
finalAttempt: attempts
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Determine if a request should be retried based on the response and retry configuration.
|
|
785
|
+
* This private method evaluates various failure conditions to decide whether another
|
|
786
|
+
* attempt should be made.
|
|
787
|
+
*
|
|
788
|
+
* @private
|
|
789
|
+
* @param {object} response - Current response object from the request attempt
|
|
790
|
+
* @param {boolean} response.success - Whether the request was successful
|
|
791
|
+
* @param {number} response.statusCode - HTTP status code
|
|
792
|
+
* @param {string} response.body - Response body
|
|
793
|
+
* @param {object} retryConfig - Retry configuration object
|
|
794
|
+
* @param {boolean} retryConfig.enabled - Whether retries are enabled
|
|
795
|
+
* @param {number} retryConfig.maxRetries - Maximum number of retry attempts
|
|
796
|
+
* @param {object} retryConfig.retryOn - Conditions for retrying
|
|
797
|
+
* @param {boolean} retryConfig.retryOn.networkError - Retry on network errors
|
|
798
|
+
* @param {boolean} retryConfig.retryOn.emptyResponse - Retry on empty responses
|
|
799
|
+
* @param {boolean} retryConfig.retryOn.parseError - Retry on JSON parse errors
|
|
800
|
+
* @param {boolean} retryConfig.retryOn.serverError - Retry on 5xx errors
|
|
801
|
+
* @param {boolean} retryConfig.retryOn.clientError - Retry on 4xx errors
|
|
802
|
+
* @param {number} currentAttempt - Current attempt number (0-indexed)
|
|
803
|
+
* @param {number} maxRetries - Maximum retry attempts allowed
|
|
804
|
+
* @returns {boolean} True if the request should be retried, false otherwise
|
|
805
|
+
*
|
|
806
|
+
* @example
|
|
807
|
+
* // Internal usage within _handleRetries()
|
|
808
|
+
* const shouldRetry = this._shouldRetry(lastResponse, retryConfig, attempt, maxRetries);
|
|
809
|
+
* if (!shouldRetry) {
|
|
810
|
+
* break; // Exit retry loop
|
|
811
|
+
* }
|
|
812
|
+
*/
|
|
813
|
+
_shouldRetry(response, retryConfig, currentAttempt, maxRetries) {
|
|
814
|
+
// No more retries available
|
|
815
|
+
if (currentAttempt >= maxRetries) {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const retryOn = retryConfig.retryOn || {};
|
|
820
|
+
|
|
821
|
+
// Check for network errors (no response)
|
|
822
|
+
if (!response && retryOn.networkError !== false) {
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Check for empty/null body
|
|
827
|
+
if (retryOn.emptyResponse !== false &&
|
|
828
|
+
(response.body === null || response.body === "")) {
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Check for server errors (5xx)
|
|
833
|
+
if (retryOn.serverError !== false &&
|
|
834
|
+
response.statusCode >= 500 && response.statusCode < 600) {
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Check for client errors (4xx) - default is NOT to retry
|
|
839
|
+
if (retryOn.clientError === true &&
|
|
840
|
+
response.statusCode >= 400 && response.statusCode < 500) {
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Try to parse JSON if it's expected to be JSON
|
|
845
|
+
if (retryOn.parseError !== false && response.body) {
|
|
846
|
+
try {
|
|
847
|
+
JSON.parse(response.body);
|
|
848
|
+
} catch (error) {
|
|
849
|
+
return true;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Handle pagination for API responses. This private method automatically retrieves
|
|
858
|
+
* all pages of results from a paginated API and combines them into a single response.
|
|
859
|
+
*
|
|
860
|
+
* @private
|
|
861
|
+
* @param {object} initialResponse - Initial API response from the first request
|
|
862
|
+
* @param {boolean} initialResponse.success - Whether the initial request was successful
|
|
863
|
+
* @param {number} initialResponse.statusCode - HTTP status code
|
|
864
|
+
* @param {string} initialResponse.body - Response body (JSON string)
|
|
865
|
+
* @param {object} initialResponse.headers - Response headers
|
|
866
|
+
* @returns {Promise<{response: object, metadata: object}>} Promise resolving to combined response and pagination metadata
|
|
867
|
+
* @returns {object} return.response - The combined response with all pages
|
|
868
|
+
* @returns {boolean} return.response.success - Whether pagination was successful
|
|
869
|
+
* @returns {number} return.response.statusCode - HTTP status code
|
|
870
|
+
* @returns {string} return.response.body - Combined response body with all items
|
|
871
|
+
* @returns {object} return.metadata - Metadata about pagination
|
|
872
|
+
* @returns {object} return.metadata.pagination - Pagination information
|
|
873
|
+
* @returns {boolean} return.metadata.pagination.occurred - Whether pagination occurred
|
|
874
|
+
* @returns {number} [return.metadata.pagination.totalPages] - Total pages retrieved (if pagination occurred)
|
|
875
|
+
* @returns {number} [return.metadata.pagination.totalItems] - Total items returned (if pagination occurred)
|
|
876
|
+
* @returns {boolean} [return.metadata.pagination.incomplete] - Whether pagination was incomplete due to errors
|
|
877
|
+
* @returns {string|null} [return.metadata.pagination.error] - Error message if pagination was incomplete
|
|
878
|
+
*
|
|
879
|
+
* @example
|
|
880
|
+
* // Internal usage within send_get()
|
|
881
|
+
* const { response: finalResponse, metadata: paginationMetadata } =
|
|
882
|
+
* await this._handlePagination(response);
|
|
883
|
+
* if (paginationMetadata.pagination.occurred) {
|
|
884
|
+
* console.log(`Retrieved ${paginationMetadata.pagination.totalItems} items from ${paginationMetadata.pagination.totalPages} pages`);
|
|
885
|
+
* }
|
|
886
|
+
*/
|
|
887
|
+
async _handlePagination(initialResponse) {
|
|
888
|
+
const paginationConfig = this.#request.pagination || { enabled: false };
|
|
889
|
+
|
|
890
|
+
// If pagination not enabled or initial request failed, return as-is
|
|
891
|
+
if (!paginationConfig.enabled || !initialResponse.success) {
|
|
892
|
+
return {
|
|
893
|
+
response: initialResponse,
|
|
894
|
+
metadata: { pagination: { occurred: false } }
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Parse body to check for pagination indicators
|
|
899
|
+
let body;
|
|
900
|
+
try {
|
|
901
|
+
body = JSON.parse(initialResponse.body);
|
|
902
|
+
} catch (error) {
|
|
903
|
+
// Can't paginate if body isn't JSON
|
|
904
|
+
return {
|
|
905
|
+
response: initialResponse,
|
|
906
|
+
metadata: { pagination: { occurred: false } }
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const {
|
|
911
|
+
totalItemsLabel = 'totalItems',
|
|
912
|
+
itemsLabel = 'items',
|
|
913
|
+
offsetLabel = 'offset',
|
|
914
|
+
limitLabel = 'limit',
|
|
915
|
+
continuationTokenLabel = null,
|
|
916
|
+
responseReturnCountLabel = 'returnedItemCount',
|
|
917
|
+
defaultLimit = 200,
|
|
918
|
+
batchSize = 5
|
|
919
|
+
} = paginationConfig;
|
|
920
|
+
|
|
921
|
+
// Check if response has pagination indicators
|
|
922
|
+
if (!(totalItemsLabel in body) || !(itemsLabel in body)) {
|
|
923
|
+
return {
|
|
924
|
+
response: initialResponse,
|
|
925
|
+
metadata: { pagination: { occurred: false } }
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Check if we're already on a paginated request (offset > 0)
|
|
930
|
+
if (offsetLabel in this.#request.parameters &&
|
|
931
|
+
this.#request.parameters[offsetLabel] > 0) {
|
|
932
|
+
return {
|
|
933
|
+
response: initialResponse,
|
|
934
|
+
metadata: { pagination: { occurred: false } }
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const limit = this.#request.parameters[limitLabel] || defaultLimit;
|
|
939
|
+
const totalRecords = body[totalItemsLabel];
|
|
940
|
+
|
|
941
|
+
// Calculate offsets for remaining pages
|
|
942
|
+
const offsets = [];
|
|
943
|
+
for (let offset = limit; offset < totalRecords; offset += limit) {
|
|
944
|
+
offsets.push(offset);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// If no more pages, return initial response
|
|
948
|
+
if (offsets.length === 0) {
|
|
949
|
+
return {
|
|
950
|
+
response: initialResponse,
|
|
951
|
+
metadata: { pagination: { occurred: false } }
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Fetch remaining pages in batches
|
|
956
|
+
const allResults = [];
|
|
957
|
+
let incomplete = false;
|
|
958
|
+
let paginationError = null;
|
|
959
|
+
|
|
960
|
+
for (let i = 0; i < offsets.length; i += batchSize) {
|
|
961
|
+
const batchOffsets = offsets.slice(i, i + batchSize);
|
|
962
|
+
const batchPromises = batchOffsets.map(offset =>
|
|
963
|
+
this._fetchPage(offset, offsetLabel, limitLabel)
|
|
964
|
+
);
|
|
965
|
+
|
|
966
|
+
try {
|
|
967
|
+
const batchResults = await Promise.all(batchPromises);
|
|
968
|
+
allResults.push(...batchResults);
|
|
969
|
+
} catch (error) {
|
|
970
|
+
incomplete = true;
|
|
971
|
+
paginationError = error.message;
|
|
972
|
+
DebugAndLog.warn(`Pagination incomplete: ${error.message}`);
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Combine all results
|
|
978
|
+
const allRecords = [
|
|
979
|
+
...body[itemsLabel],
|
|
980
|
+
...allResults.flatMap(result => {
|
|
981
|
+
if (!result || !result.body) {
|
|
982
|
+
incomplete = true;
|
|
983
|
+
return [];
|
|
984
|
+
}
|
|
985
|
+
try {
|
|
986
|
+
const pageBody = JSON.parse(result.body);
|
|
987
|
+
return pageBody[itemsLabel] || [];
|
|
988
|
+
} catch (error) {
|
|
989
|
+
incomplete = true;
|
|
990
|
+
return [];
|
|
991
|
+
}
|
|
992
|
+
})
|
|
993
|
+
];
|
|
994
|
+
|
|
995
|
+
// Build combined response
|
|
996
|
+
const combinedBody = {
|
|
997
|
+
...body,
|
|
998
|
+
[itemsLabel]: allRecords
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
// Clean up pagination parameters from response
|
|
1002
|
+
delete combinedBody[offsetLabel];
|
|
1003
|
+
delete combinedBody[limitLabel];
|
|
1004
|
+
combinedBody[responseReturnCountLabel] = allRecords.length;
|
|
1005
|
+
|
|
1006
|
+
const combinedResponse = {
|
|
1007
|
+
...initialResponse,
|
|
1008
|
+
body: JSON.stringify(combinedBody)
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
return {
|
|
1012
|
+
response: combinedResponse,
|
|
1013
|
+
metadata: {
|
|
1014
|
+
pagination: {
|
|
1015
|
+
occurred: true,
|
|
1016
|
+
totalPages: allResults.length + 1,
|
|
1017
|
+
totalItems: allRecords.length,
|
|
1018
|
+
incomplete: incomplete,
|
|
1019
|
+
error: paginationError
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Fetch a single page of paginated results. This private method creates a new
|
|
1027
|
+
* APIRequest instance for a specific page offset and retrieves that page's data.
|
|
1028
|
+
* If X-Ray is available, it creates a subsegment to track the page request.
|
|
1029
|
+
*
|
|
1030
|
+
* @private
|
|
1031
|
+
* @param {number} offset - Offset value for this page (e.g., 200, 400, 600)
|
|
1032
|
+
* @param {string} offsetLabel - Parameter name for offset in the API (e.g., 'offset', 'skip')
|
|
1033
|
+
* @param {string} limitLabel - Parameter name for limit in the API (e.g., 'limit', 'take')
|
|
1034
|
+
* @returns {Promise<object>} Promise resolving to the page response
|
|
1035
|
+
* @returns {boolean} return.success - Whether the page request was successful
|
|
1036
|
+
* @returns {number} return.statusCode - HTTP status code
|
|
1037
|
+
* @returns {string} return.body - Page response body (JSON string)
|
|
1038
|
+
* @returns {object} return.headers - Response headers
|
|
1039
|
+
*
|
|
1040
|
+
* @example
|
|
1041
|
+
* // Internal usage within _handlePagination()
|
|
1042
|
+
* const pageResponse = await this._fetchPage(200, 'offset', 'limit');
|
|
1043
|
+
* const pageBody = JSON.parse(pageResponse.body);
|
|
1044
|
+
* const pageItems = pageBody.items;
|
|
1045
|
+
*/
|
|
1046
|
+
async _fetchPage(offset, offsetLabel, limitLabel) {
|
|
1047
|
+
// Clone the current request
|
|
1048
|
+
const pageRequest = {
|
|
1049
|
+
...this.#request,
|
|
1050
|
+
parameters: {
|
|
1051
|
+
...this.#request.parameters,
|
|
1052
|
+
[offsetLabel]: offset
|
|
1053
|
+
},
|
|
1054
|
+
note: `${this.#request.note} [Offset ${offset}]`,
|
|
1055
|
+
// Disable pagination for sub-requests to avoid infinite loops
|
|
1056
|
+
pagination: { enabled: false },
|
|
1057
|
+
// Keep retry configuration
|
|
1058
|
+
retry: this.#request.retry
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
// Create subsegment for this paginated request if X-Ray is available
|
|
1062
|
+
if (AWSXRay) {
|
|
1063
|
+
const subsegmentName = `APIRequest/${this.getHost()}/Page-${offset}`;
|
|
1064
|
+
|
|
1065
|
+
return await AWSXRay.captureAsyncFunc(subsegmentName, async (subsegment) => {
|
|
1066
|
+
try {
|
|
1067
|
+
subsegment.namespace = 'remote';
|
|
1068
|
+
|
|
1069
|
+
// Add page metadata to subsegment
|
|
1070
|
+
subsegment.addAnnotation('page_offset', offset);
|
|
1071
|
+
subsegment.addAnnotation('request_host', this.getHost());
|
|
1072
|
+
subsegment.addAnnotation('request_note', pageRequest.note);
|
|
1073
|
+
subsegment.addMetadata('page_info', {
|
|
1074
|
+
offset: offset,
|
|
1075
|
+
offsetLabel: offsetLabel,
|
|
1076
|
+
limitLabel: limitLabel,
|
|
1077
|
+
parentNote: this.#request.note
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
// Create new APIRequest instance for this page
|
|
1081
|
+
const pageApiRequest = new APIRequest(pageRequest);
|
|
1082
|
+
const response = await pageApiRequest.send();
|
|
1083
|
+
|
|
1084
|
+
subsegment.addAnnotation('success', response.success ? "true" : "false");
|
|
1085
|
+
subsegment.addAnnotation('status_code', response?.statusCode || 500);
|
|
1086
|
+
|
|
1087
|
+
return response;
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
DebugAndLog.error(`Error fetching page at offset ${offset}: ${error.message}`, error.stack);
|
|
1090
|
+
subsegment.addError(error);
|
|
1091
|
+
throw error;
|
|
1092
|
+
} finally {
|
|
1093
|
+
subsegment.close();
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
} else {
|
|
1097
|
+
// No X-Ray available, just create and send the request
|
|
1098
|
+
const pageApiRequest = new APIRequest(pageRequest);
|
|
1099
|
+
return await pageApiRequest.send();
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
397
1103
|
/**
|
|
398
1104
|
* Clears out any redirects, completion flag, and response
|
|
399
1105
|
*/
|
|
@@ -486,8 +1192,56 @@ class APIRequest {
|
|
|
486
1192
|
};
|
|
487
1193
|
|
|
488
1194
|
/**
|
|
489
|
-
* Send the request
|
|
490
|
-
*
|
|
1195
|
+
* Send the request. This method dispatches the request based on the HTTP method
|
|
1196
|
+
* (GET or POST). It automatically handles pagination and retries if configured.
|
|
1197
|
+
*
|
|
1198
|
+
* @returns {Promise<object>} Promise resolving to the response object
|
|
1199
|
+
* @returns {boolean} return.success - Whether the request was successful
|
|
1200
|
+
* @returns {number} return.statusCode - HTTP status code
|
|
1201
|
+
* @returns {object} return.headers - Response headers
|
|
1202
|
+
* @returns {string} return.body - Response body
|
|
1203
|
+
* @returns {string} return.message - Response message
|
|
1204
|
+
* @returns {object} [return.metadata] - Optional metadata (present if pagination or retries occurred)
|
|
1205
|
+
* @returns {object} [return.metadata.retries] - Retry metadata (if retries occurred)
|
|
1206
|
+
* @returns {boolean} return.metadata.retries.occurred - Whether retries occurred
|
|
1207
|
+
* @returns {number} return.metadata.retries.attempts - Total attempts made
|
|
1208
|
+
* @returns {number} return.metadata.retries.finalAttempt - Which attempt succeeded
|
|
1209
|
+
* @returns {object} [return.metadata.pagination] - Pagination metadata (if pagination occurred)
|
|
1210
|
+
* @returns {boolean} return.metadata.pagination.occurred - Whether pagination occurred
|
|
1211
|
+
* @returns {number} return.metadata.pagination.totalPages - Total pages retrieved
|
|
1212
|
+
* @returns {number} return.metadata.pagination.totalItems - Total items returned
|
|
1213
|
+
* @returns {boolean} return.metadata.pagination.incomplete - Whether pagination was incomplete
|
|
1214
|
+
* @returns {string|null} return.metadata.pagination.error - Error message if incomplete
|
|
1215
|
+
*
|
|
1216
|
+
* @example
|
|
1217
|
+
* // Basic request
|
|
1218
|
+
* const apiRequest = new APIRequest({ host: 'api.example.com', path: '/users' });
|
|
1219
|
+
* const response = await apiRequest.send();
|
|
1220
|
+
* console.log(response.body);
|
|
1221
|
+
*
|
|
1222
|
+
* @example
|
|
1223
|
+
* // Request with pagination
|
|
1224
|
+
* const apiRequest = new APIRequest({
|
|
1225
|
+
* host: 'api.example.com',
|
|
1226
|
+
* path: '/data',
|
|
1227
|
+
* pagination: { enabled: true }
|
|
1228
|
+
* });
|
|
1229
|
+
* const response = await apiRequest.send();
|
|
1230
|
+
* if (response.metadata?.pagination?.occurred) {
|
|
1231
|
+
* console.log(`Retrieved ${response.metadata.pagination.totalItems} items`);
|
|
1232
|
+
* }
|
|
1233
|
+
*
|
|
1234
|
+
* @example
|
|
1235
|
+
* // Request with retry
|
|
1236
|
+
* const apiRequest = new APIRequest({
|
|
1237
|
+
* host: 'api.example.com',
|
|
1238
|
+
* path: '/data',
|
|
1239
|
+
* retry: { enabled: true, maxRetries: 2 }
|
|
1240
|
+
* });
|
|
1241
|
+
* const response = await apiRequest.send();
|
|
1242
|
+
* if (response.metadata?.retries?.occurred) {
|
|
1243
|
+
* console.log(`Succeeded after ${response.metadata.retries.attempts} attempts`);
|
|
1244
|
+
* }
|
|
491
1245
|
*/
|
|
492
1246
|
async send() {
|
|
493
1247
|
|
|
@@ -510,8 +1264,34 @@ class APIRequest {
|
|
|
510
1264
|
}
|
|
511
1265
|
|
|
512
1266
|
/**
|
|
513
|
-
* Process the request
|
|
514
|
-
*
|
|
1267
|
+
* Process the request with GET or POST method. This method handles the actual
|
|
1268
|
+
* HTTP request execution, including retry logic, pagination, and X-Ray tracing.
|
|
1269
|
+
* It creates unique X-Ray subsegments for each request and includes metadata
|
|
1270
|
+
* about retries and pagination in the subsegment annotations.
|
|
1271
|
+
*
|
|
1272
|
+
* @returns {Promise<object>} Promise resolving to the response object
|
|
1273
|
+
* @returns {boolean} return.success - Whether the request was successful
|
|
1274
|
+
* @returns {number} return.statusCode - HTTP status code
|
|
1275
|
+
* @returns {object} return.headers - Response headers
|
|
1276
|
+
* @returns {string} return.body - Response body
|
|
1277
|
+
* @returns {string} return.message - Response message
|
|
1278
|
+
* @returns {object} [return.metadata] - Optional metadata (present if pagination or retries occurred)
|
|
1279
|
+
* @returns {object} [return.metadata.retries] - Retry metadata (if retries occurred)
|
|
1280
|
+
* @returns {object} [return.metadata.pagination] - Pagination metadata (if pagination occurred)
|
|
1281
|
+
*
|
|
1282
|
+
* @example
|
|
1283
|
+
* // Internal usage - typically called via send()
|
|
1284
|
+
* const response = await this.send_get();
|
|
1285
|
+
*
|
|
1286
|
+
* @example
|
|
1287
|
+
* // Response with retry metadata
|
|
1288
|
+
* const response = await this.send_get();
|
|
1289
|
+
* // response.metadata.retries = { occurred: true, attempts: 2, finalAttempt: 2 }
|
|
1290
|
+
*
|
|
1291
|
+
* @example
|
|
1292
|
+
* // Response with pagination metadata
|
|
1293
|
+
* const response = await this.send_get();
|
|
1294
|
+
* // response.metadata.pagination = { occurred: true, totalPages: 5, totalItems: 1000, incomplete: false, error: null }
|
|
515
1295
|
*/
|
|
516
1296
|
async send_get() {
|
|
517
1297
|
|
|
@@ -544,66 +1324,117 @@ class APIRequest {
|
|
|
544
1324
|
try {
|
|
545
1325
|
|
|
546
1326
|
// we will want to follow redirects, so keep submitting until considered complete
|
|
547
|
-
|
|
548
|
-
if (AWSXRay) {
|
|
1327
|
+
if (AWSXRay) {
|
|
549
1328
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const subsegmentName = "APIRequest/" + ((this.getHost()) ? this.getHost() : new URL(this.getURI()).hostname);
|
|
570
|
-
// const parentSegment = AWSXRay.getSegment();
|
|
571
|
-
// const customSegment = new AWSXRay.Segment('APIRequest');
|
|
572
|
-
|
|
573
|
-
await AWSXRay.captureAsyncFunc(subsegmentName, async (subsegment) => {
|
|
574
|
-
// AWSXRay.setSegment(subsegment);
|
|
575
|
-
|
|
576
|
-
try {
|
|
577
|
-
|
|
578
|
-
subsegment.namespace = 'remote';
|
|
579
|
-
|
|
580
|
-
// Add searchable annotations
|
|
581
|
-
subsegment.addAnnotation('request_method', this.getMethod());
|
|
582
|
-
subsegment.addAnnotation('request_host', this.getHost());
|
|
583
|
-
subsegment.addAnnotation('request_uri', this.getURI(false));
|
|
584
|
-
subsegment.addAnnotation('request_note', this.getNote());
|
|
585
|
-
|
|
586
|
-
const result = await _httpGetExecute(options, this, subsegment);
|
|
587
|
-
|
|
588
|
-
subsegment.addAnnotation('success', result ? "true" : "false");
|
|
589
|
-
subsegment.addAnnotation('status_code', this.#response?.statusCode || 500);
|
|
590
|
-
subsegment.addAnnotation('note', this.getNote());
|
|
591
|
-
return result;
|
|
592
|
-
} catch (error) {
|
|
593
|
-
DebugAndLog.error(`Error in APIRequest call to remote endpoint (${this.getNote()}): ${error.message}`, error.stack);
|
|
594
|
-
subsegment.addError(error);
|
|
595
|
-
throw error;
|
|
596
|
-
} finally {
|
|
597
|
-
subsegment.close();
|
|
598
|
-
// AWSXRay.setSegment(parentSegment);
|
|
1329
|
+
// Use timestamp to ensure unique subsegment names for each request
|
|
1330
|
+
const subsegmentName = `APIRequest/${this.getHost()}/${Date.now()}`;
|
|
1331
|
+
|
|
1332
|
+
await AWSXRay.captureAsyncFunc(subsegmentName, async (subsegment) => {
|
|
1333
|
+
|
|
1334
|
+
try {
|
|
1335
|
+
|
|
1336
|
+
subsegment.namespace = 'remote';
|
|
1337
|
+
|
|
1338
|
+
// Add searchable annotations
|
|
1339
|
+
subsegment.addAnnotation('request_method', this.getMethod());
|
|
1340
|
+
subsegment.addAnnotation('request_host', this.getHost());
|
|
1341
|
+
subsegment.addAnnotation('request_uri', this.getURI(false));
|
|
1342
|
+
subsegment.addAnnotation('request_note', this.getNote());
|
|
1343
|
+
|
|
1344
|
+
// Add retry configuration to subsegment metadata (if enabled)
|
|
1345
|
+
if (this.#request.retry?.enabled) {
|
|
1346
|
+
subsegment.addMetadata('retry_config', this.#request.retry);
|
|
599
1347
|
}
|
|
600
|
-
}/*, customSegment*/);
|
|
601
|
-
} else {
|
|
602
|
-
await _httpGetExecute(options, this);
|
|
603
|
-
}
|
|
604
|
-
};
|
|
605
1348
|
|
|
606
|
-
|
|
1349
|
+
// Add pagination configuration to subsegment metadata (if enabled)
|
|
1350
|
+
if (this.#request.pagination?.enabled) {
|
|
1351
|
+
subsegment.addMetadata('pagination_config', this.#request.pagination);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// Use retry handler which handles redirects internally
|
|
1355
|
+
const { response, metadata: retryMetadata } = await this._handleRetries(options, subsegment);
|
|
1356
|
+
|
|
1357
|
+
// Add retry metadata to subsegment annotations and metadata
|
|
1358
|
+
if (retryMetadata.retries?.occurred) {
|
|
1359
|
+
subsegment.addAnnotation('retry_attempts', retryMetadata.retries.attempts);
|
|
1360
|
+
subsegment.addMetadata('retry_details', retryMetadata.retries);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// Handle pagination if enabled
|
|
1364
|
+
const { response: finalResponse, metadata: paginationMetadata } =
|
|
1365
|
+
await this._handlePagination(response);
|
|
1366
|
+
|
|
1367
|
+
// Add pagination metadata to subsegment annotations and metadata
|
|
1368
|
+
if (paginationMetadata.pagination?.occurred) {
|
|
1369
|
+
subsegment.addAnnotation('pagination_pages', paginationMetadata.pagination.totalPages);
|
|
1370
|
+
subsegment.addAnnotation('pagination_items', paginationMetadata.pagination.totalItems);
|
|
1371
|
+
subsegment.addMetadata('pagination_details', paginationMetadata.pagination);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// Store final response and combined metadata
|
|
1375
|
+
this.#response = finalResponse;
|
|
1376
|
+
this.#responseMetadata = {
|
|
1377
|
+
...retryMetadata,
|
|
1378
|
+
...paginationMetadata
|
|
1379
|
+
};
|
|
1380
|
+
|
|
1381
|
+
subsegment.addAnnotation('success', finalResponse.success ? "true" : "false");
|
|
1382
|
+
subsegment.addAnnotation('status_code', finalResponse?.statusCode || 500);
|
|
1383
|
+
subsegment.addAnnotation('note', this.getNote());
|
|
1384
|
+
|
|
1385
|
+
return true;
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
DebugAndLog.error(`Error in APIRequest call to remote endpoint (${this.getNote()}): ${error.message}`, error.stack);
|
|
1388
|
+
subsegment.addError(error);
|
|
1389
|
+
throw error;
|
|
1390
|
+
} finally {
|
|
1391
|
+
subsegment.close();
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
} else {
|
|
1395
|
+
// Use retry handler which handles redirects internally
|
|
1396
|
+
const { response, metadata: retryMetadata } = await this._handleRetries(options, xRayProxy);
|
|
1397
|
+
|
|
1398
|
+
// Handle pagination if enabled
|
|
1399
|
+
const { response: finalResponse, metadata: paginationMetadata } =
|
|
1400
|
+
await this._handlePagination(response);
|
|
1401
|
+
|
|
1402
|
+
// Store final response and combined metadata
|
|
1403
|
+
this.#response = finalResponse;
|
|
1404
|
+
this.#responseMetadata = {
|
|
1405
|
+
...retryMetadata,
|
|
1406
|
+
...paginationMetadata
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// Add metadata to response if retries or pagination occurred
|
|
1411
|
+
if (this.#responseMetadata) {
|
|
1412
|
+
const hasRetries = this.#responseMetadata.retries?.occurred === true;
|
|
1413
|
+
const hasPagination = this.#responseMetadata.pagination?.occurred === true;
|
|
1414
|
+
|
|
1415
|
+
if (hasRetries || hasPagination) {
|
|
1416
|
+
// Create response with metadata
|
|
1417
|
+
const responseWithMetadata = {
|
|
1418
|
+
...this.#response,
|
|
1419
|
+
metadata: {}
|
|
1420
|
+
};
|
|
1421
|
+
|
|
1422
|
+
// Add retry metadata if retries occurred
|
|
1423
|
+
if (hasRetries) {
|
|
1424
|
+
responseWithMetadata.metadata.retries = this.#responseMetadata.retries;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// Add pagination metadata if pagination occurred
|
|
1428
|
+
if (hasPagination) {
|
|
1429
|
+
responseWithMetadata.metadata.pagination = this.#responseMetadata.pagination;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
resolve(responseWithMetadata);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// we now have a completed response (without metadata)
|
|
607
1438
|
resolve( this.#response );
|
|
608
1439
|
}
|
|
609
1440
|
catch (error) {
|
|
@@ -640,21 +1471,47 @@ class APIRequest {
|
|
|
640
1471
|
};
|
|
641
1472
|
|
|
642
1473
|
/**
|
|
643
|
-
* Formats the response for returning to program logic
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
* @param {
|
|
648
|
-
* @param {
|
|
649
|
-
* @
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
*
|
|
655
|
-
*
|
|
656
|
-
*
|
|
657
|
-
* }
|
|
1474
|
+
* Formats the response for returning to program logic. When pagination or retry
|
|
1475
|
+
* features are used, the response may include an optional metadata field with
|
|
1476
|
+
* details about those operations.
|
|
1477
|
+
*
|
|
1478
|
+
* @param {boolean} [success=false] - Whether the request was successful
|
|
1479
|
+
* @param {number} [statusCode=0] - HTTP status code
|
|
1480
|
+
* @param {string|null} [message=null] - Response message
|
|
1481
|
+
* @param {object|null} [headers=null] - Response headers
|
|
1482
|
+
* @param {string|null} [body=null] - Response body
|
|
1483
|
+
* @returns {object} Formatted response object
|
|
1484
|
+
* @returns {boolean} return.success - Whether the request was successful
|
|
1485
|
+
* @returns {number} return.statusCode - HTTP status code
|
|
1486
|
+
* @returns {object|null} return.headers - Response headers
|
|
1487
|
+
* @returns {string|null} return.body - Response body
|
|
1488
|
+
* @returns {string|null} return.message - Response message
|
|
1489
|
+
*
|
|
1490
|
+
* @example
|
|
1491
|
+
* // Basic response format
|
|
1492
|
+
* const response = APIRequest.responseFormat(true, 200, "SUCCESS", headers, body);
|
|
1493
|
+
* // { success: true, statusCode: 200, headers: {...}, body: "...", message: "SUCCESS" }
|
|
1494
|
+
*
|
|
1495
|
+
* @example
|
|
1496
|
+
* // Error response format
|
|
1497
|
+
* const response = APIRequest.responseFormat(false, 500, "Internal Server Error");
|
|
1498
|
+
* // { success: false, statusCode: 500, headers: null, body: null, message: "Internal Server Error" }
|
|
1499
|
+
*
|
|
1500
|
+
* @example
|
|
1501
|
+
* // Response with metadata (added by send() method when pagination/retry occurs)
|
|
1502
|
+
* // Note: metadata is NOT added by this static method, but by the send() method
|
|
1503
|
+
* const response = await apiRequest.send();
|
|
1504
|
+
* // {
|
|
1505
|
+
* // success: true,
|
|
1506
|
+
* // statusCode: 200,
|
|
1507
|
+
* // headers: {...},
|
|
1508
|
+
* // body: "...",
|
|
1509
|
+
* // message: "SUCCESS",
|
|
1510
|
+
* // metadata: {
|
|
1511
|
+
* // retries: { occurred: true, attempts: 2, finalAttempt: 2 },
|
|
1512
|
+
* // pagination: { occurred: true, totalPages: 5, totalItems: 1000, incomplete: false, error: null }
|
|
1513
|
+
* // }
|
|
1514
|
+
* // }
|
|
658
1515
|
*/
|
|
659
1516
|
static responseFormat(success = false, statusCode = 0, message = null, headers = null, body = null) {
|
|
660
1517
|
|