diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-a18a347.json b/.changes/next-release/bugfix-AWSSDKforJavav2-a18a347.json new file mode 100644 index 000000000000..b447c704790c --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-a18a347.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Fixed an issue in sync clients where empty response payloads could cause a null pointer exception." +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseSyncClientHandler.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseSyncClientHandler.java index 7f3f6cbe97df..cff3d891c893 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseSyncClientHandler.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseSyncClientHandler.java @@ -203,7 +203,7 @@ private HttpResponseHandlerAdapter(HttpResponseHandler httpResponseHand @Override public ReturnT handle(SdkHttpFullResponse response, ExecutionAttributes executionAttributes) throws Exception { OutputT resp = httpResponseHandler.handle(response, executionAttributes); - return transformResponse(resp, response.content().orElse(null)); + return transformResponse(resp, response.content().orElseGet(AbortableInputStream::createEmpty)); } @Override diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/sync/ResponseTransformer.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/sync/ResponseTransformer.java index e32695b3b0fb..b2a39e2d12ac 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/sync/ResponseTransformer.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/sync/ResponseTransformer.java @@ -172,7 +172,7 @@ static ResponseTransformer> toBy return (response, inputStream) -> { try { InterruptMonitor.checkInterrupted(); - return ResponseBytes.fromByteArray(response, IoUtils.toByteArray(inputStream)); + return ResponseBytes.fromByteArrayUnsafe(response, IoUtils.toByteArray(inputStream)); } catch (IOException e) { throw RetryableException.builder().message("Failed to read response.").cause(e).build(); } diff --git a/http-client-spi/src/main/java/software/amazon/awssdk/http/AbortableInputStream.java b/http-client-spi/src/main/java/software/amazon/awssdk/http/AbortableInputStream.java index 403e610ae9e6..0a7cdda6bc9c 100644 --- a/http-client-spi/src/main/java/software/amazon/awssdk/http/AbortableInputStream.java +++ b/http-client-spi/src/main/java/software/amazon/awssdk/http/AbortableInputStream.java @@ -17,6 +17,7 @@ import static software.amazon.awssdk.utils.Validate.paramNotNull; +import java.io.ByteArrayInputStream; import java.io.FilterInputStream; import java.io.InputStream; import software.amazon.awssdk.annotations.SdkProtectedApi; @@ -61,6 +62,10 @@ public static AbortableInputStream create(InputStream delegate) { return new AbortableInputStream(delegate, () -> { }); } + public static AbortableInputStream createEmpty() { + return create(new ByteArrayInputStream(new byte[0])); + } + @Override public void abort() { abortable.abort(); diff --git a/http-clients/url-connection-client/src/it/java/software/amazon/awssdk/http/urlconnection/EmptyFileS3IntegrationTest.java b/http-clients/url-connection-client/src/it/java/software/amazon/awssdk/http/urlconnection/EmptyFileS3IntegrationTest.java new file mode 100644 index 000000000000..cedcf17cbd6d --- /dev/null +++ b/http-clients/url-connection-client/src/it/java/software/amazon/awssdk/http/urlconnection/EmptyFileS3IntegrationTest.java @@ -0,0 +1,57 @@ +/* + * 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.http.urlconnection; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; + +public class EmptyFileS3IntegrationTest extends UrlHttpConnectionS3IntegrationTestBase { + private static final String BUCKET = temporaryBucketName(EmptyFileS3IntegrationTest.class); + + @BeforeClass + public static void setup() { + createBucket(BUCKET); + } + + @AfterClass + public static void cleanup() { + deleteBucketAndAllContents(BUCKET); + } + + @Test + public void s3EmptyFileGetAsBytesWorksWithoutChecksumValidationEnabled() { + try (S3Client s3 = s3ClientBuilder().serviceConfiguration(c -> c.checksumValidationEnabled(false)) + .build()) { + s3.putObject(r -> r.bucket(BUCKET).key("x"), RequestBody.empty()); + assertThat(s3.getObjectAsBytes(r -> r.bucket(BUCKET).key("x")).asUtf8String()).isEmpty(); + } + } + + @Test + public void s3EmptyFileContentLengthIsCorrectWithoutChecksumValidationEnabled() { + try (S3Client s3 = s3ClientBuilder().serviceConfiguration(c -> c.checksumValidationEnabled(false)) + .build()) { + s3.putObject(r -> r.bucket(BUCKET).key("x"), RequestBody.empty()); + assertThat(s3.getObject(r -> r.bucket(BUCKET).key("x")).response().contentLength()).isEqualTo(0); + } + } +} diff --git a/http-clients/url-connection-client/src/it/java/software/amazon/awssdk/http/urlconnection/UrlHttpConnectionS3IntegrationTestBase.java b/http-clients/url-connection-client/src/it/java/software/amazon/awssdk/http/urlconnection/UrlHttpConnectionS3IntegrationTestBase.java index d8674a1728a5..6c61550f218c 100644 --- a/http-clients/url-connection-client/src/it/java/software/amazon/awssdk/http/urlconnection/UrlHttpConnectionS3IntegrationTestBase.java +++ b/http-clients/url-connection-client/src/it/java/software/amazon/awssdk/http/urlconnection/UrlHttpConnectionS3IntegrationTestBase.java @@ -15,24 +15,12 @@ package software.amazon.awssdk.http.urlconnection; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Iterator; import java.util.List; import org.junit.BeforeClass; -import software.amazon.awssdk.core.ClientType; -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.apache.ApacheHttpClient; import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.s3.S3AsyncClient; -import software.amazon.awssdk.services.s3.S3AsyncClientBuilder; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.S3ClientBuilder; -import software.amazon.awssdk.services.s3.model.BucketLocationConstraint; -import software.amazon.awssdk.services.s3.model.CreateBucketConfiguration; -import software.amazon.awssdk.services.s3.model.CreateBucketRequest; import software.amazon.awssdk.services.s3.model.DeleteBucketRequest; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.ListObjectVersionsRequest; @@ -40,7 +28,6 @@ import software.amazon.awssdk.services.s3.model.ListObjectsRequest; import software.amazon.awssdk.services.s3.model.ListObjectsResponse; import software.amazon.awssdk.services.s3.model.NoSuchBucketException; -import software.amazon.awssdk.services.s3.model.S3Exception; import software.amazon.awssdk.services.s3.model.S3Object; import software.amazon.awssdk.testutils.Waiter; import software.amazon.awssdk.testutils.service.AwsTestBase; @@ -79,6 +66,7 @@ protected static void createBucket(String bucket) { Waiter.run(() -> s3.createBucket(r -> r.bucket(bucket))) .ignoringException(NoSuchBucketException.class) .orFail(); + s3.waiter().waitUntilBucketExists(r -> r.bucket(bucket)); } protected static void deleteBucketAndAllContents(String bucketName) {