Skip to content
6 changes: 6 additions & 0 deletions .changes/next-release/bugfix-AWSSDKforJavaV2-2300783.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "bugfix",
"category": "AWS SDK for Java V2",
"contributor": "",
"description": "Ensure rpc 1.0/1.1 error code parsing matches smithy spec: use both __type and code fields and handle uris in body error codes."
}
6 changes: 6 additions & 0 deletions .changes/next-release/bugfix-AWSSDKforJavav2-bfa9190.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "bugfix",
"category": "AWS SDK for Java v2",
"contributor": "",
"description": "Don't use the value of AwsQueryError in json rpc/smithy-rpc-v2-cbor protocols."
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,14 @@ private String getErrorCode(String errorShapeName) {
}

private boolean isErrorCodeOverridden(ErrorTrait errorTrait) {
return errorTrait != null && !Utils.isNullOrEmpty(errorTrait.getCode());
return protocolSupportsErrorCodeOverride() && errorTrait != null && !Utils.isNullOrEmpty(errorTrait.getCode());
}

// JSON (ie non-rest) and smithy-rpc-v2-cbor should ignore the AwsQueryError trait
// error code on the deserialized exceptions is parsed based on the awsQueryCompatible trait in:
// AwsJsonProtocolErrorUnmarshaller#getEffectiveErrorCode
private boolean protocolSupportsErrorCodeOverride() {
return !getProtocol().equals("json") && !getProtocol().equals("smithy-rpc-v2-cbor");
}

private Integer getHttpStatusCode(String errorShapeName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ public CompletableFuture<APostOperationResponse> aPostOperation(APostOperationRe
return Optional.empty();
}
switch (errorCode) {
case "InvalidInput":
return Optional.of(ExceptionMetadata.builder().errorCode("InvalidInput").httpStatusCode(400)
case "InvalidInputException":
return Optional.of(ExceptionMetadata.builder().errorCode("InvalidInputException").httpStatusCode(400)
.exceptionBuilderSupplier(InvalidInputException::builder).build());
default:
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ public APostOperationResponse aPostOperation(APostOperationRequest aPostOperatio
return Optional.empty();
}
switch (errorCode) {
case "InvalidInput":
return Optional.of(ExceptionMetadata.builder().errorCode("InvalidInput").httpStatusCode(400)
case "InvalidInputException":
return Optional.of(ExceptionMetadata.builder().errorCode("InvalidInputException").httpStatusCode(400)
.exceptionBuilderSupplier(InvalidInputException::builder).build());
default:
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package software.amazon.awssdk.protocols.json.internal.unmarshall;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import software.amazon.awssdk.annotations.SdkInternalApi;
Expand All @@ -37,15 +38,18 @@ public class JsonErrorCodeParser implements ErrorCodeParser {

static final String EXCEPTION_TYPE_HEADER = ":exception-type";

static final List<String> ERROR_CODE_FIELDS = Collections.unmodifiableList(Arrays.asList("__type", "code"));

/**
* List of header keys that represent the error code sent by service.
* Response should only contain one of these headers
*/
private final List<String> errorCodeHeaders;
private final String errorCodeFieldName;
private final List<String> errorCodeFieldNames;

public JsonErrorCodeParser(String errorCodeFieldName) {
this.errorCodeFieldName = errorCodeFieldName == null ? "__type" : errorCodeFieldName;
public JsonErrorCodeParser(String customErrorCodeFieldName) {
this.errorCodeFieldNames = customErrorCodeFieldName == null ?
ERROR_CODE_FIELDS : Collections.singletonList(customErrorCodeFieldName);
this.errorCodeHeaders = Arrays.asList(X_AMZN_ERROR_TYPE, ERROR_CODE_HEADER, EXCEPTION_TYPE_HEADER);
}

Expand Down Expand Up @@ -88,10 +92,7 @@ private String parseErrorCodeFromHeader(SdkHttpFullResponse response) {

private String parseErrorCodeFromXAmzErrorType(String headerValue) {
if (headerValue != null) {
int separator = headerValue.indexOf(':');
if (separator != -1) {
headerValue = headerValue.substring(0, separator);
}
return parseErrorCode(headerValue);
}
return headerValue;
}
Expand All @@ -106,12 +107,47 @@ private String parseErrorCodeFromContents(JsonNode jsonContents) {
if (jsonContents == null) {
return null;
}
JsonNode errorCodeField = jsonContents.field(errorCodeFieldName).orElse(null);
JsonNode errorCodeField = errorCodeFieldNames.stream()
.map(jsonContents::field)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.orElse(null);
if (errorCodeField == null) {
return null;
}
String code = errorCodeField.text();
int separator = code.lastIndexOf('#');
return code.substring(separator + 1);
return parseErrorCode(errorCodeField.text());
}

// Extract the error code from the error code contents following the smithy defined rules:
// 1) If a : character is present, then take only the contents before the first : character in the value.
// 2) If a # character is present, then take only the contents after the first # character in the value.
// see: https://smithy.io/2.0/aws/protocols/aws-json-1_1-protocol.html#operation-error-serialization
private static String parseErrorCode(String value) {
if (value == null || value.isEmpty()) {
return value;
}

int start = 0;
int end = value.length();

// 1 - everything before the first ':'
int colonIndex = value.indexOf(':');
if (colonIndex >= 0) {
end = colonIndex;
}

// 2 - everything after the first '#'
int hashIndex = value.indexOf('#');
if (hashIndex >= 0 && hashIndex + 1 < end) {
start = hashIndex + 1;
}

// return original string if unchanged
if (start == 0 && end == value.length()) {
return value;
}

return value.substring(start, end);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.protocols.json.BaseAwsStructuredJsonFactory;
import software.amazon.awssdk.protocols.json.ErrorCodeParser;
import software.amazon.awssdk.protocols.json.StructuredJsonGenerator;
import software.amazon.awssdk.protocols.json.internal.unmarshall.JsonErrorCodeParser;
import software.amazon.awssdk.thirdparty.jackson.core.JsonFactory;
import software.amazon.awssdk.thirdparty.jackson.dataformat.cbor.CBORFactory;
import software.amazon.awssdk.thirdparty.jackson.dataformat.cbor.CBORFactoryBuilder;
Expand Down Expand Up @@ -48,6 +50,13 @@ protected StructuredJsonGenerator createWriter(JsonFactory jsonFactory,
public CBORFactory getJsonFactory() {
return CBOR_FACTORY;
}

@Override
public ErrorCodeParser getErrorCodeParser(String customErrorCodeFieldName) {
// smithy cbor ONLY supports the __type field and not code. Set a customErrorCode to use only __type
String errorCodeField = customErrorCodeFieldName == null ? "__type" : customErrorCodeFieldName;
return new JsonErrorCodeParser(errorCodeField);
}
};

private SdkStructuredRpcV2CborFactory() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.isIn;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
Expand Down Expand Up @@ -59,9 +60,11 @@
import software.amazon.awssdk.services.cloudwatch.model.Metric;
import software.amazon.awssdk.services.cloudwatch.model.MetricAlarm;
import software.amazon.awssdk.services.cloudwatch.model.MetricDatum;
import software.amazon.awssdk.services.cloudwatch.model.MissingRequiredParameterException;
import software.amazon.awssdk.services.cloudwatch.model.PutMetricAlarmRequest;
import software.amazon.awssdk.services.cloudwatch.model.PutMetricDataRequest;
import software.amazon.awssdk.services.cloudwatch.model.StateValue;
import software.amazon.awssdk.services.cloudwatch.model.Statistic;
import software.amazon.awssdk.testutils.Waiter;
import software.amazon.awssdk.testutils.service.AwsIntegrationTestBase;

Expand Down Expand Up @@ -369,8 +372,15 @@ public void testExceptionHandling() throws Exception {
cloudwatch.getMetricStatistics(GetMetricStatisticsRequest.builder()
.namespace("fake-namespace").build());
fail("Expected an SdkServiceException, but wasn't thrown");
} catch (SdkServiceException e) {
assertThat(e, isValidSdkServiceException());
} catch (MissingRequiredParameterException e) {
// There is a strong contract on the value of these fields, and they should never change unexpectedly
assertEquals("MissingParameter", e.awsErrorDetails().errorCode());
assertEquals(400, e.statusCode());

// There is no strong contract on the exact value of these fields, but they should always be at least present
assertFalse(e.requestId().isEmpty());
assertFalse(e.awsErrorDetails().serviceName().isEmpty());
assertFalse(e.getMessage().isEmpty());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package software.amazon.awssdk.protocol.asserts.unmarshalling;


import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.unitils.reflectionassert.ReflectionAssert.assertReflectionEquals;

Expand All @@ -39,6 +40,7 @@ protected void doAssert(UnmarshallingTestContext context, Object actual) throws
}
SdkServiceException actualException = (SdkServiceException) actual;
SdkServiceException expectedException = createExpectedResult(context);
assertEquals(expectedException.getClass(), actualException.getClass());
for (Field field : expectedException.getClass().getDeclaredFields()) {
assertFieldEquals(field, actualException, expectedException);
}
Expand Down
Loading