diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-757a115.json b/.changes/next-release/bugfix-AWSSDKforJavav2-757a115.json new file mode 100644 index 000000000000..c8ac66466fa0 --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-757a115.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Treat zero-byte responses from async HTTP clients as not having a payload, regardless of the response content-length. This fixes an issue that could cause HEAD responses (e.g. s3's headObject responses) with a content-length specified from being treated as having a payload. This fixes issues like [#1216](https://github.com/aws/aws-sdk-java-v2/issues/1216) where the SDK attempts to read data from the response based on the content-length, not based on whether there was actually a payload." +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/async/AsyncResponseHandler.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/async/AsyncResponseHandler.java index 81400eaefc6d..fc22abde310e 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/async/AsyncResponseHandler.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/async/AsyncResponseHandler.java @@ -80,10 +80,12 @@ public void onError(Throwable err) { public CompletableFuture prepare() { streamFuture = new CompletableFuture<>(); return streamFuture.thenCompose(baos -> { - ByteArrayInputStream content = new ByteArrayInputStream(baos.toByteArray()); - // Ignore aborts - we already have all of the content. - AbortableInputStream abortableContent = AbortableInputStream.create(content); - httpResponse.content(abortableContent); + byte[] responseBytes = baos.toByteArray(); + if (responseBytes.length > 0) { + // Ignore aborts - we already have all of the content. + httpResponse.content(AbortableInputStream.create(new ByteArrayInputStream(responseBytes))); + } + try { return CompletableFuture.completedFuture(responseHandler.handle(crc32Validator.apply(httpResponse.build()), executionAttributes)); diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/H2ServerErrorTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/H2ServerErrorTest.java index bf22b813b15e..c4aed9966c19 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/H2ServerErrorTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/fault/H2ServerErrorTest.java @@ -90,7 +90,7 @@ public void teardown() throws InterruptedException { @Test public void serviceReturn500_newRequestShouldUseNewConnection() { server.return500OnFirstRequest = true; - CompletableFuture firstRequest = sendGetRequest(server.port(), netty); + CompletableFuture firstRequest = sendGetRequest(server.port(), netty); firstRequest.join(); sendGetRequest(server.port(), netty).join(); @@ -100,7 +100,7 @@ public void serviceReturn500_newRequestShouldUseNewConnection() { @Test public void serviceReturn200_newRequestShouldReuseNewConnection() { server.return500OnFirstRequest = false; - CompletableFuture firstRequest = sendGetRequest(server.port(), netty); + CompletableFuture firstRequest = sendGetRequest(server.port(), netty); firstRequest.join(); sendGetRequest(server.port(), netty).join(); diff --git a/services/s3/src/it/java/software/amazon/awssdk/services/s3/HeadObjectIntegrationTest.java b/services/s3/src/it/java/software/amazon/awssdk/services/s3/HeadObjectIntegrationTest.java new file mode 100644 index 000000000000..4218791bc6ce --- /dev/null +++ b/services/s3/src/it/java/software/amazon/awssdk/services/s3/HeadObjectIntegrationTest.java @@ -0,0 +1,64 @@ +/* + * 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.s3; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.zip.GZIPOutputStream; +import org.junit.BeforeClass; +import org.junit.Test; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.model.HeadObjectRequest; +import software.amazon.awssdk.services.s3.model.HeadObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +public class HeadObjectIntegrationTest extends S3IntegrationTestBase { + private static final String BUCKET = temporaryBucketName(HeadObjectIntegrationTest.class); + + private static final String GZIPPED_KEY = "some-key"; + + @BeforeClass + public static void setupFixture() throws IOException { + createBucket(BUCKET); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + GZIPOutputStream gzos = new GZIPOutputStream(baos); + gzos.write("Test".getBytes(StandardCharsets.UTF_8)); + + s3.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(GZIPPED_KEY) + .contentEncoding("gzip") + .build(), + RequestBody.fromBytes(baos.toByteArray())); + } + + @Test + public void asyncClientSupportsGzippedObjects() { + HeadObjectResponse response = s3Async.headObject(r -> r.bucket(BUCKET).key(GZIPPED_KEY)).join(); + assertThat(response.contentEncoding()).isEqualTo("gzip"); + } + + @Test + public void syncClientSupportsGzippedObjects() { + HeadObjectResponse response = s3.headObject(r -> r.bucket(BUCKET).key(GZIPPED_KEY)); + assertThat(response.contentEncoding()).isEqualTo("gzip"); + } +} diff --git a/services/s3/src/it/resources/log4j2.xml b/services/s3/src/it/resources/log4j2.xml deleted file mode 100644 index 80ace9124cfc..000000000000 --- a/services/s3/src/it/resources/log4j2.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/test/http-client-tests/src/main/java/software/amazon/awssdk/http/HttpTestUtils.java b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/HttpTestUtils.java index 6660d990df2e..07bb9378ad36 100644 --- a/test/http-client-tests/src/main/java/software/amazon/awssdk/http/HttpTestUtils.java +++ b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/HttpTestUtils.java @@ -21,6 +21,7 @@ import com.github.tomakehurst.wiremock.WireMockServer; import io.reactivex.Flowable; +import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.URL; import java.nio.ByteBuffer; @@ -38,6 +39,7 @@ import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler; import software.amazon.awssdk.http.async.SdkHttpContentPublisher; +import software.amazon.awssdk.utils.BinaryUtils; public class HttpTestUtils { private HttpTestUtils() { @@ -64,36 +66,44 @@ public static KeyStore getSelfSignedKeyStore() throws Exception { return keyStore; } - public static CompletableFuture sendGetRequest(int serverPort, SdkAsyncHttpClient client) { - AsyncExecuteRequest req = AsyncExecuteRequest.builder() - .responseHandler(new SdkAsyncHttpResponseHandler() { - private SdkHttpResponse headers; - - @Override - public void onHeaders(SdkHttpResponse headers) { - this.headers = headers; - } + public static CompletableFuture sendGetRequest(int serverPort, SdkAsyncHttpClient client) { + return sendRequest(serverPort, client, SdkHttpMethod.GET); + } - @Override - public void onStream(Publisher stream) { - Flowable.fromPublisher(stream).forEach(b -> { - }); - } + public static CompletableFuture sendHeadRequest(int serverPort, SdkAsyncHttpClient client) { + return sendRequest(serverPort, client, SdkHttpMethod.HEAD); + } - @Override - public void onError(Throwable error) { - } - }) - .request(SdkHttpFullRequest.builder() - .method(SdkHttpMethod.GET) - .protocol("https") - .host("127.0.0.1") - .port(serverPort) - .build()) - .requestContentPublisher(new EmptyPublisher()) - .build(); - - return client.execute(req); + private static CompletableFuture sendRequest(int serverPort, + SdkAsyncHttpClient client, + SdkHttpMethod httpMethod) { + ByteArrayOutputStream data = new ByteArrayOutputStream(); + return client.execute(AsyncExecuteRequest.builder() + .responseHandler(new SdkAsyncHttpResponseHandler() { + @Override + public void onHeaders(SdkHttpResponse headers) { + } + + @Override + public void onStream(Publisher stream) { + Flowable.fromPublisher(stream).forEach(b -> { + data.write(BinaryUtils.copyAllBytesFrom(b)); + }); + } + + @Override + public void onError(Throwable error) { + } + }) + .request(SdkHttpFullRequest.builder() + .method(httpMethod) + .protocol("https") + .host("127.0.0.1") + .port(serverPort) + .build()) + .requestContentPublisher(new EmptyPublisher()) + .build()) + .thenApply(v -> data.toByteArray()); } public static SdkHttpContentPublisher createProvider(String body) { diff --git a/test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkAsyncHttpClientH1TestSuite.java b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkAsyncHttpClientH1TestSuite.java index a17ff0887a17..cc9b7f330fd5 100644 --- a/test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkAsyncHttpClientH1TestSuite.java +++ b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkAsyncHttpClientH1TestSuite.java @@ -118,6 +118,12 @@ public void connectionReceiveCloseHeaderShouldNotReuseConnection() throws Interr assertThat(server.channels.size()).isEqualTo(2); } + @Test + public void headRequestResponsesHaveNoPayload() { + byte[] responseData = HttpTestUtils.sendHeadRequest(server.port(), client).join(); + assertThat(responseData).hasSize(0); + } + private static class Server extends ChannelInitializer { private static final byte[] CONTENT = "helloworld".getBytes(StandardCharsets.UTF_8); private ServerBootstrap bootstrap;