-
Notifications
You must be signed in to change notification settings - Fork 909
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this already implemented in the S3 client? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?