From 05848d0a375535bea30b1610e8741eddd8c54e22 Mon Sep 17 00:00:00 2001 From: RanVaknin <50976344+RanVaknin@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:57:35 -0700 Subject: [PATCH 1/5] Add S3 custom 503 throttling detection --- .../ExceptionTranslationInterceptor.java | 8 +++ .../ExceptionTranslationInterceptorTest.java | 63 +++++++++++++++++++ 2 files changed, 71 insertions(+) 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 6036434123b6..3f3228e07aed 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,6 +75,14 @@ public Throwable modifyException(Context.FailedExecution context, ExecutionAttri .message(message) .build(); } + } else if (exception.statusCode() == 503 && "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(); } else 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". 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 ab0a8a6dde8e..c83ca7a50dd5 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(); + } } From 5d43d0d7e22607423535417b8a3e6d9886678f68 Mon Sep 17 00:00:00 2001 From: RanVaknin <50976344+RanVaknin@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:17:41 -0700 Subject: [PATCH 2/5] Fix checkstyle --- .../ExceptionTranslationInterceptor.java | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) 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 3f3228e07aed..99d035d90c7b 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,15 +75,22 @@ public Throwable modifyException(Context.FailedExecution context, ExecutionAttri .message(message) .build(); } - } else if (exception.statusCode() == 503 && "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(); - } 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); From 2e103bbd607b93eecb9ce043364eaa25c4b3e76a Mon Sep 17 00:00:00 2001 From: RanVaknin <50976344+RanVaknin@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:42:27 -0700 Subject: [PATCH 3/5] Add functional tests --- .../HeadOperationsThrottlingTest.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 services/s3/src/test/java/software/amazon/awssdk/services/s3/functionaltests/HeadOperationsThrottlingTest.java 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 000000000000..244d008b9b14 --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/functionaltests/HeadOperationsThrottlingTest.java @@ -0,0 +1,86 @@ +/* + * 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.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 -> { + assert e.statusCode() == 503; + assert e.isThrottlingException(); + assert "SlowDown".equals(e.awsErrorDetails().errorCode()); + }); + } + + @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 -> { + assert e.statusCode() == 503; + assert e.isThrottlingException(); + assert "SlowDown".equals(e.awsErrorDetails().errorCode()); + }); + } + + @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 -> { + assert e.statusCode() == 503; + assert !e.isThrottlingException(); + assert e.awsErrorDetails().errorCode() == null; + }); + } +} From 4dfe282a52097425ac2baeb1b304308257f1a31a Mon Sep 17 00:00:00 2001 From: RanVaknin <50976344+RanVaknin@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:05:38 -0700 Subject: [PATCH 4/5] Add changelog --- .changes/next-release/bugfix-AmazonS3-0dda0b4.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changes/next-release/bugfix-AmazonS3-0dda0b4.json diff --git a/.changes/next-release/bugfix-AmazonS3-0dda0b4.json b/.changes/next-release/bugfix-AmazonS3-0dda0b4.json new file mode 100644 index 000000000000..20611ca8ce35 --- /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" +} From e37a10c0e3c46873de18aaf9b7da258645bcc5ff Mon Sep 17 00:00:00 2001 From: RanVaknin <50976344+RanVaknin@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:15:55 -0700 Subject: [PATCH 5/5] Fix assertions --- .../HeadOperationsThrottlingTest.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) 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 index 244d008b9b14..b7d3d4ef7872 100644 --- 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 @@ -19,6 +19,7 @@ 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; @@ -54,9 +55,9 @@ public void headObject503SlowDown_shouldBeThrottlingException() { assertThatThrownBy(() -> client.headObject(r -> r.bucket("bucket").key("key"))) .isInstanceOfSatisfying(S3Exception.class, e -> { - assert e.statusCode() == 503; - assert e.isThrottlingException(); - assert "SlowDown".equals(e.awsErrorDetails().errorCode()); + assertThat(e.statusCode()).isEqualTo(503); + assertThat(e.isThrottlingException()).isTrue(); + assertThat(e.awsErrorDetails().errorCode()).isEqualTo("SlowDown"); }); } @@ -66,9 +67,9 @@ public void headBucket503SlowDown_shouldBeThrottlingException() { assertThatThrownBy(() -> client.headBucket(r -> r.bucket("bucket"))) .isInstanceOfSatisfying(S3Exception.class, e -> { - assert e.statusCode() == 503; - assert e.isThrottlingException(); - assert "SlowDown".equals(e.awsErrorDetails().errorCode()); + assertThat(e.statusCode()).isEqualTo(503); + assertThat(e.isThrottlingException()).isTrue(); + assertThat(e.awsErrorDetails().errorCode()).isEqualTo("SlowDown"); }); } @@ -78,9 +79,9 @@ public void headObject503OtherException_shouldNotBeThrottlingException() { assertThatThrownBy(() -> client.headObject(r -> r.bucket("bucket").key("key"))) .isInstanceOfSatisfying(S3Exception.class, e -> { - assert e.statusCode() == 503; - assert !e.isThrottlingException(); - assert e.awsErrorDetails().errorCode() == null; + assertThat(e.statusCode()).isEqualTo(503); + assertThat(e.isThrottlingException()).isFalse(); + assertThat(e.awsErrorDetails().errorCode()).isNull(); }); } }