diff --git a/.changes/next-release/bugfix-AWSS3Control-1ad7752.json b/.changes/next-release/bugfix-AWSS3Control-1ad7752.json
new file mode 100644
index 000000000000..42d2da4c751c
--- /dev/null
+++ b/.changes/next-release/bugfix-AWSS3Control-1ad7752.json
@@ -0,0 +1,5 @@
+{
+ "category": "AWS S3 Control",
+ "type": "bugfix",
+ "description": "Adds support for parsing errors with a top level error root XML structure such as InvalidRequest errors"
+}
diff --git a/services/s3control/pom.xml b/services/s3control/pom.xml
index 712ecfd627aa..2583ef06ae17 100644
--- a/services/s3control/pom.xml
+++ b/services/s3control/pom.xml
@@ -51,6 +51,11 @@
aws-xml-protocol
${awsjavasdk.version}
+
+ software.amazon.awssdk
+ aws-query-protocol
+ ${awsjavasdk.version}
+
software.amazon.awssdk
protocol-core
@@ -78,5 +83,10 @@
${awsjavasdk.version}
test
+
+ com.github.tomakehurst
+ wiremock
+ test
+
diff --git a/services/s3control/src/it/java/software.amazon.awssdk.services.s3control/S3ControlIntegrationTest.java b/services/s3control/src/it/java/software.amazon.awssdk.services.s3control/S3ControlIntegrationTest.java
index 48954def06cd..02a7621a23d6 100644
--- a/services/s3control/src/it/java/software.amazon.awssdk.services.s3control/S3ControlIntegrationTest.java
+++ b/services/s3control/src/it/java/software.amazon.awssdk.services.s3control/S3ControlIntegrationTest.java
@@ -29,6 +29,7 @@
import software.amazon.awssdk.http.SdkHttpFullRequest;
import software.amazon.awssdk.services.s3control.model.DeletePublicAccessBlockRequest;
import software.amazon.awssdk.services.s3control.model.GetPublicAccessBlockResponse;
+import software.amazon.awssdk.services.s3control.model.InvalidRequestException;
import software.amazon.awssdk.services.s3control.model.NoSuchPublicAccessBlockConfigurationException;
import software.amazon.awssdk.services.s3control.model.PutPublicAccessBlockResponse;
import software.amazon.awssdk.services.s3control.model.S3ControlException;
@@ -91,6 +92,30 @@ public void putPublicAccessBlock_NoSuchAccount() {
}
}
+ @Test
+ public void describeJob_NotFound() {
+ try {
+ client.describeJob(r -> r.accountId(accountId).jobId("jobid"));
+ fail("Expected exception");
+ } catch (InvalidRequestException e) {
+ assertEquals("InvalidRequest", e.awsErrorDetails().errorCode());
+ assertEquals("Job not found", e.awsErrorDetails().errorMessage());
+ assertNotNull(e.requestId());
+ }
+ }
+
+ @Test
+ public void listJobs_IncorrectStatus() {
+ try {
+ client.listJobs(r -> r.jobStatusesWithStrings("TEST").accountId(accountId));
+ fail("Expected exception");
+ } catch (InvalidRequestException e) {
+ assertEquals("InvalidRequest", e.awsErrorDetails().errorCode());
+ assertEquals("Request invalid", e.awsErrorDetails().errorMessage());
+ assertNotNull(e.requestId());
+ }
+ }
+
@Test
public void getPublicAccessBlock_NoSuchAccount() {
try {
diff --git a/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/interceptors/TopLevelXMLErrorInterceptor.java b/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/interceptors/TopLevelXMLErrorInterceptor.java
new file mode 100644
index 000000000000..7f706fec0af8
--- /dev/null
+++ b/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/interceptors/TopLevelXMLErrorInterceptor.java
@@ -0,0 +1,91 @@
+/*
+ * 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.s3control.internal.interceptors;
+
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
+import software.amazon.awssdk.core.interceptor.Context;
+import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
+import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
+import software.amazon.awssdk.protocols.query.unmarshall.XmlDomParser;
+import software.amazon.awssdk.protocols.query.unmarshall.XmlElement;
+import software.amazon.awssdk.services.s3control.model.InvalidRequestException;
+import software.amazon.awssdk.services.s3control.model.S3ControlException;
+
+/**
+ * Translate S3 style exceptions, which have the Error tag at root instead of wrapped in ErrorResponse.
+ * If the exception follows this structure but isn't known, create an S3ControlException with the
+ * error code and message.
+ */
+@SdkInternalApi
+public final class TopLevelXMLErrorInterceptor implements ExecutionInterceptor {
+
+ private static final String XML_ERROR_ROOT = "Error";
+ private static final String XML_ELEMENT_CODE = "Code";
+ private static final String XML_ELEMENT_MESSAGE = "Message";
+
+ private static final String INVALID_REQUEST_CODE = "InvalidRequest";
+
+ @Override
+ public Throwable modifyException(Context.FailedExecution context, ExecutionAttributes executionAttributes) {
+ S3ControlException exception = (S3ControlException) (context.exception());
+ AwsErrorDetails awsErrorDetails = exception.awsErrorDetails();
+
+ if (!(exception.getMessage().contains("null"))) {
+ return context.exception();
+ }
+
+ XmlElement errorXml = XmlDomParser.parse(awsErrorDetails.rawResponse().asInputStream());
+ if (!XML_ERROR_ROOT.equals(errorXml.elementName())) {
+ return context.exception();
+ }
+
+ String errorCode = getChildElement(errorXml, XML_ELEMENT_CODE);
+ String errorMessage = getChildElement(errorXml, XML_ELEMENT_MESSAGE);
+
+ S3ControlException.Builder builder = findMatchingBuilder(errorCode);
+ copyErrorDetails(exception, builder);
+ return builder
+ .message(errorMessage)
+ .awsErrorDetails(copyAwsErrorDetails(awsErrorDetails, errorCode, errorMessage))
+ .build();
+ }
+
+ private String getChildElement(XmlElement root, String elementName) {
+ return root.getOptionalElementByName(elementName)
+ .map(XmlElement::textContent)
+ .orElse(null);
+ }
+
+ private S3ControlException.Builder findMatchingBuilder(String errorCode) {
+ return INVALID_REQUEST_CODE.equals(errorCode) ?
+ InvalidRequestException.builder() :
+ S3ControlException.builder();
+ }
+
+ private void copyErrorDetails(S3ControlException exception, S3ControlException.Builder builder) {
+ builder.cause(exception.getCause());
+ builder.requestId(exception.requestId());
+ builder.extendedRequestId(exception.extendedRequestId());
+ }
+
+ private AwsErrorDetails copyAwsErrorDetails(AwsErrorDetails original, String errorCode, String errorMessage) {
+ return original.toBuilder()
+ .errorMessage(errorMessage)
+ .errorCode(errorCode)
+ .build();
+ }
+}
diff --git a/services/s3control/src/main/resources/software/amazon/awssdk/services/s3control/execution.interceptors b/services/s3control/src/main/resources/software/amazon/awssdk/services/s3control/execution.interceptors
index b19e9f79a356..e6f9225c634e 100644
--- a/services/s3control/src/main/resources/software/amazon/awssdk/services/s3control/execution.interceptors
+++ b/services/s3control/src/main/resources/software/amazon/awssdk/services/s3control/execution.interceptors
@@ -1,2 +1,3 @@
software.amazon.awssdk.services.s3control.internal.interceptors.EndpointAddressInterceptor
-software.amazon.awssdk.services.s3control.internal.interceptors.PayloadSigningInterceptor
\ No newline at end of file
+software.amazon.awssdk.services.s3control.internal.interceptors.PayloadSigningInterceptor
+software.amazon.awssdk.services.s3control.internal.interceptors.TopLevelXMLErrorInterceptor
\ No newline at end of file
diff --git a/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/functionaltests/XMLErrorTypesTranslationFunctionalTest.java b/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/functionaltests/XMLErrorTypesTranslationFunctionalTest.java
new file mode 100644
index 000000000000..9680e84073bf
--- /dev/null
+++ b/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/functionaltests/XMLErrorTypesTranslationFunctionalTest.java
@@ -0,0 +1,232 @@
+/*
+ * 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.s3control.functionaltests;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.any;
+import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.net.URI;
+import java.util.concurrent.CompletionException;
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import org.junit.Rule;
+import org.junit.Test;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.core.interceptor.Context;
+import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
+import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
+import software.amazon.awssdk.http.SdkHttpRequest;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3control.S3ControlAsyncClient;
+import software.amazon.awssdk.services.s3control.S3ControlAsyncClientBuilder;
+import software.amazon.awssdk.services.s3control.S3ControlClient;
+import software.amazon.awssdk.services.s3control.S3ControlClientBuilder;
+import software.amazon.awssdk.services.s3control.model.InvalidRequestException;
+import software.amazon.awssdk.services.s3control.model.NoSuchPublicAccessBlockConfigurationException;
+import software.amazon.awssdk.services.s3control.model.S3ControlException;
+
+public class XMLErrorTypesTranslationFunctionalTest {
+
+ private static final URI HTTP_LOCALHOST_URI = URI.create("http://localhost:8080/");
+
+ @Rule
+ public WireMockRule wireMock = new WireMockRule();
+
+ private S3ControlClientBuilder getSyncClientBuilder() {
+ return S3ControlClient.builder()
+ .region(Region.US_EAST_1)
+ .overrideConfiguration(c -> c.addExecutionInterceptor(new LocalhostEndpointAddressInterceptor()))
+ .credentialsProvider(
+ StaticCredentialsProvider.create(
+ AwsBasicCredentials.create("key", "secret")));
+ }
+
+ private S3ControlAsyncClientBuilder getAsyncClientBuilder() {
+ return S3ControlAsyncClient.builder()
+ .region(Region.US_EAST_1)
+ .overrideConfiguration(c -> c.addExecutionInterceptor(new LocalhostEndpointAddressInterceptor()))
+ .credentialsProvider(
+ StaticCredentialsProvider.create(
+ AwsBasicCredentials.create("key", "secret")));
+ }
+
+ @Test
+ public void standardErrorXML_translated_correctly_with_syncClient() {
+ String accountId = "Account-Id";
+ String xmlResponseBody = "\n"
+ + "\n"
+ + "\n"
+ + "Account-Id\n"
+ + "NoSuchPublicAccessBlockConfiguration
\n"
+ + "The public access block configuration was not found\n"
+ + "\n"
+ + "656c76696e6727732072657175657374\n"
+ + "Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg==\n"
+ + "";
+
+ stubFor(any(anyUrl()).willReturn(aResponse().withStatus(400).withBody(xmlResponseBody)));
+
+ S3ControlClient s3Client = getSyncClientBuilder().build();
+
+ assertThatThrownBy(() -> s3Client.getPublicAccessBlock(r -> r.accountId(accountId)))
+ .isInstanceOf(S3ControlException.class)
+ .isInstanceOf(NoSuchPublicAccessBlockConfigurationException.class)
+ .satisfies(e -> assertThat(((S3ControlException) e).awsErrorDetails().errorCode())
+ .isEqualTo("NoSuchPublicAccessBlockConfiguration"))
+ .satisfies(e -> assertThat(((S3ControlException) e).awsErrorDetails().errorMessage()).contains("block"));
+ }
+
+ @Test
+ public void standardErrorXML_translated_correctly_with_asyncClient() {
+ String accountId = "Account-Id";
+ String xmlResponseBody = "\n"
+ + "\n"
+ + "\n"
+ + "Account-Id\n"
+ + "NoSuchPublicAccessBlockConfiguration
\n"
+ + "The public access block configuration was not found\n"
+ + "\n"
+ + "656c76696e6727732072657175657374\n"
+ + "Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg==\n"
+ + "";
+
+ stubFor(any(anyUrl()).willReturn(aResponse().withStatus(400).withBody(xmlResponseBody)));
+
+ S3ControlAsyncClient s3Client = getAsyncClientBuilder().build();
+
+ assertThatThrownBy(() -> s3Client.createJob(r -> r.accountId(accountId)).join())
+ .isInstanceOf(CompletionException.class)
+ .hasCauseExactlyInstanceOf(NoSuchPublicAccessBlockConfigurationException.class)
+ .satisfies(e -> {
+ S3ControlException s3ControlException = (S3ControlException) e.getCause();
+ assertThat(s3ControlException.awsErrorDetails().errorCode())
+ .isEqualTo("NoSuchPublicAccessBlockConfiguration");
+ assertThat(s3ControlException.awsErrorDetails().errorMessage()).contains("block");
+ });
+ }
+
+ @Test
+ public void xmlRootError_specificException_translated_correctly_with_syncClient() {
+ String accountId = "Account-Id";
+ String xmlResponseBody = "\n"
+ + "\n"
+ + "InvalidRequest
\n"
+ + "Missing role arn\n"
+ + "656c76696e6727732072657175657374\n"
+ + "Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg==\n"
+ + "";
+
+ stubFor(any(anyUrl()).willReturn(aResponse().withStatus(400).withBody(xmlResponseBody)));
+
+ S3ControlClient s3Client = getSyncClientBuilder().build();
+
+ assertThatThrownBy(() -> s3Client.createJob(r -> r.accountId(accountId)))
+ .isInstanceOf(S3ControlException.class)
+ .isInstanceOf(InvalidRequestException.class)
+ .satisfies(e -> assertThat(((S3ControlException) e).awsErrorDetails().errorCode()).isEqualTo("InvalidRequest"))
+ .satisfies(e -> assertThat(((S3ControlException) e).awsErrorDetails().errorMessage()).isEqualTo("Missing role arn"));
+ }
+
+ @Test
+ public void xmlRootError_specificException_translated_correctly_with_asyncClient() {
+ String accountId = "Account-Id";
+ String xmlResponseBody = "\n"
+ + "\n"
+ + "InvalidRequest
\n"
+ + "Missing role arn\n"
+ + "656c76696e6727732072657175657374\n"
+ + "Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg==\n"
+ + "";
+
+ stubFor(any(anyUrl()).willReturn(aResponse().withStatus(400).withBody(xmlResponseBody)));
+
+ S3ControlAsyncClient s3Client = getAsyncClientBuilder().build();
+
+ assertThatThrownBy(() -> s3Client.createJob(r -> r.accountId(accountId)).join())
+ .isInstanceOf(CompletionException.class)
+ .hasCauseInstanceOf(S3ControlException.class)
+ .hasCauseInstanceOf(InvalidRequestException.class)
+ .satisfies(e -> {
+ S3ControlException s3ControlException = (S3ControlException) e.getCause();
+ assertThat(s3ControlException.awsErrorDetails().errorCode()).isEqualTo("InvalidRequest");
+ assertThat(s3ControlException.awsErrorDetails().errorMessage()).isEqualTo("Missing role arn");
+ });
+ }
+
+ @Test
+ public void xmlRootError_unknownException_translated_correctly_with_syncClient() {
+ String accountId = "Account-Id";
+ String xmlResponseBody = "\n"
+ + "\n"
+ + "UnrecognizedCode
\n"
+ + "Error message\n"
+ + "656c76696e6727732072657175657374\n"
+ + "Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg==\n"
+ + "";
+
+ stubFor(any(anyUrl()).willReturn(aResponse().withStatus(400).withBody(xmlResponseBody)));
+
+ S3ControlClient s3Client = getSyncClientBuilder().build();
+
+ assertThatThrownBy(() -> s3Client.createJob(r -> r.accountId(accountId)))
+ .isInstanceOf(S3ControlException.class)
+ .isNotInstanceOf(InvalidRequestException.class)
+ .satisfies(e -> assertThat(((S3ControlException) e).awsErrorDetails().errorCode()).isEqualTo("UnrecognizedCode"))
+ .satisfies(e -> assertThat(((S3ControlException) e).awsErrorDetails().errorMessage()).isEqualTo("Error message"));
+ }
+
+ @Test
+ public void xmlRootError_unknownException_translated_correctly_with_asyncClient() {
+ String accountId = "Account-Id";
+ String xmlResponseBody = "\n"
+ + "\n"
+ + "UnrecognizedCode
\n"
+ + "Error message\n"
+ + "656c76696e6727732072657175657374\n"
+ + "Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg==\n"
+ + "";
+
+ stubFor(any(anyUrl()).willReturn(aResponse().withStatus(400).withBody(xmlResponseBody)));
+
+ S3ControlAsyncClient s3Client = getAsyncClientBuilder().build();
+
+ assertThatThrownBy(() -> s3Client.createJob(r -> r.accountId(accountId)).join())
+ .isInstanceOf(CompletionException.class)
+ .hasCauseExactlyInstanceOf(S3ControlException.class)
+ .satisfies(e -> {
+ S3ControlException s3ControlException = (S3ControlException) e.getCause();
+ assertThat(s3ControlException.awsErrorDetails().errorCode()).isEqualTo("UnrecognizedCode");
+ assertThat(s3ControlException.awsErrorDetails().errorMessage()).isEqualTo("Error message");
+ });
+ }
+
+ private static final class LocalhostEndpointAddressInterceptor implements ExecutionInterceptor {
+
+ @Override
+ public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) {
+ return context.httpRequest()
+ .toBuilder()
+ .protocol(HTTP_LOCALHOST_URI.getScheme())
+ .host(HTTP_LOCALHOST_URI.getHost())
+ .port(HTTP_LOCALHOST_URI.getPort())
+ .build();
+ }
+ }
+}
diff --git a/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/interceptors/TopLevelXMLErrorInterceptorTest.java b/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/interceptors/TopLevelXMLErrorInterceptorTest.java
new file mode 100644
index 000000000000..d81097f7cbb0
--- /dev/null
+++ b/services/s3control/src/test/java/software/amazon/awssdk/services/s3control/internal/interceptors/TopLevelXMLErrorInterceptorTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.s3control.internal.interceptors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Optional;
+import org.junit.Test;
+import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.core.SdkBytes;
+import software.amazon.awssdk.core.SdkRequest;
+import software.amazon.awssdk.core.SdkResponse;
+import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
+import software.amazon.awssdk.http.SdkHttpRequest;
+import software.amazon.awssdk.http.SdkHttpResponse;
+import software.amazon.awssdk.services.s3control.model.InvalidRequestException;
+import software.amazon.awssdk.services.s3control.model.S3ControlException;
+
+public class TopLevelXMLErrorInterceptorTest {
+
+ @Test
+ public void when_correctlyParsedException_returnsExceptionUnmodified() {
+ AwsServiceException originalException = S3ControlException.builder()
+ .message("This is a correctly parsed error")
+ .build();
+ TopLevelXMLErrorInterceptor interceptor = new TopLevelXMLErrorInterceptor();
+ Throwable translatedException = interceptor.modifyException(new Context(originalException), new ExecutionAttributes());
+ assertThat(translatedException).isEqualTo(originalException);
+ }
+
+ @Test
+ public void when_incorrectlyParsedException_wrongXMLStructure_returnsExceptionUnmodified() {
+ String xmlResponseBody = "\n"
+ + "\n"
+ + "Value\n"
+ + "";
+ AwsErrorDetails awsErrorDetails = AwsErrorDetails.builder()
+ .rawResponse(SdkBytes.fromUtf8String(xmlResponseBody))
+ .build();
+
+ AwsServiceException originalException = S3ControlException.builder()
+ .message("Error message with null")
+ .awsErrorDetails(awsErrorDetails)
+ .build();
+ TopLevelXMLErrorInterceptor interceptor = new TopLevelXMLErrorInterceptor();
+ Throwable translatedException = interceptor.modifyException(new Context(originalException), new ExecutionAttributes());
+ assertThat(translatedException).isEqualTo(originalException);
+ }
+
+ @Test
+ public void when_incorrectlyParsedException_correctXMLStructure_returnsSpecificException() {
+ String xmlResponseBody = "\n"
+ + "\n"
+ + "InvalidRequest
\n"
+ + "Missing role arn\n"
+ + "656c76696e6727732072657175657374\n"
+ + "Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg==\n"
+ + "";
+ AwsErrorDetails awsErrorDetails = AwsErrorDetails.builder()
+ .rawResponse(SdkBytes.fromUtf8String(xmlResponseBody))
+ .build();
+
+ AwsServiceException originalException = S3ControlException.builder()
+ .message("Error message with null")
+ .awsErrorDetails(awsErrorDetails)
+ .build();
+ TopLevelXMLErrorInterceptor interceptor = new TopLevelXMLErrorInterceptor();
+ Throwable translatedException = interceptor.modifyException(new Context(originalException), new ExecutionAttributes());
+ assertThat(translatedException).isNotEqualTo(originalException);
+ assertThat(translatedException).isInstanceOf(InvalidRequestException.class);
+ InvalidRequestException s3ControlException = (InvalidRequestException) translatedException;
+ assertThat(s3ControlException.awsErrorDetails().errorCode()).isEqualTo("InvalidRequest");
+ assertThat(s3ControlException.awsErrorDetails().errorMessage()).isEqualTo("Missing role arn");
+ }
+
+ @Test
+ public void when_incorrectlyParsedException_correctXMLStructure_returnsGenericException() {
+ String xmlResponseBody = "\n"
+ + "\n"
+ + "SomeOtherException
\n"
+ + "The exception message\n"
+ + "656c76696e6727732072657175657374\n"
+ + "Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg==\n"
+ + "";
+ AwsErrorDetails awsErrorDetails = AwsErrorDetails.builder()
+ .rawResponse(SdkBytes.fromUtf8String(xmlResponseBody))
+ .build();
+
+ AwsServiceException originalException = S3ControlException.builder()
+ .message("Error message with null")
+ .awsErrorDetails(awsErrorDetails)
+ .build();
+ TopLevelXMLErrorInterceptor interceptor = new TopLevelXMLErrorInterceptor();
+ Throwable translatedException = interceptor.modifyException(new Context(originalException), new ExecutionAttributes());
+ assertThat(translatedException).isNotEqualTo(originalException);
+ assertThat(translatedException).isInstanceOf(S3ControlException.class);
+ S3ControlException s3ControlException = (S3ControlException) translatedException;
+ assertThat(s3ControlException.awsErrorDetails().errorCode()).isEqualTo("SomeOtherException");
+ assertThat(s3ControlException.awsErrorDetails().errorMessage()).isEqualTo("The exception message");
+ }
+
+ public static final class Context implements software.amazon.awssdk.core.interceptor.Context.FailedExecution {
+
+ private final Throwable exception;
+
+ public Context(Throwable exception) {
+ this.exception = exception;
+ }
+
+ @Override
+ public Throwable exception() { return exception; }
+
+ @Override
+ public SdkRequest request() {
+ return null;
+ }
+
+ @Override
+ public Optional response() { return Optional.empty(); }
+
+ @Override
+ public Optional httpRequest() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional httpResponse() {
+ return Optional.empty();
+ }
+
+ }
+}
diff --git a/services/s3control/src/test/resources/log4j.properties b/services/s3control/src/test/resources/log4j.properties
new file mode 100644
index 000000000000..9aad44287cbe
--- /dev/null
+++ b/services/s3control/src/test/resources/log4j.properties
@@ -0,0 +1,33 @@
+#
+# 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.
+#
+
+log4j.rootLogger=WARN, A1
+log4j.appender.A1=org.apache.log4j.ConsoleAppender
+log4j.appender.A1.layout=org.apache.log4j.PatternLayout
+
+# Print the date in ISO 8601 format
+log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c - %m%n
+
+# Adjust to see more / less logging
+#log4j.logger.com.amazonaws.ec2=DEBUG
+
+# HttpClient 3 Wire Logging
+#log4j.logger.httpclient.wire=DEBUG
+
+# HttpClient 4 Wire Logging
+#log4j.logger.org.apache.http.wire=DEBUG
+log4j.logger.org.apache.http=DEBUG
+log4j.logger.org.apache.http.wire=DEBUG
+log4j.logger.software.amazon.awssdk=WARN