Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changes/next-release/bugfix-AWSSDKforJavav2-6d1ba46.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": "Fixed an issue where responses with a non-zero x-amz-crc32 header but no response body were silently returned to the caller as empty results. The SDK now throws Crc32MismatchException that is retryable when a non-zero CRC32 is claimed but no body is delivered, matching v1 SDK behavior."
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.Optional;
import java.util.zip.GZIPInputStream;
import software.amazon.awssdk.annotations.SdkProtectedApi;
import software.amazon.awssdk.core.exception.Crc32MismatchException;
import software.amazon.awssdk.core.internal.util.Crc32ChecksumValidatingInputStream;
import software.amazon.awssdk.http.AbortableInputStream;
import software.amazon.awssdk.http.SdkHttpFullResponse;
Expand All @@ -37,6 +38,16 @@ public static SdkHttpFullResponse validate(boolean calculateCrc32FromCompressedD
SdkHttpFullResponse httpResponse) {

if (!httpResponse.content().isPresent()) {
// CRC32 of zero bytes is 0, so a 0 header is a valid match for an empty body.
// A non-zero header with no content means the Crc32 mismatch error.
Optional<Long> expectedChecksum = getCrc32Checksum(httpResponse);
if (expectedChecksum.isPresent() && expectedChecksum.get() != 0L) {
throw Crc32MismatchException.builder()
.message(String.format("Expected %d as the Crc32 checksum but the response "
+ "had no content",
expectedChecksum.get()))
.build();
}
return httpResponse;
}

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

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.io.IOException;
import java.io.InputStream;
Expand All @@ -27,6 +28,7 @@
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import org.unitils.util.ReflectionUtils;
import software.amazon.awssdk.core.exception.Crc32MismatchException;
import software.amazon.awssdk.core.internal.util.Crc32ChecksumValidatingInputStream;
import software.amazon.awssdk.http.AbortableInputStream;
import software.amazon.awssdk.http.SdkHttpFullResponse;
Expand Down Expand Up @@ -109,18 +111,41 @@ public void adapt_InvalidGzipContent_ThrowsException() throws UnsupportedEncodin
}

@Test
public void adapt_ResponseWithCrc32Header_And_NoContent_DoesNotThrowNPE() throws UnsupportedEncodingException {
public void adapt_ResponseWithNonZeroCrc32Header_AndNoContent_ThrowsCrc32Mismatch() {
SdkHttpFullResponse httpResponse = SdkHttpFullResponse.builder()
.statusCode(200)
.putHeader("x-amz-crc32", "1234")
.build();

assertThatThrownBy(() -> adapt(httpResponse))
.isInstanceOf(Crc32MismatchException.class)
.hasMessageContaining("1234")
.hasMessageContaining("no content");
}

@Test
public void adapt_ResponseWithZeroCrc32Header_AndNoContent_PassesThrough() {
SdkHttpFullResponse httpResponse = SdkHttpFullResponse.builder()
.statusCode(200)
.putHeader("x-amz-crc32", "0")
.build();

SdkHttpFullResponse adapted = adapt(httpResponse);
assertThat(adapted.content().isPresent()).isFalse();
}

@Test
public void adapt_ResponseGzipEncoding_And_NoContent_DoesNotThrowNPE() throws IOException {
public void adapt_ResponseWithoutCrc32Header_AndNoContent_PassesThrough() {
SdkHttpFullResponse httpResponse = SdkHttpFullResponse.builder()
.statusCode(200)
.build();

SdkHttpFullResponse adapted = adapt(httpResponse);
assertThat(adapted.content().isPresent()).isFalse();
}

@Test
public void adapt_ResponseGzipEncoding_AndNoContent_PassesThrough() throws IOException {
SdkHttpFullResponse httpResponse = SdkHttpFullResponse.builder()
.statusCode(200)
.putHeader("Content-Encoding", "gzip")
Expand All @@ -130,6 +155,18 @@ public void adapt_ResponseGzipEncoding_And_NoContent_DoesNotThrowNPE() throws IO
assertThat(adapted.content().isPresent()).isFalse();
}

@Test
public void adapt_ResponseGzip_NonZeroCrc32_AndNoContent_ThrowsCrc32Mismatch() {
SdkHttpFullResponse httpResponse = SdkHttpFullResponse.builder()
.statusCode(200)
.putHeader("Content-Encoding", "gzip")
.putHeader("x-amz-crc32", "1234")
.build();

assertThatThrownBy(() -> adapt(httpResponse))
.isInstanceOf(Crc32MismatchException.class);
}

private SdkHttpFullResponse adapt(SdkHttpFullResponse httpResponse) {
return Crc32Validation.validate(false, httpResponse);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,15 @@ public void useGzipFalse_WhenCrc32IsInvalid_ThrowException() throws Exception {
assertThatThrownBy(() -> jsonRpcAsync.allTypes(AllTypesRequest.builder().build()).get())
.hasRootCauseInstanceOf(Crc32MismatchException.class);
}

@Test
public void emptyBody_WhenCrc32HeaderIsNonZero_ThrowsCrc32Mismatch() {
stubFor(post(urlEqualTo("/")).willReturn(aResponse()
.withStatus(200)
.withHeader("x-amz-crc32", JSON_BODY_Crc32_CHECKSUM)
.withHeader("Content-Length", "0")));

assertThatThrownBy(() -> jsonRpcAsync.allTypes(AllTypesRequest.builder().build()).get())
.hasRootCauseInstanceOf(Crc32MismatchException.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.github.tomakehurst.wiremock.common.SingleRootFileSource;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
Expand All @@ -29,7 +30,7 @@
import org.junit.Test;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.core.exception.Crc32MismatchException;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.protocoljsonrpc.ProtocolJsonRpcClient;
import software.amazon.awssdk.services.protocoljsonrpc.model.AllTypesRequest;
Expand Down Expand Up @@ -97,7 +98,7 @@ public void clientCalculatesCrc32FromCompressedData_ExtraData_WhenCrc32IsValid()
Assert.assertEquals("foo", result.stringMember());
}

@Test(expected = SdkClientException.class)
@Test
public void clientCalculatesCrc32FromCompressedData_WhenCrc32IsInvalid_ThrowsException() {
stubFor(post(urlEqualTo("/")).willReturn(aResponse()
.withStatus(200)
Expand All @@ -111,7 +112,8 @@ public void clientCalculatesCrc32FromCompressedData_WhenCrc32IsInvalid_ThrowsExc
.endpointOverride(URI.create("http://localhost:" + mockServer.port()))
.build();

jsonRpc.simple(SimpleRequest.builder().build());
assertThatThrownBy(() -> jsonRpc.simple(SimpleRequest.builder().build()))
.isInstanceOf(Crc32MismatchException.class);
}

@Test
Expand All @@ -133,7 +135,7 @@ public void clientCalculatesCrc32FromDecompressedData_WhenCrc32IsValid() {
Assert.assertEquals("foo", result.stringMember());
}

@Test(expected = SdkClientException.class)
@Test
public void clientCalculatesCrc32FromDecompressedData_WhenCrc32IsInvalid_ThrowsException() {
stubFor(post(urlEqualTo("/")).willReturn(aResponse()
.withStatus(200)
Expand All @@ -147,7 +149,8 @@ public void clientCalculatesCrc32FromDecompressedData_WhenCrc32IsInvalid_ThrowsE
.endpointOverride(URI.create("http://localhost:" + mockServer.port()))
.build();

jsonRpc.allTypes(AllTypesRequest.builder().build());
assertThatThrownBy(() -> jsonRpc.allTypes(AllTypesRequest.builder().build()))
.isInstanceOf(Crc32MismatchException.class);
}

@Test
Expand All @@ -168,7 +171,7 @@ public void useGzipFalse_WhenCrc32IsValid() {
Assert.assertEquals("foo", result.stringMember());
}

@Test(expected = SdkClientException.class)
@Test
public void useGzipFalse_WhenCrc32IsInvalid_ThrowException() {
stubFor(post(urlEqualTo("/")).willReturn(aResponse()
.withStatus(200)
Expand All @@ -181,6 +184,24 @@ public void useGzipFalse_WhenCrc32IsInvalid_ThrowException() {
.endpointOverride(URI.create("http://localhost:" + mockServer.port()))
.build();

jsonRpc.allTypes(AllTypesRequest.builder().build());
assertThatThrownBy(() -> jsonRpc.allTypes(AllTypesRequest.builder().build()))
.isInstanceOf(Crc32MismatchException.class);
}

@Test
public void emptyBody_WhenCrc32HeaderIsNonZero_ThrowsCrc32Mismatch() {
stubFor(post(urlEqualTo("/")).willReturn(aResponse()
.withStatus(200)
.withHeader("x-amz-crc32", JSON_BODY_Crc32_CHECKSUM)
.withHeader("Content-Length", "0")));

ProtocolJsonRpcClient jsonRpc = ProtocolJsonRpcClient.builder()
.credentialsProvider(FAKE_CREDENTIALS_PROVIDER)
.region(Region.US_EAST_1)
.endpointOverride(URI.create("http://localhost:" + mockServer.port()))
.build();

assertThatThrownBy(() -> jsonRpc.allTypes(AllTypesRequest.builder().build()))
.isInstanceOf(Crc32MismatchException.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.github.tomakehurst.wiremock.common.SingleRootFileSource;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
Expand All @@ -30,7 +31,7 @@
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.core.exception.Crc32MismatchException;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonClient;
import software.amazon.awssdk.services.protocolrestjson.model.AllTypesRequest;
Expand Down Expand Up @@ -71,7 +72,7 @@ public void clientCalculatesCrc32FromCompressedData_WhenCrc32IsValid() {
Assert.assertEquals("foo", result.stringMember());
}

@Test(expected = SdkClientException.class)
@Test
public void clientCalculatesCrc32FromCompressedData_WhenCrc32IsInvalid_ThrowsException() {
stubFor(post(urlEqualTo(RESOURCE_PATH)).willReturn(aResponse()
.withStatus(200)
Expand All @@ -84,7 +85,8 @@ public void clientCalculatesCrc32FromCompressedData_WhenCrc32IsInvalid_ThrowsExc
.endpointOverride(URI.create("http://localhost:" + mockServer.port()))
.build();

client.simple(SimpleRequest.builder().build());
assertThatThrownBy(() -> client.simple(SimpleRequest.builder().build()))
.isInstanceOf(Crc32MismatchException.class);
}

@Test
Expand All @@ -105,7 +107,7 @@ public void clientCalculatesCrc32FromDecompressedData_WhenCrc32IsValid() {
Assert.assertEquals("foo", result.stringMember());
}

@Test(expected = SdkClientException.class)
@Test
public void clientCalculatesCrc32FromDecompressedData_WhenCrc32IsInvalid_ThrowsException() {
stubFor(post(urlEqualTo(RESOURCE_PATH)).willReturn(aResponse()
.withStatus(200)
Expand All @@ -118,7 +120,8 @@ public void clientCalculatesCrc32FromDecompressedData_WhenCrc32IsInvalid_ThrowsE
.endpointOverride(URI.create("http://localhost:" + mockServer.port()))
.build();

client.allTypes(AllTypesRequest.builder().build());
assertThatThrownBy(() -> client.allTypes(AllTypesRequest.builder().build()))
.isInstanceOf(Crc32MismatchException.class);
}

@Test
Expand All @@ -138,7 +141,7 @@ public void useGzipFalse_WhenCrc32IsValid() {
Assert.assertEquals("foo", result.stringMember());
}

@Test(expected = SdkClientException.class)
@Test
public void useGzipFalse_WhenCrc32IsInvalid_ThrowException() {
stubFor(post(urlEqualTo(RESOURCE_PATH)).willReturn(aResponse()
.withStatus(200)
Expand All @@ -151,6 +154,24 @@ public void useGzipFalse_WhenCrc32IsInvalid_ThrowException() {
.endpointOverride(URI.create("http://localhost:" + mockServer.port()))
.build();

client.allTypes(AllTypesRequest.builder().build());
assertThatThrownBy(() -> client.allTypes(AllTypesRequest.builder().build()))
.isInstanceOf(Crc32MismatchException.class);
}

@Test
public void emptyBody_WhenCrc32HeaderIsNonZero_ThrowsCrc32Mismatch() {
stubFor(post(urlEqualTo(RESOURCE_PATH)).willReturn(aResponse()
.withStatus(200)
.withHeader("x-amz-crc32", JSON_BODY_Crc32_CHECKSUM)
.withHeader("Content-Length", "0")));

ProtocolRestJsonClient client = ProtocolRestJsonClient.builder()
.credentialsProvider(FAKE_CREDENTIALS_PROVIDER)
.region(Region.US_EAST_1)
.endpointOverride(URI.create("http://localhost:" + mockServer.port()))
.build();

assertThatThrownBy(() -> client.allTypes(AllTypesRequest.builder().build()))
.isInstanceOf(Crc32MismatchException.class);
}
}
Loading