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());
}
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,29 @@ 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>
* <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 +534,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 +630,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 @@ -37,6 +37,7 @@ public final class StreamingRequestInterceptor implements ExecutionInterceptor {

private static final String DECODED_CONTENT_LENGTH_HEADER = "x-amz-decoded-content-length";
private static final String CONTENT_LENGTH_HEADER = "Content-Length";
private static final long DEFAULT_EXPECT_CONTINUE_THRESHOLD_IN_BYTES = 1_048_576L;

@Override
public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context,
Expand All @@ -55,22 +56,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()
: DEFAULT_EXPECT_CONTINUE_THRESHOLD_IN_BYTES;

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 @@ public void createConfiguration_minimal() {
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 @@ public void useArnRegionEnabled_enabledInCProfile_shouldResolveToConfigCorrectly
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() {
S3Configuration config = S3Configuration.builder()
.expectContinueThresholdInBytes(2_097_152L)
.build();
assertThat(config.expectContinueThresholdInBytes()).isEqualTo(2_097_152L);
}

@Test
public void expectContinueThresholdInBytes_toBuilder_preservesUserSetValue() {
S3Configuration config = S3Configuration.builder()
.expectContinueThresholdInBytes(512L)
.build();
S3Configuration rebuilt = config.toBuilder().build();
assertThat(rebuilt.expectContinueThresholdInBytes()).isEqualTo(512L);
}

@Test
public void expectContinueThresholdInBytes_toBuilder_returnsNullForDefault() {
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() {
S3Configuration config = S3Configuration.builder()
.expectContinueThresholdInBytes(0L)
.build();
assertThat(config.expectContinueThresholdInBytes()).isEqualTo(0L);
}

}
Loading
Loading