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/feature-AmazonS3-08f1e6b.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "Amazon S3",
"contributor": "",
"description": "Add configurable `expectContinueThresholdInBytes` to S3Configuration (default 1 MB). The Expect: 100-continue header is now only added to PutObject and UploadPart requests when the content-length meets or exceeds the threshold, reducing latency overhead for small uploads."
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,19 @@ public final class S3Configuration implements ServiceConfiguration, ToCopyableBu
*/
private static final boolean DEFAULT_EXPECT_CONTINUE_ENABLED = true;

/**
* The default minimum content-length in bytes at which the {@code Expect: 100-continue} header is added.
* Requests with a content-length below this threshold will not include the header.
*/
private static final long DEFAULT_EXPECT_CONTINUE_THRESHOLD_IN_BYTES = 1_048_576L;

private final FieldWithDefault<Boolean> pathStyleAccessEnabled;
private final FieldWithDefault<Boolean> accelerateModeEnabled;
private final FieldWithDefault<Boolean> dualstackEnabled;
private final FieldWithDefault<Boolean> checksumValidationEnabled;
private final FieldWithDefault<Boolean> chunkedEncodingEnabled;
private final FieldWithDefault<Boolean> expectContinueEnabled;
private final FieldWithDefault<Long> expectContinueThresholdInBytes;
private final Boolean useArnRegionEnabled;
private final Boolean multiRegionEnabled;
private final FieldWithDefault<Supplier<ProfileFile>> profileFile;
Expand All @@ -97,6 +104,13 @@ private S3Configuration(DefaultS3ServiceConfigurationBuilder builder) {
this.chunkedEncodingEnabled = FieldWithDefault.create(builder.chunkedEncodingEnabled, DEFAULT_CHUNKED_ENCODING_ENABLED);
this.expectContinueEnabled = FieldWithDefault.create(builder.expectContinueEnabled,
DEFAULT_EXPECT_CONTINUE_ENABLED);
this.expectContinueThresholdInBytes = FieldWithDefault.create(builder.expectContinueThresholdInBytes,
DEFAULT_EXPECT_CONTINUE_THRESHOLD_IN_BYTES);
if (this.expectContinueThresholdInBytes.value() < 0) {
throw new IllegalArgumentException(
"expectContinueThresholdInBytes must not be negative, but was: "
+ this.expectContinueThresholdInBytes.value());
}
Comment on lines +109 to +113
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to raise this earlier... should we throw error if expectContinueEnabled is false and expectContinueThresholdInBytes is configured? Don't remember how we handle it for mutlipartEnabled vs multipartConfiguration, let me check

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - good call out - I believe for multipartEnable + multipartConfiguration we do not throw - we just silently ignore the multipartConfiguration.

this.profileFile = FieldWithDefault.create(builder.profileFile, ProfileFile::defaultProfileFile);
this.profileName = FieldWithDefault.create(builder.profileName,
ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow());
Expand Down Expand Up @@ -247,6 +261,26 @@ public boolean expectContinueEnabled() {
return expectContinueEnabled.value();
}

/**
* Returns the minimum content-length in bytes at which the {@code Expect: 100-continue} header is added to
* {@link PutObjectRequest} and {@link UploadPartRequest}. Requests with a content-length below this threshold
* will not include the header.
* <p>
* The default value is 1048576 bytes (1 MB).
* <p>
* <b>Note:</b> When using the {@code ApacheHttpClient} (Apache 4), the Apache 4 client also independently adds the
* {@code Expect: 100-continue} header by default without any threshold via its own {@code expectContinueEnabled}
* setting. To benefit from the `expectContinueThresholdInBytes` you must disable {@code expectContinueEnabled}
* on the Apache4 HTTP client builder using {@code ApacheHttpClient.builder().expectContinueEnabled(false)}.
* This does NOT apply to the {@code Apache5HttpClient} which defaults {@code expectContinueEnabled} to false.
*
* @return The threshold in bytes.
* @see S3Configuration.Builder#expectContinueThresholdInBytes(Long)
*/
public long expectContinueThresholdInBytes() {
return expectContinueThresholdInBytes.value();
}

/**
* Returns whether the client is allowed to make cross-region calls when an S3 Access Point ARN has a different
* region to the one configured on the client.
Expand Down Expand Up @@ -278,6 +312,7 @@ public Builder toBuilder() {
.checksumValidationEnabled(checksumValidationEnabled.valueOrNullIfDefault())
.chunkedEncodingEnabled(chunkedEncodingEnabled.valueOrNullIfDefault())
.expectContinueEnabled(expectContinueEnabled.valueOrNullIfDefault())
.expectContinueThresholdInBytes(expectContinueThresholdInBytes.valueOrNullIfDefault())
.useArnRegionEnabled(useArnRegionEnabled)
.profileFile(profileFile.valueOrNullIfDefault())
.profileName(profileName.valueOrNullIfDefault());
Expand Down Expand Up @@ -407,6 +442,32 @@ public interface Builder extends CopyableBuilder<Builder, S3Configuration> {
*/
Builder expectContinueEnabled(Boolean expectContinueEnabled);

Long expectContinueThresholdInBytes();

/**
* Option to configure the minimum content-length in bytes at which the {@code Expect: 100-continue} header
* is added to {@link PutObjectRequest} and {@link UploadPartRequest}. Requests with a content-length below
* this threshold will not include the header, reducing latency for small uploads where the round-trip cost
* of the 100-continue handshake outweighs the benefit.
* <p>
* The default value is 1048576 bytes (1 MB). Setting this to 0 restores the pre-threshold behavior where
* the header is added for all non-zero content-length requests.
* <p>
* This setting only takes effect when {@link #expectContinueEnabled(Boolean)} is {@code true} (the default).
* <p>
* When content length is not known, the {@code Expect: 100-continue} header will always be added
* when {@link #expectContinueEnabled(Boolean)} is {@code true}.
* <p>
* <b>Note:</b> When using the {@code ApacheHttpClient} (Apache 4), the Apache 4 client also independently adds the
* {@code Expect: 100-continue} header by default via its own {@code expectContinueEnabled} setting. This threshold
* only controls the SDK's own header addition; it does not affect the Apache client's behavior.
*
* @param expectContinueThresholdInBytes The threshold in bytes, or {@code null} to use the default (1048576).
* @return This builder for method chaining.
* @see S3Configuration#expectContinueThresholdInBytes()
*/
Builder expectContinueThresholdInBytes(Long expectContinueThresholdInBytes);

Boolean useArnRegionEnabled();

/**
Expand Down Expand Up @@ -476,6 +537,7 @@ static final class DefaultS3ServiceConfigurationBuilder implements Builder {
private Boolean checksumValidationEnabled;
private Boolean chunkedEncodingEnabled;
private Boolean expectContinueEnabled;
private Long expectContinueThresholdInBytes;
private Boolean useArnRegionEnabled;
private Boolean multiRegionEnabled;
private Supplier<ProfileFile> profileFile;
Expand Down Expand Up @@ -571,6 +633,21 @@ public void setExpectContinueEnabled(Boolean expectContinueEnabled) {
expectContinueEnabled(expectContinueEnabled);
}

@Override
public Long expectContinueThresholdInBytes() {
return expectContinueThresholdInBytes;
}

@Override
public Builder expectContinueThresholdInBytes(Long expectContinueThresholdInBytes) {
this.expectContinueThresholdInBytes = expectContinueThresholdInBytes;
return this;
}

public void setExpectContinueThresholdInBytes(Long expectContinueThresholdInBytes) {
expectContinueThresholdInBytes(expectContinueThresholdInBytes);
}

@Override
public Boolean useArnRegionEnabled() {
return useArnRegionEnabled;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,24 @@ private boolean shouldAddExpectContinueHeader(Context.ModifyHttpRequest context,
return false;
}

if (isExpect100ContinueDisabled(executionAttributes)) {
S3Configuration s3Config = getS3Configuration(executionAttributes);

if (s3Config != null && !s3Config.expectContinueEnabled()) {
return false;
}

long threshold = s3Config != null ? s3Config.expectContinueThresholdInBytes()
: 0L;

return getContentLengthHeader(context.httpRequest())
.map(Long::parseLong)
.map(length -> length != 0L)
.map(length -> length >= threshold && length != 0L)
.orElse(true);
}

private boolean isExpect100ContinueDisabled(ExecutionAttributes executionAttributes) {
private S3Configuration getS3Configuration(ExecutionAttributes executionAttributes) {
ServiceConfiguration serviceConfig = executionAttributes.getAttribute(SdkExecutionAttribute.SERVICE_CONFIG);
if (serviceConfig instanceof S3Configuration) {
return !((S3Configuration) serviceConfig).expectContinueEnabled();
}
return false;
return serviceConfig instanceof S3Configuration ? (S3Configuration) serviceConfig : null;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package software.amazon.awssdk.services.s3;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static software.amazon.awssdk.profiles.ProfileFileSystemSetting.AWS_CONFIG_FILE;
import static software.amazon.awssdk.services.s3.S3SystemSetting.AWS_S3_DISABLE_MULTIREGION_ACCESS_POINTS;
import static software.amazon.awssdk.services.s3.S3SystemSetting.AWS_S3_USE_ARN_REGION;
Expand Down Expand Up @@ -47,6 +48,7 @@
assertThat(config.pathStyleAccessEnabled()).isFalse();
assertThat(config.useArnRegionEnabled()).isFalse();
assertThat(config.expectContinueEnabled()).isTrue();
assertThat(config.expectContinueThresholdInBytes()).isEqualTo(1048576L);
}

@Test
Expand Down Expand Up @@ -116,5 +118,55 @@
assertThat(config.useArnRegionEnabled()).isEqualTo(false);
}

// -----------------------------------------------------------------------
// expectContinueThresholdInBytes
// -----------------------------------------------------------------------

@Test
public void expectContinueThresholdInBytes_defaultValue_is1MB() {
S3Configuration config = S3Configuration.builder().build();
assertThat(config.expectContinueThresholdInBytes()).isEqualTo(1048576L);
}

@Test
public void expectContinueThresholdInBytes_customValue_isPreserved() {

Check warning on line 132 in services/s3/src/test/java/software/amazon/awssdk/services/s3/S3ConfigurationTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this 'public' modifier.

See more on https://sonarcloud.io/project/issues?id=aws_aws-sdk-java-v2&issues=AZ2YWazZ3GBU49WbZRo_&open=AZ2YWazZ3GBU49WbZRo_&pullRequest=6864
S3Configuration config = S3Configuration.builder()
.expectContinueThresholdInBytes(2_097_152L)
.build();
assertThat(config.expectContinueThresholdInBytes()).isEqualTo(2_097_152L);
}

@Test
public void expectContinueThresholdInBytes_toBuilder_preservesUserSetValue() {

Check warning on line 140 in services/s3/src/test/java/software/amazon/awssdk/services/s3/S3ConfigurationTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this 'public' modifier.

See more on https://sonarcloud.io/project/issues?id=aws_aws-sdk-java-v2&issues=AZ2YWazZ3GBU49WbZRpA&open=AZ2YWazZ3GBU49WbZRpA&pullRequest=6864
S3Configuration config = S3Configuration.builder()
.expectContinueThresholdInBytes(512L)
.build();
S3Configuration rebuilt = config.toBuilder().build();
assertThat(rebuilt.expectContinueThresholdInBytes()).isEqualTo(512L);
}

@Test
public void expectContinueThresholdInBytes_toBuilder_returnsNullForDefault() {

Check warning on line 149 in services/s3/src/test/java/software/amazon/awssdk/services/s3/S3ConfigurationTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this 'public' modifier.

See more on https://sonarcloud.io/project/issues?id=aws_aws-sdk-java-v2&issues=AZ2YWazZ3GBU49WbZRpB&open=AZ2YWazZ3GBU49WbZRpB&pullRequest=6864
S3Configuration config = S3Configuration.builder().build();
S3Configuration.Builder builder = config.toBuilder();
assertThat(builder.expectContinueThresholdInBytes()).isNull();
}

@Test
public void expectContinueThresholdInBytes_negativeValue_throwsException() {
assertThatThrownBy(() -> S3Configuration.builder()
.expectContinueThresholdInBytes(-1L)
.build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("expectContinueThresholdInBytes must not be negative");
}

@Test
public void expectContinueThresholdInBytes_zeroValue_isAccepted() {

Check warning on line 165 in services/s3/src/test/java/software/amazon/awssdk/services/s3/S3ConfigurationTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this 'public' modifier.

See more on https://sonarcloud.io/project/issues?id=aws_aws-sdk-java-v2&issues=AZ2YWazZ3GBU49WbZRpD&open=AZ2YWazZ3GBU49WbZRpD&pullRequest=6864
S3Configuration config = S3Configuration.builder()
.expectContinueThresholdInBytes(0L)
.build();
assertThat(config.expectContinueThresholdInBytes()).isEqualTo(0L);

Check warning on line 169 in services/s3/src/test/java/software/amazon/awssdk/services/s3/S3ConfigurationTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use isZero() instead.

See more on https://sonarcloud.io/project/issues?id=aws_aws-sdk-java-v2&issues=AZ2YWazZ3GBU49WbZRpF&open=AZ2YWazZ3GBU49WbZRpF&pullRequest=6864
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ void setup(WireMockRuntimeInfo wmRuntimeInfo) {
.requestChecksumCalculation(RequestChecksumCalculation.WHEN_REQUIRED)
.responseChecksumValidation(ResponseChecksumValidation.WHEN_REQUIRED)
.forcePathStyle(true)
.serviceConfiguration(S3Configuration.builder()
.expectContinueThresholdInBytes(0L)
.build())
.credentialsProvider(staticCredentials())
.build();

Expand All @@ -106,6 +109,9 @@ void setup(WireMockRuntimeInfo wmRuntimeInfo) {
.requestChecksumCalculation(RequestChecksumCalculation.WHEN_REQUIRED)
.responseChecksumValidation(ResponseChecksumValidation.WHEN_REQUIRED)
.forcePathStyle(true)
.serviceConfiguration(S3Configuration.builder()
.expectContinueThresholdInBytes(0L)
.build())
.credentialsProvider(staticCredentials())
.build();
}
Expand Down Expand Up @@ -290,6 +296,7 @@ private static Stream<Arguments> expectContinueConfigProvider() {
.build();
S3Configuration enabledConfig = S3Configuration.builder()
.expectContinueEnabled(true)
.expectContinueThresholdInBytes(0L)
.build();

return Stream.of(
Expand Down Expand Up @@ -381,7 +388,10 @@ private S3Client buildSyncClient(String clientType, WireMockRuntimeInfo wmInfo,
.requestChecksumCalculation(RequestChecksumCalculation.WHEN_REQUIRED)
.responseChecksumValidation(ResponseChecksumValidation.WHEN_REQUIRED)
.forcePathStyle(true)
.serviceConfiguration(config != null ? config : S3Configuration.builder().build())
.serviceConfiguration(config != null ? config
: S3Configuration.builder()
.expectContinueThresholdInBytes(0L)
.build())
.credentialsProvider(staticCredentials())
.build();
}
Expand All @@ -406,7 +416,10 @@ private S3AsyncClient buildAsyncClient(String clientType, WireMockRuntimeInfo wm
.requestChecksumCalculation(RequestChecksumCalculation.WHEN_REQUIRED)
.responseChecksumValidation(ResponseChecksumValidation.WHEN_REQUIRED)
.forcePathStyle(true)
.serviceConfiguration(config != null ? config : S3Configuration.builder().build())
.serviceConfiguration(config != null ? config
: S3Configuration.builder()
.expectContinueThresholdInBytes(0L)
.build())
.credentialsProvider(staticCredentials())
.build();
}
Expand Down
Loading
Loading