Skip to content

Adds support for parsing errors with a top level root XML structure #2060

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/next-release/bugfix-AWSS3Control-1ad7752.json
Original file line number Diff line number Diff line change
@@ -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"
}
10 changes: 10 additions & 0 deletions services/s3control/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@
<artifactId>aws-xml-protocol</artifactId>
<version>${awsjavasdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>aws-query-protocol</artifactId>
<version>${awsjavasdk.version}</version>
</dependency>
Comment on lines +54 to +58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a new dependency, or has it always been there?

Copy link
Contributor Author

@cenedhryn cenedhryn Sep 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a new dependency, on the XmlDomParser

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately it's not backwards-compatible to add new dependencies on existing packages, since it causes Brazil major version conflicts. Is there any way to avoid this new dependency?

<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>protocol-core</artifactId>
Expand Down Expand Up @@ -78,5 +83,10 @@
<version>${awsjavasdk.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Comment on lines +29 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this already implemented in the S3 client?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In S3 the whole client is customized to use the AwsS3ProtocolFactory instead of AwsXmlProtocolFactory. It's not possible to use it dynamically for one or two exception types.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not use the AwsS3ProtocolFactory for all exception types?

*/
@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();
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
software.amazon.awssdk.services.s3control.internal.interceptors.EndpointAddressInterceptor
software.amazon.awssdk.services.s3control.internal.interceptors.PayloadSigningInterceptor
software.amazon.awssdk.services.s3control.internal.interceptors.PayloadSigningInterceptor
software.amazon.awssdk.services.s3control.internal.interceptors.TopLevelXMLErrorInterceptor
Original file line number Diff line number Diff line change
@@ -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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<ErrorResponse>\n"
+ "<Error>\n"
+ "<AccountId>Account-Id</AccountId>\n"
+ "<Code>NoSuchPublicAccessBlockConfiguration</Code>\n"
+ "<Message>The public access block configuration was not found</Message>\n"
+ "</Error>\n"
+ "<RequestId>656c76696e6727732072657175657374</RequestId>\n"
+ "<HostId>Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg==</HostId>\n"
+ "</ErrorResponse>";

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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<ErrorResponse>\n"
+ "<Error>\n"
+ "<AccountId>Account-Id</AccountId>\n"
+ "<Code>NoSuchPublicAccessBlockConfiguration</Code>\n"
+ "<Message>The public access block configuration was not found</Message>\n"
+ "</Error>\n"
+ "<RequestId>656c76696e6727732072657175657374</RequestId>\n"
+ "<HostId>Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg==</HostId>\n"
+ "</ErrorResponse>";

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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<Error>\n"
+ "<Code>InvalidRequest</Code>\n"
+ "<Message>Missing role arn</Message>\n"
+ "<RequestId>656c76696e6727732072657175657374</RequestId>\n"
+ "<HostId>Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg==</HostId>\n"
+ "</Error>";

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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<Error>\n"
+ "<Code>InvalidRequest</Code>\n"
+ "<Message>Missing role arn</Message>\n"
+ "<RequestId>656c76696e6727732072657175657374</RequestId>\n"
+ "<HostId>Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg==</HostId>\n"
+ "</Error>";

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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<Error>\n"
+ "<Code>UnrecognizedCode</Code>\n"
+ "<Message>Error message</Message>\n"
+ "<RequestId>656c76696e6727732072657175657374</RequestId>\n"
+ "<HostId>Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg==</HostId>\n"
+ "</Error>";

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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<Error>\n"
+ "<Code>UnrecognizedCode</Code>\n"
+ "<Message>Error message</Message>\n"
+ "<RequestId>656c76696e6727732072657175657374</RequestId>\n"
+ "<HostId>Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg==</HostId>\n"
+ "</Error>";

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();
}
}
}
Loading