@@ -8,6 +8,8 @@ import { defaultDelayDecider } from "./delayDecider";
8
8
import { defaultRetryDecider } from "./retryDecider" ;
9
9
import { StandardRetryStrategy , RetryQuota } from "./defaultStrategy" ;
10
10
import { getDefaultRetryQuota } from "./defaultRetryQuota" ;
11
+ import { HttpRequest } from "@aws-sdk/protocol-http" ;
12
+ import { v4 } from "uuid" ;
11
13
12
14
jest . mock ( "@aws-sdk/service-error-classification" , ( ) => ( {
13
15
isThrottlingError : jest . fn ( ) . mockReturnValue ( true )
@@ -30,32 +32,43 @@ jest.mock("./defaultRetryQuota", () => {
30
32
return { getDefaultRetryQuota : ( ) => mockDefaultRetryQuota } ;
31
33
} ) ;
32
34
35
+ jest . mock ( "@aws-sdk/protocol-http" , ( ) => ( {
36
+ HttpRequest : {
37
+ isInstance : jest . fn ( ) . mockReturnValue ( false )
38
+ }
39
+ } ) ) ;
40
+
41
+ jest . mock ( "uuid" , ( ) => ( {
42
+ v4 : jest . fn ( ( ) => "42" )
43
+ } ) ) ;
44
+
33
45
describe ( "defaultStrategy" , ( ) => {
46
+ let next : jest . Mock ; // variable for next mock function in utility methods
34
47
const maxAttempts = 3 ;
35
48
36
49
const mockSuccessfulOperation = (
37
50
maxAttempts : number ,
38
51
options ?: { mockResponse ?: string }
39
52
) => {
40
- const next = jest . fn ( ) . mockResolvedValueOnce ( {
53
+ next = jest . fn ( ) . mockResolvedValueOnce ( {
41
54
response : options ?. mockResponse ,
42
55
output : { $metadata : { } }
43
56
} ) ;
44
57
45
58
const retryStrategy = new StandardRetryStrategy ( maxAttempts ) ;
46
- return retryStrategy . retry ( next , { } as any ) ;
59
+ return retryStrategy . retry ( next , { request : { headers : { } } } as any ) ;
47
60
} ;
48
61
49
62
const mockFailedOperation = async (
50
63
maxAttempts : number ,
51
64
options ?: { mockError ?: Error }
52
65
) => {
53
66
const mockError = options ?. mockError ?? new Error ( "mockError" ) ;
54
- const next = jest . fn ( ) . mockRejectedValue ( mockError ) ;
67
+ next = jest . fn ( ) . mockRejectedValue ( mockError ) ;
55
68
56
69
const retryStrategy = new StandardRetryStrategy ( maxAttempts ) ;
57
70
try {
58
- await retryStrategy . retry ( next , { } as any ) ;
71
+ await retryStrategy . retry ( next , { request : { headers : { } } } as any ) ;
59
72
} catch ( error ) {
60
73
expect ( error ) . toStrictEqual ( mockError ) ;
61
74
return error ;
@@ -72,13 +85,13 @@ describe("defaultStrategy", () => {
72
85
output : { $metadata : { } }
73
86
} ;
74
87
75
- const next = jest
88
+ next = jest
76
89
. fn ( )
77
90
. mockRejectedValueOnce ( mockError )
78
91
. mockResolvedValueOnce ( mockResponse ) ;
79
92
80
93
const retryStrategy = new StandardRetryStrategy ( maxAttempts ) ;
81
- return retryStrategy . retry ( next , { } as any ) ;
94
+ return retryStrategy . retry ( next , { request : { headers : { } } } as any ) ;
82
95
} ;
83
96
84
97
const mockSuccessAfterTwoFails = (
@@ -91,14 +104,14 @@ describe("defaultStrategy", () => {
91
104
output : { $metadata : { } }
92
105
} ;
93
106
94
- const next = jest
107
+ next = jest
95
108
. fn ( )
96
109
. mockRejectedValueOnce ( mockError )
97
110
. mockRejectedValueOnce ( mockError )
98
111
. mockResolvedValueOnce ( mockResponse ) ;
99
112
100
113
const retryStrategy = new StandardRetryStrategy ( maxAttempts ) ;
101
- return retryStrategy . retry ( next , { } as any ) ;
114
+ return retryStrategy . retry ( next , { request : { headers : { } } } as any ) ;
102
115
} ;
103
116
104
117
afterEach ( ( ) => {
@@ -423,4 +436,145 @@ describe("defaultStrategy", () => {
423
436
} ) ;
424
437
} ) ;
425
438
} ) ;
439
+
440
+ describe ( "retry informational header: amz-sdk-invocation-id" , ( ) => {
441
+ describe ( "not added if HttpRequest.isInstance returns false" , ( ) => {
442
+ it ( "on successful operation" , async ( ) => {
443
+ await mockSuccessfulOperation ( maxAttempts ) ;
444
+ expect ( next ) . toHaveBeenCalledTimes ( 1 ) ;
445
+ expect (
446
+ next . mock . calls [ 0 ] [ 0 ] . request . headers [ "amz-sdk-invocation-id" ]
447
+ ) . not . toBeDefined ( ) ;
448
+ } ) ;
449
+
450
+ it ( "in case of single failure" , async ( ) => {
451
+ await mockSuccessAfterOneFail ( maxAttempts ) ;
452
+ expect ( next ) . toHaveBeenCalledTimes ( 2 ) ;
453
+ [ 0 , 1 ] . forEach ( index => {
454
+ expect (
455
+ next . mock . calls [ index ] [ 0 ] . request . headers [ "amz-sdk-invocation-id" ]
456
+ ) . not . toBeDefined ( ) ;
457
+ } ) ;
458
+ } ) ;
459
+
460
+ it ( "in case of all failures" , async ( ) => {
461
+ await mockFailedOperation ( maxAttempts ) ;
462
+ expect ( next ) . toHaveBeenCalledTimes ( maxAttempts ) ;
463
+ [ ...Array ( maxAttempts ) . keys ( ) ] . forEach ( index => {
464
+ expect (
465
+ next . mock . calls [ index ] [ 0 ] . request . headers [ "amz-sdk-invocation-id" ]
466
+ ) . not . toBeDefined ( ) ;
467
+ } ) ;
468
+ } ) ;
469
+ } ) ;
470
+
471
+ it ( "uses a unique header for every SDK operation invocation" , async ( ) => {
472
+ const { isInstance } = HttpRequest ;
473
+ ( ( isInstance as unknown ) as jest . Mock ) . mockReturnValue ( true ) ;
474
+
475
+ const uuidForInvocationOne = "uuid-invocation-1" ;
476
+ const uuidForInvocationTwo = "uuid-invocation-2" ;
477
+ ( v4 as jest . Mock )
478
+ . mockReturnValueOnce ( uuidForInvocationOne )
479
+ . mockReturnValueOnce ( uuidForInvocationTwo ) ;
480
+
481
+ const next = jest . fn ( ) . mockResolvedValue ( {
482
+ response : "mockResponse" ,
483
+ output : { $metadata : { } }
484
+ } ) ;
485
+
486
+ const retryStrategy = new StandardRetryStrategy ( maxAttempts ) ;
487
+ await retryStrategy . retry ( next , { request : { headers : { } } } as any ) ;
488
+ await retryStrategy . retry ( next , { request : { headers : { } } } as any ) ;
489
+
490
+ expect ( next ) . toHaveBeenCalledTimes ( 2 ) ;
491
+ expect (
492
+ next . mock . calls [ 0 ] [ 0 ] . request . headers [ "amz-sdk-invocation-id" ]
493
+ ) . toBe ( uuidForInvocationOne ) ;
494
+ expect (
495
+ next . mock . calls [ 1 ] [ 0 ] . request . headers [ "amz-sdk-invocation-id" ]
496
+ ) . toBe ( uuidForInvocationTwo ) ;
497
+
498
+ ( ( isInstance as unknown ) as jest . Mock ) . mockReturnValue ( false ) ;
499
+ } ) ;
500
+
501
+ it ( "uses same value for additional HTTP requests associated with an SDK operation" , async ( ) => {
502
+ const { isInstance } = HttpRequest ;
503
+ ( ( isInstance as unknown ) as jest . Mock ) . mockReturnValueOnce ( true ) ;
504
+
505
+ const uuidForInvocation = "uuid-invocation-1" ;
506
+ ( v4 as jest . Mock ) . mockReturnValueOnce ( uuidForInvocation ) ;
507
+
508
+ await mockSuccessAfterOneFail ( maxAttempts ) ;
509
+
510
+ expect ( next ) . toHaveBeenCalledTimes ( 2 ) ;
511
+ expect (
512
+ next . mock . calls [ 0 ] [ 0 ] . request . headers [ "amz-sdk-invocation-id" ]
513
+ ) . toBe ( uuidForInvocation ) ;
514
+ expect (
515
+ next . mock . calls [ 1 ] [ 0 ] . request . headers [ "amz-sdk-invocation-id" ]
516
+ ) . toBe ( uuidForInvocation ) ;
517
+
518
+ ( ( isInstance as unknown ) as jest . Mock ) . mockReturnValue ( false ) ;
519
+ } ) ;
520
+ } ) ;
521
+
522
+ describe ( "retry informational header: amz-sdk-request" , ( ) => {
523
+ describe ( "not added if HttpRequest.isInstance returns false" , ( ) => {
524
+ it ( "on successful operation" , async ( ) => {
525
+ await mockSuccessfulOperation ( maxAttempts ) ;
526
+ expect ( next ) . toHaveBeenCalledTimes ( 1 ) ;
527
+ expect (
528
+ next . mock . calls [ 0 ] [ 0 ] . request . headers [ "amz-sdk-request" ]
529
+ ) . not . toBeDefined ( ) ;
530
+ } ) ;
531
+
532
+ it ( "in case of single failure" , async ( ) => {
533
+ await mockSuccessAfterOneFail ( maxAttempts ) ;
534
+ expect ( next ) . toHaveBeenCalledTimes ( 2 ) ;
535
+ [ 0 , 1 ] . forEach ( index => {
536
+ expect (
537
+ next . mock . calls [ index ] [ 0 ] . request . headers [ "amz-sdk-request" ]
538
+ ) . not . toBeDefined ( ) ;
539
+ } ) ;
540
+ } ) ;
541
+
542
+ it ( "in case of all failures" , async ( ) => {
543
+ await mockFailedOperation ( maxAttempts ) ;
544
+ expect ( next ) . toHaveBeenCalledTimes ( maxAttempts ) ;
545
+ [ ...Array ( maxAttempts ) . keys ( ) ] . forEach ( index => {
546
+ expect (
547
+ next . mock . calls [ index ] [ 0 ] . request . headers [ "amz-sdk-request" ]
548
+ ) . not . toBeDefined ( ) ;
549
+ } ) ;
550
+ } ) ;
551
+ } ) ;
552
+
553
+ it ( "adds header for each attempt" , async ( ) => {
554
+ const { isInstance } = HttpRequest ;
555
+ ( ( isInstance as unknown ) as jest . Mock ) . mockReturnValue ( true ) ;
556
+
557
+ const mockError = new Error ( "mockError" ) ;
558
+ next = jest . fn ( args => {
559
+ // the header needs to be verified inside jest.Mock as arguments in
560
+ // jest.mocks.calls has the value passed in final call
561
+ const index = next . mock . calls . length - 1 ;
562
+ expect ( args . request . headers [ "amz-sdk-request" ] ) . toBe (
563
+ `attempt=${ index + 1 } ; max=${ maxAttempts } `
564
+ ) ;
565
+ throw mockError ;
566
+ } ) ;
567
+
568
+ const retryStrategy = new StandardRetryStrategy ( maxAttempts ) ;
569
+ try {
570
+ await retryStrategy . retry ( next , { request : { headers : { } } } as any ) ;
571
+ } catch ( error ) {
572
+ expect ( error ) . toStrictEqual ( mockError ) ;
573
+ return error ;
574
+ }
575
+
576
+ expect ( next ) . toHaveBeenCalledTimes ( maxAttempts ) ;
577
+ ( ( isInstance as unknown ) as jest . Mock ) . mockReturnValue ( false ) ;
578
+ } ) ;
579
+ } ) ;
426
580
} ) ;
0 commit comments