Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/next-release/bugfix-AmazonS3-0dda0b4.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "bugfix",
"category": "Amazon S3",
"contributor": "",
"description": "Add custom 503 throttling detection for S3 head operations"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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();
}
}
Loading