@@ -104,12 +104,17 @@ void verifyScenario(Scenario scenario) {
104104 case RETRY_REQUEST : {
105105 ScenarioTestException scenarioTestException = new ScenarioTestException (response .statusCode ,
106106 response .throttling );
107- RefreshRetryTokenRequest refreshRequest = RefreshRetryTokenRequest .builder ()
108- .failure (scenarioTestException )
109- .isLongPolling (given .isLongPolling )
110- .token (token .get ())
111- .build ();
112- RefreshRetryTokenResponse refreshResponse = strategy .refreshRetryToken (refreshRequest );
107+ RefreshRetryTokenRequest .Builder refreshRequest = RefreshRetryTokenRequest .builder ();
108+
109+ if (response .xAmzRetryAfter != null ) {
110+ refreshRequest .suggestedDelay (response .xAmzRetryAfter );
111+ }
112+
113+ refreshRequest .failure (scenarioTestException )
114+ .isLongPolling (given .isLongPolling )
115+ .token (token .get ())
116+ .build ();
117+ RefreshRetryTokenResponse refreshResponse = strategy .refreshRetryToken (refreshRequest .build ());
113118 DefaultRetryToken refreshedToken = (DefaultRetryToken ) refreshResponse .token ();
114119 token .set (refreshedToken );
115120
@@ -120,14 +125,18 @@ void verifyScenario(Scenario scenario) {
120125 case RETRY_QUOTA_EXCEEDED : {
121126 ScenarioTestException scenarioTestException = new ScenarioTestException (response .statusCode ,
122127 response .throttling );
123- RefreshRetryTokenRequest refreshRequest = RefreshRetryTokenRequest .builder ()
124- . failure ( scenarioTestException )
125- . isLongPolling ( given . isLongPolling )
126- . token ( token . get ())
127- . build ();
128+ RefreshRetryTokenRequest . Builder refreshRequest = RefreshRetryTokenRequest .builder ();
129+
130+ if ( response . xAmzRetryAfter != null ) {
131+ refreshRequest . suggestedDelay ( response . xAmzRetryAfter );
132+ }
128133
134+ refreshRequest .failure (scenarioTestException )
135+ .isLongPolling (given .isLongPolling )
136+ .token (token .get ())
137+ .build ();
129138
130- assertThatThrownBy (() -> strategy .refreshRetryToken (refreshRequest ))
139+ assertThatThrownBy (() -> strategy .refreshRetryToken (refreshRequest . build () ))
131140 .isInstanceOf (TokenAcquisitionFailedException .class )
132141 .matches (e -> {
133142 TokenAcquisitionFailedException acquireException = (TokenAcquisitionFailedException ) e ;
@@ -149,12 +158,18 @@ void verifyScenario(Scenario scenario) {
149158 case MAX_ATTEMPTS_EXCEEDED : {
150159 ScenarioTestException scenarioTestException = new ScenarioTestException (response .statusCode ,
151160 response .throttling );
152- RefreshRetryTokenRequest refreshRequest = RefreshRetryTokenRequest .builder ()
153- .failure (scenarioTestException )
154- .isLongPolling (given .isLongPolling )
155- .token (token .get ())
156- .build ();
157- assertThatThrownBy (() -> strategy .refreshRetryToken (refreshRequest ))
161+ RefreshRetryTokenRequest .Builder refreshRequest = RefreshRetryTokenRequest .builder ();
162+
163+ if (response .xAmzRetryAfter != null ) {
164+ refreshRequest .suggestedDelay (response .xAmzRetryAfter );
165+ }
166+
167+ refreshRequest .failure (scenarioTestException )
168+ .isLongPolling (given .isLongPolling )
169+ .token (token .get ())
170+ .build ();
171+
172+ assertThatThrownBy (() -> strategy .refreshRetryToken (refreshRequest .build ()))
158173 .isInstanceOf (TokenAcquisitionFailedException .class )
159174 .matches (e -> {
160175 TokenAcquisitionFailedException acquireException = (TokenAcquisitionFailedException ) e ;
@@ -673,7 +688,52 @@ private static Stream<Scenario> retriesV21Tests() {
673688 .expected (e ->
674689 e .outcome (Outcome .MAX_ATTEMPTS_EXCEEDED )
675690 .delay (Duration .ZERO )
691+ .retryQuota (486 ))),
692+
693+ aScenario ("Honor x-amz-retry-after Header" )
694+ .newRetries2026 (true )
695+ .addResponse (r ->
696+ r .statusCode (500 )
697+ .xAmzRetryAfter (Duration .ofMillis (1500 ))
698+ .expected (e ->
699+ e .outcome (Outcome .RETRY_REQUEST )
700+ .delay (Duration .ofMillis (1500 ))
676701 .retryQuota (486 )))
702+ .addResponse (r ->
703+ r .statusCode (200 )
704+ .expected (e ->
705+ e .outcome (Outcome .SUCCESS )
706+ .retryQuota (500 ))),
707+
708+ aScenario ("x-amz-retry-after minimum is exponential backoff duration" )
709+ .newRetries2026 (true )
710+ .addResponse (r ->
711+ r .statusCode (500 )
712+ .xAmzRetryAfter (Duration .ofMillis (0 ))
713+ .expected (e ->
714+ e .outcome (Outcome .RETRY_REQUEST )
715+ .delay (Duration .ofMillis (50 ))
716+ .retryQuota (486 )))
717+ .addResponse (r ->
718+ r .statusCode (200 )
719+ .expected (e ->
720+ e .outcome (Outcome .SUCCESS )
721+ .retryQuota (500 ))),
722+
723+ aScenario ("x-amz-retry-after maximum is 5+exponential backoff duration" )
724+ .newRetries2026 (true )
725+ .addResponse (r ->
726+ r .statusCode (500 )
727+ .xAmzRetryAfter (Duration .ofMillis (10000 ))
728+ .expected (e ->
729+ e .outcome (Outcome .RETRY_REQUEST )
730+ .delay (Duration .ofMillis (5050 ))
731+ .retryQuota (486 )))
732+ .addResponse (r ->
733+ r .statusCode (200 )
734+ .expected (e ->
735+ e .outcome (Outcome .SUCCESS )
736+ .retryQuota (500 )))
677737 );
678738 }
679739
@@ -721,6 +781,7 @@ public Given maxBackoff(Duration maxBackoff) {
721781 private static class Response {
722782 private int statusCode ;
723783 private boolean throttling ;
784+ private Duration xAmzRetryAfter ;
724785 private Expected expected ;
725786
726787 public Response statusCode (int statusCode ) {
@@ -733,6 +794,11 @@ public Response isThrottling(boolean throttling) {
733794 return this ;
734795 }
735796
797+ public Response xAmzRetryAfter (Duration xAmzRetryAfter ) {
798+ this .xAmzRetryAfter = xAmzRetryAfter ;
799+ return this ;
800+ }
801+
736802 public Response expected (Consumer <Expected > acceptor ) {
737803 this .expected = new Expected ();
738804 acceptor .accept (this .expected );
0 commit comments