diff --git a/.changes/next-release/feature-AWSS3-233f74c.json b/.changes/next-release/feature-AWSS3-233f74c.json new file mode 100644 index 000000000000..32b4e526740f --- /dev/null +++ b/.changes/next-release/feature-AWSS3-233f74c.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS S3", + "contributor": "", + "description": "Add support for using CRT's response file in the CRT based S3AsyncClient - CRT will directly write to the file when calling getObject with a Path." +} diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/CrtResponseFileResponseTransformer.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/CrtResponseFileResponseTransformer.java new file mode 100644 index 000000000000..ffe0771855ce --- /dev/null +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/CrtResponseFileResponseTransformer.java @@ -0,0 +1,110 @@ +/* + * 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.internal.crt; + +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.utils.Logger; + +/** + * When the CRT Response File option is used in a request, the body is streamed directly to the file. + * The S3CrtResponseHandlerAdapter in this case will never receive a response body but will call onStream + * when the request is complete with a publisher that will complete immediately. + * This transformer is effectively a no-op transformer that waits for the stream to complete and then + * completes the future with the response. + * + * @param Pojo response type. + */ +@SdkInternalApi +public final class CrtResponseFileResponseTransformer implements AsyncResponseTransformer { + + private static final Logger log = Logger.loggerFor(CrtResponseFileResponseTransformer.class); + + private volatile CompletableFuture cf; + private volatile ResponseT response; + + @Override + public CompletableFuture prepare() { + cf = new CompletableFuture<>(); + return cf.thenApply(ignored -> response); + } + + @Override + public void onResponse(ResponseT response) { + this.response = response; + } + + @Override + public void onStream(SdkPublisher publisher) { + publisher.subscribe(new OnCompleteSubscriber(cf, this::exceptionOccurred)); + } + + @Override + public void exceptionOccurred(Throwable throwable) { + if (cf != null) { + cf.completeExceptionally(throwable); + } else { + log.warn(() -> "An exception occurred before the call to prepare() was able to instantiate the CompletableFuture. " + + "The future cannot be completed exceptionally because it is null"); + } + } + + private static final class OnCompleteSubscriber implements Subscriber { + + private final CompletableFuture future; + private final Consumer onErrorMethod; + private Subscription subscription; + + private OnCompleteSubscriber(CompletableFuture future, Consumer onErrorMethod) { + this.future = future; + this.onErrorMethod = onErrorMethod; + } + + @Override + public void onSubscribe(Subscription s) { + if (this.subscription != null) { + s.cancel(); + return; + } + this.subscription = s; + // do not request data from the subscription since body is written directly to file + } + + @Override + public void onNext(ByteBuffer byteBuffer) { + // The response body is streamed directly to the file - this method should never be called. + // ensure the future is completed exceptionally if this occurs + onErrorMethod.accept(new IllegalStateException("OnCompleteSubscriber received unexpected call to onNext.")); + } + + @Override + public void onError(Throwable throwable) { + onErrorMethod.accept(throwable); + } + + @Override + public void onComplete() { + future.complete(null); + } + } +} diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClient.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClient.java index b749c262d180..676e060e3218 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClient.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClient.java @@ -42,6 +42,7 @@ import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; import software.amazon.awssdk.awscore.retry.AwsRetryStrategy; import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; import software.amazon.awssdk.core.checksums.ChecksumValidation; import software.amazon.awssdk.core.checksums.RequestChecksumCalculation; import software.amazon.awssdk.core.checksums.ResponseChecksumValidation; @@ -57,6 +58,7 @@ import software.amazon.awssdk.core.signer.NoOpSigner; import software.amazon.awssdk.crt.io.ExponentialBackoffRetryOptions; import software.amazon.awssdk.crt.io.StandardRetryOptions; +import software.amazon.awssdk.crt.s3.S3MetaRequestOptions; import software.amazon.awssdk.http.SdkHttpExecutionAttributes; import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; import software.amazon.awssdk.identity.spi.IdentityProvider; @@ -72,6 +74,8 @@ import software.amazon.awssdk.services.s3.internal.s3express.S3ExpressUtils; import software.amazon.awssdk.services.s3.model.CopyObjectRequest; import software.amazon.awssdk.services.s3.model.CopyObjectResponse; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; import software.amazon.awssdk.utils.CollectionUtils; @@ -80,6 +84,9 @@ @SdkInternalApi public final class DefaultS3CrtAsyncClient extends DelegatingS3AsyncClient implements S3CrtAsyncClient { public static final ExecutionAttribute OBJECT_FILE_PATH = new ExecutionAttribute<>("objectFilePath"); + public static final ExecutionAttribute RESPONSE_FILE_PATH = new ExecutionAttribute<>("responseFilePath"); + public static final ExecutionAttribute RESPONSE_FILE_OPTION = + new ExecutionAttribute<>("responseFileOption"); private static final String CRT_CLIENT_CLASSPATH = "software.amazon.awssdk.crt.s3.S3Client"; private final CopyObjectHelper copyObjectHelper; @@ -106,6 +113,22 @@ public CompletableFuture putObject(PutObjectRequest putObject new CrtContentLengthOnlyAsyncFileRequestBody(sourcePath)); } + @Override + public CompletableFuture getObject(GetObjectRequest getObjectRequest, Path destinationPath) { + AsyncResponseTransformer responseTransformer = + new CrtResponseFileResponseTransformer<>(); + + AwsRequestOverrideConfiguration overrideConfig = + getObjectRequest.overrideConfiguration() + .map(config -> config.toBuilder().putExecutionAttribute(RESPONSE_FILE_PATH, destinationPath)) + .orElseGet(() -> AwsRequestOverrideConfiguration.builder() + .putExecutionAttribute(RESPONSE_FILE_PATH, + destinationPath)) + .build(); + + return getObject(getObjectRequest.toBuilder().overrideConfiguration(overrideConfig).build(), responseTransformer); + } + @Override public CompletableFuture copyObject(CopyObjectRequest copyObjectRequest) { return copyObjectHelper.copyObject(copyObjectRequest); @@ -243,7 +266,7 @@ public DefaultS3CrtClientBuilder credentialsProvider(AwsCredentialsProvider cred @Override public DefaultS3CrtClientBuilder credentialsProvider( - IdentityProvider credentialsProvider) { + IdentityProvider credentialsProvider) { this.credentialsProvider = credentialsProvider; return this; } @@ -396,6 +419,10 @@ public void afterMarshalling(Context.AfterMarshalling context, executionAttributes.getAttribute(SdkInternalExecutionAttribute.REQUEST_CHECKSUM_CALCULATION)) .put(RESPONSE_CHECKSUM_VALIDATION, executionAttributes.getAttribute(SdkInternalExecutionAttribute.RESPONSE_CHECKSUM_VALIDATION)) + .put(S3InternalSdkHttpExecutionAttribute.RESPONSE_FILE_PATH, + executionAttributes.getAttribute(RESPONSE_FILE_PATH)) + .put(S3InternalSdkHttpExecutionAttribute.RESPONSE_FILE_OPTION, + executionAttributes.getAttribute(RESPONSE_FILE_OPTION)) .build(); // We rely on CRT to perform checksum validation, disable SDK flexible checksum implementation diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3CrtAsyncHttpClient.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3CrtAsyncHttpClient.java index 665b4d2213c4..1fed55813d33 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3CrtAsyncHttpClient.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3CrtAsyncHttpClient.java @@ -24,6 +24,8 @@ import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.OPERATION_NAME; import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.REQUEST_CHECKSUM_CALCULATION; import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.RESPONSE_CHECKSUM_VALIDATION; +import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.RESPONSE_FILE_OPTION; +import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.RESPONSE_FILE_PATH; import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.SIGNING_NAME; import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.SIGNING_REGION; import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.USE_S3_EXPRESS_AUTH; @@ -135,11 +137,6 @@ public CompletableFuture execute(AsyncExecuteRequest asyncRequest) { HttpRequest httpRequest = toCrtRequest(asyncRequest); SdkHttpExecutionAttributes httpExecutionAttributes = asyncRequest.httpExecutionAttributes(); CompletableFuture s3MetaRequestFuture = new CompletableFuture<>(); - S3CrtResponseHandlerAdapter responseHandler = - new S3CrtResponseHandlerAdapter(executeFuture, - asyncRequest.responseHandler(), - httpExecutionAttributes.getAttribute(CRT_PROGRESS_LISTENER), - s3MetaRequestFuture); String operationName = asyncRequest.httpExecutionAttributes().getAttribute(OPERATION_NAME); S3MetaRequestOptions.MetaRequestType requestType = requestType(operationName); @@ -156,6 +153,16 @@ public CompletableFuture execute(AsyncExecuteRequest asyncRequest) { ChecksumConfig checksumConfig = checksumConfig(httpChecksum, requestType, requestChecksumCalculation, responseChecksumValidation); + Path responseFilePath = httpExecutionAttributes.getAttribute(RESPONSE_FILE_PATH); + S3MetaRequestOptions.ResponseFileOption responseFileOption = httpExecutionAttributes.getAttribute(RESPONSE_FILE_OPTION); + + S3CrtResponseHandlerAdapter responseHandler = + new S3CrtResponseHandlerAdapter( + executeFuture, + asyncRequest.responseHandler(), + httpExecutionAttributes.getAttribute(CRT_PROGRESS_LISTENER), + s3MetaRequestFuture); + URI endpoint = getEndpoint(uri); AwsSigningConfig signingConfig = awsSigningConfig(signingRegion, httpExecutionAttributes); @@ -169,7 +176,12 @@ public CompletableFuture execute(AsyncExecuteRequest asyncRequest) { .withResumeToken(resumeToken) .withOperationName(operationName) .withRequestFilePath(requestFilePath) - .withSigningConfig(signingConfig); + .withSigningConfig(signingConfig) + .withResponseFilePath(responseFilePath); + + if (responseFileOption != null) { + requestOptions = requestOptions.withResponseFileOption(responseFileOption); + } try { S3MetaRequestWrapper requestWrapper = new S3MetaRequestWrapper(crtS3Client.makeMetaRequest(requestOptions)); diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3InternalSdkHttpExecutionAttribute.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3InternalSdkHttpExecutionAttribute.java index 072bcd1fb608..7f882ca15398 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3InternalSdkHttpExecutionAttribute.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3InternalSdkHttpExecutionAttribute.java @@ -21,6 +21,7 @@ import software.amazon.awssdk.core.checksums.ResponseChecksumValidation; import software.amazon.awssdk.core.interceptor.trait.HttpChecksum; import software.amazon.awssdk.crt.s3.ResumeToken; +import software.amazon.awssdk.crt.s3.S3MetaRequestOptions; import software.amazon.awssdk.http.SdkHttpExecutionAttribute; import software.amazon.awssdk.regions.Region; @@ -57,6 +58,12 @@ public final class S3InternalSdkHttpExecutionAttribute extends SdkHttpExecuti public static final S3InternalSdkHttpExecutionAttribute RESPONSE_CHECKSUM_VALIDATION = new S3InternalSdkHttpExecutionAttribute<>(ResponseChecksumValidation.class); + public static final S3InternalSdkHttpExecutionAttribute RESPONSE_FILE_PATH = + new S3InternalSdkHttpExecutionAttribute<>(Path.class); + + public static final S3InternalSdkHttpExecutionAttribute RESPONSE_FILE_OPTION = + new S3InternalSdkHttpExecutionAttribute<>(S3MetaRequestOptions.ResponseFileOption.class); + private S3InternalSdkHttpExecutionAttribute(Class valueClass) { super(valueClass); } diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/CrtResponseFileResponseTransformerTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/CrtResponseFileResponseTransformerTest.java new file mode 100644 index 000000000000..76dc260d8f3f --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/CrtResponseFileResponseTransformerTest.java @@ -0,0 +1,91 @@ +/* + * 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.internal.crt; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.reactivestreams.Subscriber; +import software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.core.async.SdkPublisher; + +public class CrtResponseFileResponseTransformerTest { + private CrtResponseFileResponseTransformer transformer; + private SdkResponse response; + private MockCrtPublisher publisher; + + @BeforeEach + public void setUp() throws Exception { + transformer = new CrtResponseFileResponseTransformer<>(); + response = Mockito.mock(SdkResponse.class); + publisher = new MockCrtPublisher(); + } + + @Test + void successfulResponseAndStream_returnsResponsePublisher() throws Exception { + CompletableFuture responseFuture = transformer.prepare(); + transformer.onResponse(response); + assertThat(responseFuture.isDone()).isFalse(); + transformer.onStream(publisher); + publisher.complete(); + assertThat(responseFuture.isDone()).isTrue(); + SdkResponse returnedResponse = responseFuture.get(); + assertThat(returnedResponse).isEqualTo(response); + } + + @Test + void failedResponse_completesExceptionally() { + CompletableFuture responseFuture = transformer.prepare(); + assertThat(responseFuture.isDone()).isFalse(); + transformer.exceptionOccurred(new RuntimeException("Intentional exception for testing purposes - before response.")); + assertThat(responseFuture.isDone()).isTrue(); + assertThatThrownBy(responseFuture::get) + .isInstanceOf(ExecutionException.class) + .hasCauseInstanceOf(RuntimeException.class); + } + + @Test + void failedStream_completesExceptionally() { + CompletableFuture responseFuture = transformer.prepare(); + transformer.onResponse(response); + assertThat(responseFuture.isDone()).isFalse(); + transformer.exceptionOccurred(new RuntimeException("Intentional exception for testing purposes - after response.")); + assertThat(responseFuture.isDone()).isTrue(); + assertThatThrownBy(responseFuture::get) + .isInstanceOf(ExecutionException.class) + .hasCauseInstanceOf(RuntimeException.class); + } + + private static class MockCrtPublisher implements SdkPublisher { + private Subscriber subscriber; + @Override + public void subscribe(Subscriber s) { + subscriber = s; + } + + public void complete() { + subscriber.onComplete(); + } + } + +} diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/S3CrtAsyncHttpClientTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/S3CrtAsyncHttpClientTest.java index f9cb55c3fbc9..776b69c4a10a 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/S3CrtAsyncHttpClientTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/S3CrtAsyncHttpClientTest.java @@ -24,11 +24,14 @@ import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.OPERATION_NAME; import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.REQUEST_CHECKSUM_CALCULATION; import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.RESPONSE_CHECKSUM_VALIDATION; +import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.RESPONSE_FILE_OPTION; +import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.RESPONSE_FILE_PATH; import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.SIGNING_NAME; import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.SIGNING_REGION; import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.USE_S3_EXPRESS_AUTH; import java.net.URI; +import java.nio.file.Path; import java.time.Duration; import java.util.HashMap; import java.util.Map; @@ -56,6 +59,7 @@ import software.amazon.awssdk.crt.s3.S3ClientOptions; import software.amazon.awssdk.crt.s3.S3MetaRequest; import software.amazon.awssdk.crt.s3.S3MetaRequestOptions; +import software.amazon.awssdk.crt.s3.S3MetaRequestResponseHandler; import software.amazon.awssdk.http.SdkHttpMethod; import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.async.AsyncExecuteRequest; @@ -63,6 +67,7 @@ import software.amazon.awssdk.http.async.SdkHttpContentPublisher; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.crt.S3CrtHttpConfiguration; +import software.amazon.awssdk.testutils.RandomTempFile; public class S3CrtAsyncHttpClientTest { private static final URI DEFAULT_ENDPOINT = URI.create("https://127.0.0.1:443"); @@ -148,10 +153,9 @@ public void putObject_shouldSetMetaRequestTypeCorrectly() { } @Test - public void NonStreamingOperation_shouldSetMetaRequestTypeCorrectly() { + public void nonStreamingOperation_shouldSetMetaRequestTypeCorrectly() { AsyncExecuteRequest asyncExecuteRequest = getExecuteRequestBuilder().putHttpExecutionAttribute(OPERATION_NAME, "CreateBucket").build(); - S3MetaRequestOptions actual = makeRequest(asyncExecuteRequest); assertThat(actual.getMetaRequestType()).isEqualTo(S3MetaRequestOptions.MetaRequestType.DEFAULT); assertThat(actual.getOperationName()).isEqualTo("CreateBucket"); @@ -562,6 +566,21 @@ void build_ProxyConfigurationWithEnvironmentVariables(S3CrtHttpConfiguration s3C } } + @Test + public void responseFilePathAndOption_shouldPassToCrt() { + Path path = RandomTempFile.randomUncreatedFile().toPath(); + + AsyncExecuteRequest asyncExecuteRequest = getExecuteRequestBuilder() + .putHttpExecutionAttribute(OPERATION_NAME, "GetObject") + .putHttpExecutionAttribute(RESPONSE_FILE_PATH, path) + .putHttpExecutionAttribute(RESPONSE_FILE_OPTION, S3MetaRequestOptions.ResponseFileOption.CREATE_OR_APPEND) + .build(); + + S3MetaRequestOptions actual = makeRequest(asyncExecuteRequest); + assertThat(actual.getResponseFilePath()).isEqualTo(path); + assertThat(actual.getResponseFileOption()).isEqualTo(S3MetaRequestOptions.ResponseFileOption.CREATE_OR_APPEND); + } + private AsyncExecuteRequest.Builder getExecuteRequestBuilder() { return getExecuteRequestBuilder(443); } diff --git a/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/CodingConventionWithSuppressionTest.java b/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/CodingConventionWithSuppressionTest.java index b54955cc56f2..d2edcaac742d 100644 --- a/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/CodingConventionWithSuppressionTest.java +++ b/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/CodingConventionWithSuppressionTest.java @@ -50,7 +50,9 @@ public class CodingConventionWithSuppressionTest { Arrays.asList(ArchUtils.classNameToPattern(EmfMetricLoggingPublisher.class), ArchUtils.classNameToPattern(MetricEmfConverter.class), ArchUtils.classNameToPattern(MakeHttpRequestStage.class), - ArchUtils.classNameToPattern("software.amazon.awssdk.services.s3.internal.crt.S3CrtResponseHandlerAdapter"))); + ArchUtils.classNameToPattern("software.amazon.awssdk.services.s3.internal.crt.S3CrtResponseHandlerAdapter"), + ArchUtils.classNameToPattern( + "software.amazon.awssdk.services.s3.internal.crt.CrtResponseFileResponseTransformer"))); private static final Set ALLOWED_ERROR_LOG_SUPPRESSION = new HashSet<>( Arrays.asList(