diff --git a/.changes/next-release/bugfix-URLConnectionHTTPClient-1acc5e0.json b/.changes/next-release/bugfix-URLConnectionHTTPClient-1acc5e0.json new file mode 100644 index 000000000000..73a0123a2354 --- /dev/null +++ b/.changes/next-release/bugfix-URLConnectionHTTPClient-1acc5e0.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "URL Connection HTTP Client", + "contributor": "", + "description": "Fix for S3Client with URL Connection http client fails with EOFException when executing HeadObjectRequest for gzip encodeFix to S3Client fails with EOFException." +} diff --git a/http-clients/url-connection-client/src/it/java/software/amazon/awssdk/http/urlconnection/HeadObjectIntegrationTest.java b/http-clients/url-connection-client/src/it/java/software/amazon/awssdk/http/urlconnection/HeadObjectIntegrationTest.java new file mode 100644 index 000000000000..0831a1986fdd --- /dev/null +++ b/http-clients/url-connection-client/src/it/java/software/amazon/awssdk/http/urlconnection/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.http.urlconnection; + +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.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.model.HeadObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +public class HeadObjectIntegrationTest extends UrlHttpConnectionS3IntegrationTestBase { + 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 syncClientSupportsGzippedObjects() { + HeadObjectResponse response = s3.headObject(r -> r.bucket(BUCKET).key(GZIPPED_KEY)); + assertThat(response.contentEncoding()).isEqualTo("gzip"); + } + + @AfterClass + public static void cleanup() { + deleteBucketAndAllContents(BUCKET); + } + +} 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 new file mode 100644 index 000000000000..d8674a1728a5 --- /dev/null +++ b/http-clients/url-connection-client/src/it/java/software/amazon/awssdk/http/urlconnection/UrlHttpConnectionS3IntegrationTestBase.java @@ -0,0 +1,142 @@ +/* + * 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 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; +import software.amazon.awssdk.services.s3.model.ListObjectVersionsResponse; +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; + +/** + * Base class for S3 integration tests. Loads AWS credentials from a properties + * file and creates an S3 client for callers to use. + */ +public class UrlHttpConnectionS3IntegrationTestBase extends AwsTestBase { + + protected static final Region DEFAULT_REGION = Region.US_WEST_2; + /** + * The S3 client for all tests to use. + */ + protected static S3Client s3; + + /** + * Loads the AWS account info for the integration tests and creates an S3 + * client for tests to use. + */ + @BeforeClass + public static void setUp() throws Exception { + s3 = s3ClientBuilder().build(); + } + + protected static S3ClientBuilder s3ClientBuilder() { + return S3Client.builder() + .httpClient(UrlConnectionHttpClient.create()) + .region(DEFAULT_REGION) + .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN); + + } + + + protected static void createBucket(String bucket) { + Waiter.run(() -> s3.createBucket(r -> r.bucket(bucket))) + .ignoringException(NoSuchBucketException.class) + .orFail(); + } + + protected static void deleteBucketAndAllContents(String bucketName) { + deleteBucketAndAllContents(s3, bucketName); + } + + + public static void deleteBucketAndAllContents(S3Client s3, String bucketName) { + try { + System.out.println("Deleting S3 bucket: " + bucketName); + ListObjectsResponse response = Waiter.run(() -> s3.listObjects(r -> r.bucket(bucketName))) + .ignoringException(NoSuchBucketException.class) + .orFail(); + List objectListing = response.contents(); + + if (objectListing != null) { + while (true) { + for (Iterator iterator = objectListing.iterator(); iterator.hasNext(); ) { + S3Object objectSummary = (S3Object) iterator.next(); + s3.deleteObject(DeleteObjectRequest.builder().bucket(bucketName).key(objectSummary.key()).build()); + } + + if (response.isTruncated()) { + objectListing = s3.listObjects(ListObjectsRequest.builder() + .bucket(bucketName) + .marker(response.marker()) + .build()) + .contents(); + } else { + break; + } + } + } + + + ListObjectVersionsResponse versions = s3 + .listObjectVersions(ListObjectVersionsRequest.builder().bucket(bucketName).build()); + + if (versions.deleteMarkers() != null) { + versions.deleteMarkers().forEach(v -> s3.deleteObject(DeleteObjectRequest.builder() + .versionId(v.versionId()) + .bucket(bucketName) + .key(v.key()) + .build())); + } + + if (versions.versions() != null) { + versions.versions().forEach(v -> s3.deleteObject(DeleteObjectRequest.builder() + .versionId(v.versionId()) + .bucket(bucketName) + .key(v.key()) + .build())); + } + + s3.deleteBucket(DeleteBucketRequest.builder().bucket(bucketName).build()); + } catch (Exception e) { + System.err.println("Failed to delete bucket: " + bucketName); + e.printStackTrace(); + } + } +} diff --git a/http-clients/url-connection-client/src/main/java/software/amazon/awssdk/http/urlconnection/UrlConnectionHttpClient.java b/http-clients/url-connection-client/src/main/java/software/amazon/awssdk/http/urlconnection/UrlConnectionHttpClient.java index dd81a47f9593..e956d374ba09 100644 --- a/http-clients/url-connection-client/src/main/java/software/amazon/awssdk/http/urlconnection/UrlConnectionHttpClient.java +++ b/http-clients/url-connection-client/src/main/java/software/amazon/awssdk/http/urlconnection/UrlConnectionHttpClient.java @@ -302,7 +302,10 @@ public HttpExecuteResponse call() throws IOException { int responseCode = getResponseCodeSafely(connection); boolean isErrorResponse = HttpStatusFamily.of(responseCode).isOneOf(CLIENT_ERROR, SERVER_ERROR); Optional responseContent = isErrorResponse ? tryGetErrorStream() : tryGetInputStream(); - AbortableInputStream responseBody = responseContent.map(AbortableInputStream::create).orElse(null); + + AbortableInputStream responseBody = responseHasNoContent() + ? null + : responseContent.map(AbortableInputStream::create).orElse(null); return HttpExecuteResponse.builder() .response(SdkHttpResponse.builder()