diff --git a/.changes/next-release/bugfix-AmazonS3-0dda0b4.json b/.changes/next-release/bugfix-AmazonS3-0dda0b4.json new file mode 100644 index 00000000000..20611ca8ce3 --- /dev/null +++ b/.changes/next-release/bugfix-AmazonS3-0dda0b4.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "Amazon S3", + "contributor": "", + "description": "Add custom 503 throttling detection for S3 head operations" +} diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/ExceptionTranslationInterceptor.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/ExceptionTranslationInterceptor.java index 6036434123b..99d035d90c7 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/ExceptionTranslationInterceptor.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/ExceptionTranslationInterceptor.java @@ -75,7 +75,22 @@ public Throwable modifyException(Context.FailedExecution context, ExecutionAttri .message(message) .build(); } - } else if (errorDetails.errorMessage() == null) { + } + + if (exception.statusCode() == 503) { + if ("Slow Down".equals(errorDetails.sdkHttpResponse().statusText().orElse(null))) { + return S3Exception.builder() + .awsErrorDetails(fillErrorDetails(errorDetails, "SlowDown", + "Please reduce your request rate.")) + .statusCode(503) + .requestId(requestId) + .extendedRequestId(extendedRequestId) + .message(message) + .build(); + } + } + + if (errorDetails.errorMessage() == null) { // Populate the error message using the HTTP response status text. Usually that's just the value from the // HTTP spec (e.g. "Forbidden"), but sometimes S3 throws some more useful things in there, like "Slow Down". String errorMessage = errorDetails.sdkHttpResponse().statusText().orElse(null); diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/functionaltests/HeadOperationsThrottlingTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/functionaltests/HeadOperationsThrottlingTest.java new file mode 100644 index 00000000000..b7d3d4ef787 --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/functionaltests/HeadOperationsThrottlingTest.java @@ -0,0 +1,87 @@ +/* + * 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.functionaltests; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.head; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import java.net.URI; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.S3Exception; + +public class HeadOperationsThrottlingTest { + + @Rule + public WireMockRule mockServer = new WireMockRule(0); + + private S3Client client; + + @Before + public void setup() { + client = S3Client.builder() + .endpointOverride(URI.create("http://localhost:" + mockServer.port())) + .credentialsProvider(() -> AwsBasicCredentials.create("test", "test")) + .forcePathStyle(true) + .region(Region.US_EAST_1) + .build(); + } + + @Test + public void headObject503SlowDown_shouldBeThrottlingException() { + stubFor(head(anyUrl()).willReturn(aResponse().withStatus(503).withStatusMessage("Slow Down"))); + + assertThatThrownBy(() -> client.headObject(r -> r.bucket("bucket").key("key"))) + .isInstanceOfSatisfying(S3Exception.class, e -> { + assertThat(e.statusCode()).isEqualTo(503); + assertThat(e.isThrottlingException()).isTrue(); + assertThat(e.awsErrorDetails().errorCode()).isEqualTo("SlowDown"); + }); + } + + @Test + public void headBucket503SlowDown_shouldBeThrottlingException() { + stubFor(head(anyUrl()).willReturn(aResponse().withStatus(503).withStatusMessage("Slow Down"))); + + assertThatThrownBy(() -> client.headBucket(r -> r.bucket("bucket"))) + .isInstanceOfSatisfying(S3Exception.class, e -> { + assertThat(e.statusCode()).isEqualTo(503); + assertThat(e.isThrottlingException()).isTrue(); + assertThat(e.awsErrorDetails().errorCode()).isEqualTo("SlowDown"); + }); + } + + @Test + public void headObject503OtherException_shouldNotBeThrottlingException() { + stubFor(head(anyUrl()).willReturn(aResponse().withStatus(503).withStatusMessage("Service Unavailable"))); + + assertThatThrownBy(() -> client.headObject(r -> r.bucket("bucket").key("key"))) + .isInstanceOfSatisfying(S3Exception.class, e -> { + assertThat(e.statusCode()).isEqualTo(503); + assertThat(e.isThrottlingException()).isFalse(); + assertThat(e.awsErrorDetails().errorCode()).isNull(); + }); + } +} diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/ExceptionTranslationInterceptorTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/ExceptionTranslationInterceptorTest.java index ab0a8a6dde8..c83ca7a50dd 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/ExceptionTranslationInterceptorTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/ExceptionTranslationInterceptorTest.java @@ -98,6 +98,46 @@ public void otherRequest_shouldNotThrowException() { assertThat(interceptor.modifyException(failedExecution, new ExecutionAttributes())).isEqualTo(s3Exception); } + @Test + public void headObject503SlowDown_shouldBeThrottlingException() { + S3Exception s3Exception = create503ThrottlingException(); + Context.FailedExecution failedExecution = getFailedExecution(s3Exception, + HeadObjectRequest.builder().build()); + S3Exception modifiedException = (S3Exception) interceptor.modifyException(failedExecution, new ExecutionAttributes()); + assertThat(modifiedException.awsErrorDetails().errorCode()).isEqualTo("SlowDown"); + assertThat(modifiedException.isThrottlingException()).isTrue(); + } + + @Test + public void headBucket503SlowDown_shouldBeThrottlingException() { + S3Exception s3Exception = create503ThrottlingException(); + Context.FailedExecution failedExecution = getFailedExecution(s3Exception, + HeadBucketRequest.builder().build()); + S3Exception modifiedException = (S3Exception) interceptor.modifyException(failedExecution, new ExecutionAttributes()); + assertThat(modifiedException.awsErrorDetails().errorCode()).isEqualTo("SlowDown"); + assertThat(modifiedException.isThrottlingException()).isTrue(); + } + + @Test + public void headBucket503ServiceUnavailable_shouldNotBeThrottlingException() { + S3Exception s3Exception = create503NonThrottlingException(); + Context.FailedExecution failedExecution = getFailedExecution(s3Exception, + HeadBucketRequest.builder().build()); + S3Exception modifiedException = (S3Exception) interceptor.modifyException(failedExecution, new ExecutionAttributes()); + assertThat(modifiedException.awsErrorDetails().errorCode()).isNull(); + assertThat(modifiedException.isThrottlingException()).isFalse(); + } + + @Test + public void headObject503ServiceUnavailable_shouldNotBeThrottlingException() { + S3Exception s3Exception = create503NonThrottlingException(); + Context.FailedExecution failedExecution = getFailedExecution(s3Exception, + HeadObjectRequest.builder().build()); + S3Exception modifiedException = (S3Exception) interceptor.modifyException(failedExecution, new ExecutionAttributes()); + assertThat(modifiedException.awsErrorDetails().errorCode()).isNull(); + assertThat(modifiedException.isThrottlingException()).isFalse(); + } + private S3Exception create404S3Exception() { return (S3Exception) S3Exception.builder() .awsErrorDetails(AwsErrorDetails.builder() @@ -118,4 +158,27 @@ private S3Exception create403S3Exception() { .statusCode(403) .build(); } + + private S3Exception create503ThrottlingException() { + return (S3Exception) S3Exception.builder() + .awsErrorDetails(AwsErrorDetails.builder() + .sdkHttpResponse(SdkHttpFullResponse.builder() + .statusText( + "Slow Down") + .build()) + .build()) + .statusCode(503) + .build(); + } + + private S3Exception create503NonThrottlingException() { + return (S3Exception) S3Exception.builder() + .awsErrorDetails(AwsErrorDetails.builder() + .sdkHttpResponse(SdkHttpFullResponse.builder() + .statusText("Service Unavailable") + .build()) + .build()) + .statusCode(503) + .build(); + } }