diff --git a/.changes/next-release/feature-AWSSDKforJavav2-05b771b.json b/.changes/next-release/feature-AWSSDKforJavav2-05b771b.json
new file mode 100644
index 000000000000..c47b650486d3
--- /dev/null
+++ b/.changes/next-release/feature-AWSSDKforJavav2-05b771b.json
@@ -0,0 +1,6 @@
+{
+ "category": "AWS SDK for Java v2",
+ "contributor": "",
+ "type": "feature",
+ "description": "When raising an exception as a result of a service response, if service does not return a error message, include the error code or HTTP status code in exception messages instead of the string \"null\"."
+}
diff --git a/.changes/next-release/feature-AWSSDKforJavav2-2636db0.json b/.changes/next-release/feature-AWSSDKforJavav2-2636db0.json
new file mode 100644
index 000000000000..7379fb397fd8
--- /dev/null
+++ b/.changes/next-release/feature-AWSSDKforJavav2-2636db0.json
@@ -0,0 +1,6 @@
+{
+ "category": "AWS SDK for Java v2",
+ "contributor": "",
+ "type": "feature",
+ "description": "When a request fails after SDK retries, include the retried failure messages as \"suppressed\" exceptions. Stack traces for these suppressed exceptions are not preserved."
+}
diff --git a/.idea/inspectionProfiles/AWS_Java_SDK_2_0.xml b/.idea/inspectionProfiles/AWS_Java_SDK_2_0.xml
index 525cdfe666aa..95ed390c8a7c 100644
--- a/.idea/inspectionProfiles/AWS_Java_SDK_2_0.xml
+++ b/.idea/inspectionProfiles/AWS_Java_SDK_2_0.xml
@@ -65,7 +65,7 @@
-
+
@@ -189,10 +189,6 @@
-
-
-
-
@@ -238,7 +234,6 @@
-
@@ -284,7 +279,7 @@
-
+
@@ -323,10 +318,7 @@
-
-
-
-
+
@@ -372,7 +364,6 @@
-
@@ -392,7 +383,6 @@
-
@@ -458,7 +448,7 @@
-
+
diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/model/ExceptionProperties.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/model/ExceptionProperties.java
index 3a25f227bd44..f89cb6656564 100644
--- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/model/ExceptionProperties.java
+++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/model/ExceptionProperties.java
@@ -33,7 +33,8 @@ public static List builderInterfaceMethods(ClassName className) {
builderMethod(className, "message", String.class),
builderMethod(className, "requestId", String.class),
builderMethod(className, "statusCode", int.class),
- builderMethod(className, "cause", Throwable.class));
+ builderMethod(className, "cause", Throwable.class),
+ builderMethod(className, "writableStackTrace", Boolean.class));
}
public static List builderImplMethods(ClassName className) {
@@ -42,7 +43,8 @@ public static List builderImplMethods(ClassName className) {
builderImplMethods(className, "message", String.class),
builderImplMethods(className, "requestId", String.class),
builderImplMethods(className, "statusCode", int.class),
- builderImplMethods(className, "cause", Throwable.class));
+ builderImplMethods(className, "cause", Throwable.class),
+ builderImplMethods(className, "writableStackTrace", Boolean.class));
}
private static MethodSpec builderMethod(ClassName className, String name, Class clazz) {
diff --git a/codegen/src/main/resources/software/amazon/awssdk/codegen/rules/SourceException.java.resource b/codegen/src/main/resources/software/amazon/awssdk/codegen/rules/SourceException.java.resource
index 47c6cb2862e9..9cbf91c193f9 100644
--- a/codegen/src/main/resources/software/amazon/awssdk/codegen/rules/SourceException.java.resource
+++ b/codegen/src/main/resources/software/amazon/awssdk/codegen/rules/SourceException.java.resource
@@ -15,6 +15,9 @@ public class SourceException extends SdkException {
@Override
Builder cause(Throwable cause);
+ @Override
+ Builder writableStackTrace(Boolean writableStackTrace);
+
@Override
Builder message(String message);
@@ -35,6 +38,12 @@ public class SourceException extends SdkException {
return this;
}
+ @Override
+ public Builder writableStackTrace(Boolean writableStackTrace) {
+ super.writableStackTrace(writableStackTrace);
+ return this;
+ }
+
@Override
public SourceException build() {
return new SourceException(this);
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/model/baseserviceexception.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/model/baseserviceexception.java
index d2c420e3dfe7..271613702760 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/model/baseserviceexception.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/model/baseserviceexception.java
@@ -37,6 +37,9 @@ public interface Builder extends AwsServiceException.Builder {
@Override
Builder cause(Throwable cause);
+
+ @Override
+ Builder writableStackTrace(Boolean writableStackTrace);
}
protected static class BuilderImpl extends AwsServiceException.BuilderImpl implements Builder {
@@ -77,6 +80,12 @@ public BuilderImpl cause(Throwable cause) {
return this;
}
+ @Override
+ public BuilderImpl writableStackTrace(Boolean writableStackTrace) {
+ this.writableStackTrace = writableStackTrace;
+ return this;
+ }
+
@Override
public JsonProtocolTestsException build() {
return new JsonProtocolTestsException(this);
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/model/emptymodeledexception.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/model/emptymodeledexception.java
index 1d060ad9baf2..375a03a4a6c4 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/model/emptymodeledexception.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/model/emptymodeledexception.java
@@ -56,6 +56,9 @@ public interface Builder extends SdkPojo, CopyableBuilder> future) {
}
responseFuture.whenComplete((response, exception) -> {
-
if (exception != null) {
if (exception instanceof Exception) {
maybeRetryExecute(future, (Exception) exception);
diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/RetryableStageHelper.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/RetryableStageHelper.java
index 5f3ed77659a7..e9511e9b0743 100644
--- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/RetryableStageHelper.java
+++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/RetryableStageHelper.java
@@ -16,6 +16,8 @@
package software.amazon.awssdk.core.internal.http.pipeline.stages.utils;
import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
import java.util.OptionalDouble;
import java.util.concurrent.CompletionException;
import software.amazon.awssdk.annotations.SdkInternalApi;
@@ -57,6 +59,7 @@ public class RetryableStageHelper {
private final RetryPolicy retryPolicy;
private final RateLimitingTokenBucket rateLimitingTokenBucket;
private final HttpClientDependencies dependencies;
+ private final List exceptionMessageHistory = new ArrayList<>();
private int attemptNumber = 0;
private SdkHttpResponse lastResponse = null;
@@ -116,6 +119,14 @@ public boolean retryPolicyAllowsRetry() {
*/
public SdkException retryPolicyDisallowedRetryException() {
context.executionContext().metricCollector().reportMetric(CoreMetric.RETRY_COUNT, retriesAttemptedSoFar(true));
+ for (int i = 0; i < exceptionMessageHistory.size() - 1; i++) {
+ SdkClientException pastException =
+ SdkClientException.builder()
+ .message("Request attempt " + (i + 1) + " failure: " + exceptionMessageHistory.get(i))
+ .writableStackTrace(false)
+ .build();
+ lastException.addSuppressed(pastException);
+ }
return lastException;
}
@@ -207,9 +218,11 @@ public void setLastException(Throwable lastException) {
setLastException(lastException.getCause());
} else if (lastException instanceof SdkException) {
this.lastException = (SdkException) lastException;
+ exceptionMessageHistory.add(this.lastException.getMessage());
} else {
this.lastException = SdkClientException.create("Unable to execute HTTP request: " + lastException.getMessage(),
lastException);
+ exceptionMessageHistory.add(this.lastException.getMessage());
}
}
diff --git a/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/ExpiredTokenException.java b/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/ExpiredTokenException.java
index 9284bcace31e..d8d6ad63785d 100644
--- a/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/ExpiredTokenException.java
+++ b/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/ExpiredTokenException.java
@@ -53,6 +53,9 @@ public interface Builder extends SdkPojo, SdkClientException.Builder {
@Override
Builder cause(Throwable cause);
+ @Override
+ Builder writableStackTrace(Boolean writableStackTrace);
+
@Override
ExpiredTokenException build();
}
@@ -77,6 +80,12 @@ public BuilderImpl cause(Throwable cause) {
return this;
}
+ @Override
+ public BuilderImpl writableStackTrace(Boolean writableStackTrace) {
+ this.writableStackTrace = writableStackTrace;
+ return this;
+ }
+
@Override
public ExpiredTokenException build() {
return new ExpiredTokenException(this);
diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/AsyncRetryFailureTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/AsyncRetryFailureTest.java
new file mode 100644
index 000000000000..2e5d4106e3c6
--- /dev/null
+++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/AsyncRetryFailureTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.services.retry;
+
+import java.net.URI;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.core.retry.RetryMode;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonAsyncClient;
+import software.amazon.awssdk.testutils.service.http.MockAsyncHttpClient;
+
+public class AsyncRetryFailureTest extends RetryFailureTestSuite {
+ private final ProtocolRestJsonAsyncClient client;
+
+ public AsyncRetryFailureTest() {
+ super(new MockAsyncHttpClient());
+ client = ProtocolRestJsonAsyncClient.builder()
+ .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid")))
+ .region(Region.US_EAST_1)
+ .endpointOverride(URI.create("http://localhost"))
+ .httpClient(mockHttpClient)
+ .build();
+ }
+
+ @Override
+ protected void callAllTypesOperation() {
+ client.allTypes().join();
+ }
+}
diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/RetryFailureTestSuite.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/RetryFailureTestSuite.java
new file mode 100644
index 000000000000..f6edbf8d2524
--- /dev/null
+++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/RetryFailureTestSuite.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.services.retry;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+import java.util.concurrent.CompletionException;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import software.amazon.awssdk.core.exception.SdkException;
+import software.amazon.awssdk.http.HttpExecuteResponse;
+import software.amazon.awssdk.http.SdkHttpRequest;
+import software.amazon.awssdk.http.SdkHttpResponse;
+import software.amazon.awssdk.testutils.service.http.MockHttpClient;
+
+/**
+ * A set of tests that verify the behavior of the SDK when retries are exhausted.
+ */
+public abstract class RetryFailureTestSuite {
+ protected final T mockHttpClient;
+
+ protected RetryFailureTestSuite(T mockHttpClient) {
+ this.mockHttpClient = mockHttpClient;
+ }
+
+ @BeforeEach
+ public void setupClient() {
+ mockHttpClient.reset();
+ }
+
+ protected abstract void callAllTypesOperation();
+
+ @Test
+ public void clientSideErrorsIncludeSuppressedExceptions() {
+ mockHttpClient.stubResponses(retryableFailure(),
+ retryableFailure(),
+ nonRetryableFailure());
+
+ try {
+ callAllTypesOperation();
+ } catch (Throwable e) {
+ if (e instanceof CompletionException) {
+ e = e.getCause();
+ }
+ e.printStackTrace();
+
+ assertThat(e.getSuppressed()).hasSize(2)
+ .allSatisfy(t -> assertThat(t.getMessage()).contains("500"));
+ }
+ }
+
+ private HttpExecuteResponse retryableFailure() {
+ return HttpExecuteResponse.builder()
+ .response(SdkHttpResponse.builder()
+ .statusCode(500)
+ .putHeader("content-length", "0")
+ .build())
+ .build();
+ }
+
+ private HttpExecuteResponse nonRetryableFailure() {
+ return HttpExecuteResponse.builder()
+ .response(SdkHttpResponse.builder()
+ .statusCode(400)
+ .putHeader("content-length", "0")
+ .build())
+ .build();
+ }
+}
diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/SyncRetryFailureTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/SyncRetryFailureTest.java
new file mode 100644
index 000000000000..9c914297a1d5
--- /dev/null
+++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/SyncRetryFailureTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.services.retry;
+
+import java.net.URI;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.core.retry.RetryMode;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonClient;
+import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient;
+
+public class SyncRetryFailureTest extends RetryFailureTestSuite {
+ private final ProtocolRestJsonClient client;
+
+ public SyncRetryFailureTest() {
+ super(new MockSyncHttpClient());
+ client = ProtocolRestJsonClient.builder()
+ .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid",
+ "skid")))
+ .region(Region.US_EAST_1)
+ .endpointOverride(URI.create("http://localhost"))
+ .httpClient(mockHttpClient)
+ .build();
+ }
+
+ @Override
+ protected void callAllTypesOperation() {
+ client.allTypes();
+ }
+}