diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/DefaultCustomizationProcessor.java b/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/DefaultCustomizationProcessor.java
index 1511bf6be282..d0fbd360dbf3 100644
--- a/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/DefaultCustomizationProcessor.java
+++ b/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/DefaultCustomizationProcessor.java
@@ -43,7 +43,8 @@ public static CodegenCustomizationProcessor getProcessorFor(
new S3ControlRemoveAccountIdHostPrefixProcessor(),
new ExplicitStringPayloadQueryProtocolProcessor(),
new LowercaseShapeValidatorProcessor(),
- new LongPollingOperationProcessor()
+ new LongPollingOperationProcessor(),
+ new DefaultReadWriteTimeoutExemptionProcessor()
);
}
}
diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/DefaultReadWriteTimeoutExemptionProcessor.java b/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/DefaultReadWriteTimeoutExemptionProcessor.java
new file mode 100644
index 000000000000..87766c3cfb85
--- /dev/null
+++ b/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/DefaultReadWriteTimeoutExemptionProcessor.java
@@ -0,0 +1,117 @@
+/*
+ * 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.codegen.customization.processors;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import software.amazon.awssdk.annotations.SdkTestInternalApi;
+import software.amazon.awssdk.codegen.customization.CodegenCustomizationProcessor;
+import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
+import software.amazon.awssdk.codegen.model.service.ServiceModel;
+import software.amazon.awssdk.protocols.jsoncore.JsonNode;
+import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;
+import software.amazon.awssdk.utils.Validate;
+
+/**
+ * Bakes the per-service default read/write inactivity timeout tier into the generated service HTTP config. The tiers come from a
+ * checked-in copy of the shared exemption artifact ({@code default-read-write-timeout-exemptions.json}), keyed by the service's
+ * sdkId ({@link software.amazon.awssdk.codegen.model.intermediate.Metadata#getServiceId()}).
+ *
+ *
An artifact value of {@code -1} marks a fully-exempt service (no default timeout applies); a positive value is the applied
+ * timeout in milliseconds. A service absent from the artifact has nothing baked, and {@code aws-core} supplies the flat default
+ * when the rollout gate is on. The rollout gate itself is applied later, in {@code aws-core}; this processor only bakes the
+ * per-service tier, which is the same regardless of whether the gate is on.
+ */
+public class DefaultReadWriteTimeoutExemptionProcessor implements CodegenCustomizationProcessor {
+
+ private static final String EXEMPTIONS_RESOURCE = "software/amazon/awssdk/codegen/default-read-write-timeout-exemptions.json";
+
+ private static final Map SERVICE_ID_TO_TIMEOUT_MILLIS = loadExemptions();
+
+ private final Map serviceIdToTimeoutMillis;
+
+ public DefaultReadWriteTimeoutExemptionProcessor() {
+ this(SERVICE_ID_TO_TIMEOUT_MILLIS);
+ }
+
+ @SdkTestInternalApi
+ DefaultReadWriteTimeoutExemptionProcessor(Map serviceIdToTimeoutMillis) {
+ this.serviceIdToTimeoutMillis = serviceIdToTimeoutMillis;
+ }
+
+ @Override
+ public void preprocess(ServiceModel serviceModel) {
+ // no-op
+ }
+
+ @Override
+ public void postprocess(IntermediateModel intermediateModel) {
+ String serviceId = intermediateModel.getMetadata().getServiceId();
+ Long timeoutMillis = serviceIdToTimeoutMillis.get(serviceId);
+ if (timeoutMillis != null) {
+ intermediateModel.getMetadata().setDefaultReadWriteTimeoutMillis(timeoutMillis);
+ }
+ }
+
+ /**
+ * Fails if any artifact key does not match one of {@code knownServiceIds}. Matching is exact (case-sensitive), so a stale
+ * key (no such service) or a mis-cased key both surface here: either would otherwise silently leave the intended service
+ * unlisted and wrongly apply the flat default instead of its exempt/partial tier.
+ *
+ * Codegen processes one service per run, so this whole-artifact cross-check cannot run inside {@link #postprocess} (a
+ * single run never sees every serviceId). It is invoked at build time by the coverage test against the full set of service
+ * sdkIds.
+ */
+ void validateArtifactKeys(Set knownServiceIds) {
+ List unknownKeys = serviceIdToTimeoutMillis.keySet().stream()
+ .filter(key -> !knownServiceIds.contains(key))
+ .sorted()
+ .collect(Collectors.toList());
+ if (!unknownKeys.isEmpty()) {
+ throw new IllegalStateException(
+ "Read/write timeout exemption artifact " + EXEMPTIONS_RESOURCE + " contains key(s) matching no service sdkId "
+ + "(a stale or mis-cased key silently leaves that service unlisted): " + unknownKeys);
+ }
+ }
+
+ private static Map loadExemptions() {
+ Map exemptions = new HashMap<>();
+ try (InputStream stream = DefaultReadWriteTimeoutExemptionProcessor.class.getClassLoader()
+ .getResourceAsStream(EXEMPTIONS_RESOURCE)) {
+ Validate.notNull(stream, "Failed to load read/write timeout exemption artifact: %s", EXEMPTIONS_RESOURCE);
+ JsonNode root = JsonNodeParser.create().parse(stream);
+ root.asObject().forEach((serviceId, value) -> exemptions.put(serviceId, parseTimeoutMillis(serviceId, value)));
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to read read/write timeout exemption artifact: " + EXEMPTIONS_RESOURCE, e);
+ }
+ return Collections.unmodifiableMap(exemptions);
+ }
+
+ private static long parseTimeoutMillis(String serviceId, JsonNode value) {
+ try {
+ return Long.parseLong(value.asNumber());
+ } catch (RuntimeException e) {
+ throw new IllegalArgumentException(
+ "Invalid numeric value for key '" + serviceId + "' in " + EXEMPTIONS_RESOURCE + ": " + value, e);
+ }
+ }
+}
diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java
index ab3c3f78833d..8d3837433731 100644
--- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java
+++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java
@@ -230,6 +230,11 @@ public class CustomizationConfig {
*/
private Boolean defaultNewRetries2026;
+ /**
+ * Whether the client will apply a default read/write timeout by default.
+ */
+ private Boolean defaultEnableSocketTimeout2026;
+
/**
* Whether to generate an abstract decorator class that delegates to the async service client
*/
@@ -737,6 +742,14 @@ public void setDefaultNewRetries2026(Boolean defaultNewRetries2026) {
this.defaultNewRetries2026 = defaultNewRetries2026;
}
+ public Boolean getDefaultEnableSocketTimeout2026() {
+ return defaultEnableSocketTimeout2026;
+ }
+
+ public void setDefaultEnableSocketTimeout2026(Boolean defaultEnableSocketTimeout2026) {
+ this.defaultEnableSocketTimeout2026 = defaultEnableSocketTimeout2026;
+ }
+
public ServiceConfig getServiceConfig() {
return serviceConfig;
}
diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/Metadata.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/Metadata.java
index 07b5e319d60d..5916fb1ee4de 100644
--- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/Metadata.java
+++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/Metadata.java
@@ -115,6 +115,8 @@ public class Metadata {
private String serviceId;
+ private Long defaultReadWriteTimeoutMillis;
+
private List auth;
public List getAuth() {
@@ -710,6 +712,24 @@ public Metadata withServiceId(String serviceId) {
return this;
}
+ /**
+ * The default read/write inactivity timeout baked for this service by the exemption processor, in milliseconds, or
+ * {@code null} when the service is not listed in the exemption artifact. A value of {@code -1} marks a fully-exempt service
+ * (no default timeout); a positive value is the applied timeout in milliseconds.
+ */
+ public Long getDefaultReadWriteTimeoutMillis() {
+ return defaultReadWriteTimeoutMillis;
+ }
+
+ public void setDefaultReadWriteTimeoutMillis(Long defaultReadWriteTimeoutMillis) {
+ this.defaultReadWriteTimeoutMillis = defaultReadWriteTimeoutMillis;
+ }
+
+ public Metadata withDefaultReadWriteTimeoutMillis(Long defaultReadWriteTimeoutMillis) {
+ setDefaultReadWriteTimeoutMillis(defaultReadWriteTimeoutMillis);
+ return this;
+ }
+
public String getWaitersPackageName() {
return waitersPackageName;
}
diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java
index f2ea2836be7a..47dca6ae9c20 100644
--- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java
+++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java
@@ -31,6 +31,7 @@
import com.squareup.javapoet.TypeVariableName;
import com.squareup.javapoet.WildcardTypeName;
import java.net.URI;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -48,6 +49,7 @@
import software.amazon.awssdk.awscore.client.config.AwsClientOption;
import software.amazon.awssdk.awscore.endpoint.AwsClientEndpointProvider;
import software.amazon.awssdk.codegen.internal.Utils;
+import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig;
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
import software.amazon.awssdk.codegen.model.intermediate.OperationModel;
import software.amazon.awssdk.codegen.model.rules.endpoints.BuiltInParameter;
@@ -330,9 +332,10 @@ private Optional mergeInternalDefaultsMethod() {
String userAgent = model.getCustomizationConfig().getUserAgent();
RetryMode defaultRetryMode = model.getCustomizationConfig().getDefaultRetryMode();
Boolean defaultNewRetries2026 = model.getCustomizationConfig().getDefaultNewRetries2026();
+ Boolean defaultEnableSocketTimeout2026 = model.getCustomizationConfig().getDefaultEnableSocketTimeout2026();
// If none of the options are customized, then we do not need to bother overriding the method
- if (userAgent == null && defaultRetryMode == null && defaultNewRetries2026 == null) {
+ if (!hasInternalDefaults()) {
return Optional.empty();
}
@@ -354,10 +357,22 @@ private Optional mergeInternalDefaultsMethod() {
builder.addCode("c.option($T.DEFAULT_NEW_RETRIES_2026, $L);\n",
SdkClientOption.class, defaultNewRetries2026);
}
+ if (defaultEnableSocketTimeout2026 != null) {
+ builder.addCode("c.option($T.DEFAULT_ENABLE_SOCKET_TIMEOUT_2026, $L);\n",
+ SdkClientOption.class, defaultEnableSocketTimeout2026);
+ }
builder.addCode("});\n");
return Optional.of(builder.build());
}
+ private boolean hasInternalDefaults() {
+ CustomizationConfig customizationConfig = model.getCustomizationConfig();
+ return customizationConfig.getUserAgent() != null
+ || customizationConfig.getDefaultRetryMode() != null
+ || customizationConfig.getDefaultNewRetries2026() != null
+ || customizationConfig.getDefaultEnableSocketTimeout2026() != null;
+ }
+
private MethodSpec finalizeServiceConfigurationMethod() {
String requestHandlerDirectory = Utils.packageToDirectory(model.getMetadata().getFullClientPackageName());
String requestHandlerPath = String.format("%s/execution.interceptors", requestHandlerDirectory);
@@ -828,24 +843,27 @@ private void addServiceHttpConfigIfNeeded(TypeSpec.Builder builder, Intermediate
String serviceDefaultFqcn = model.getCustomizationConfig().getServiceSpecificHttpConfig();
boolean supportsH2 = model.getMetadata().supportsH2();
boolean usePriorKnowledgeForH2 = model.getCustomizationConfig().isUsePriorKnowledgeForH2();
+ Long readWriteTimeoutMillis = model.getMetadata().getDefaultReadWriteTimeoutMillis();
- if (serviceDefaultFqcn != null || supportsH2) {
- builder.addMethod(serviceSpecificHttpConfigMethod(serviceDefaultFqcn, supportsH2, usePriorKnowledgeForH2));
+ if (serviceDefaultFqcn != null || supportsH2 || readWriteTimeoutMillis != null) {
+ builder.addMethod(serviceSpecificHttpConfigMethod(serviceDefaultFqcn, supportsH2, usePriorKnowledgeForH2,
+ readWriteTimeoutMillis));
}
}
private MethodSpec serviceSpecificHttpConfigMethod(String serviceDefaultFqcn, boolean supportsH2,
- boolean usePriorKnowledgeForH2) {
+ boolean usePriorKnowledgeForH2, Long readWriteTimeoutMillis) {
return MethodSpec.methodBuilder("serviceHttpConfig")
.addAnnotation(Override.class)
.addModifiers(PROTECTED, FINAL)
.returns(AttributeMap.class)
- .addCode(serviceSpecificHttpConfigMethodBody(serviceDefaultFqcn, supportsH2, usePriorKnowledgeForH2))
+ .addCode(serviceSpecificHttpConfigMethodBody(serviceDefaultFqcn, supportsH2, usePriorKnowledgeForH2,
+ readWriteTimeoutMillis))
.build();
}
private CodeBlock serviceSpecificHttpConfigMethodBody(String serviceDefaultFqcn, boolean supportsH2,
- boolean usePriorKnowledgeForH2) {
+ boolean usePriorKnowledgeForH2, Long readWriteTimeoutMillis) {
CodeBlock.Builder builder = CodeBlock.builder();
if (serviceDefaultFqcn != null) {
@@ -856,14 +874,28 @@ private CodeBlock serviceSpecificHttpConfigMethodBody(String serviceDefaultFqcn,
builder.addStatement("$1T result = $1T.empty()", AttributeMap.class);
}
- if (supportsH2) {
- builder.add("return result.merge(AttributeMap.builder()"
- + ".put($T.PROTOCOL, $T.HTTP2)",
- SdkHttpConfigurationOption.class, Protocol.class);
+ if (supportsH2 || readWriteTimeoutMillis != null) {
+ builder.add("return result.merge(AttributeMap.builder()");
+
+ if (supportsH2) {
+ builder.add(".put($T.PROTOCOL, $T.HTTP2)", SdkHttpConfigurationOption.class, Protocol.class);
+
+ if (!usePriorKnowledgeForH2) {
+ builder.add(".put($T.PROTOCOL_NEGOTIATION, $T.ALPN)",
+ SdkHttpConfigurationOption.class, ProtocolNegotiation.class);
+ }
+ }
- if (!usePriorKnowledgeForH2) {
- builder.add(".put($T.PROTOCOL_NEGOTIATION, $T.ALPN)",
- SdkHttpConfigurationOption.class, ProtocolNegotiation.class);
+ if (readWriteTimeoutMillis != null) {
+ // A negative artifact value marks a fully-exempt service: bake Duration.ZERO, which means apply no
+ // read/write timeout. A positive value is the timeout in milliseconds.
+ if (readWriteTimeoutMillis < 0) {
+ builder.add(".put($T.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, $T.ZERO)",
+ SdkHttpConfigurationOption.class, Duration.class);
+ } else {
+ builder.add(".put($T.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, $T.ofMillis($L))",
+ SdkHttpConfigurationOption.class, Duration.class, readWriteTimeoutMillis + "L");
+ }
}
builder.addStatement(".build())");
diff --git a/codegen/src/main/resources/software/amazon/awssdk/codegen/default-read-write-timeout-exemptions.json b/codegen/src/main/resources/software/amazon/awssdk/codegen/default-read-write-timeout-exemptions.json
new file mode 100644
index 000000000000..89bbc5e49239
--- /dev/null
+++ b/codegen/src/main/resources/software/amazon/awssdk/codegen/default-read-write-timeout-exemptions.json
@@ -0,0 +1,85 @@
+{
+ "Bedrock Runtime": -1,
+ "CloudSearch Domain": -1,
+ "codeartifact": -1,
+ "ConnectHealth": -1,
+ "EBS": -1,
+ "Glacier": -1,
+ "Lambda": -1,
+ "Lex Runtime Service": -1,
+ "Lex Runtime V2": -1,
+ "MediaStore Data": -1,
+ "Omics": -1,
+ "Polly": -1,
+ "QBusiness": -1,
+ "S3": -1,
+ "SageMaker Runtime HTTP2": -1,
+ "Transcribe Streaming": -1,
+ "b2bi": 900000,
+ "Bedrock Agent Runtime": 900000,
+ "Bedrock AgentCore": 900000,
+ "Bedrock Data Automation Runtime": 900000,
+ "Data Pipeline": 900000,
+ "DataExchange": 900000,
+ "ECS": 900000,
+ "Glue": 900000,
+ "Kinesis": 900000,
+ "Kinesis Analytics V2": 900000,
+ "Kinesis Video Archived Media": 900000,
+ "Kinesis Video Media": 900000,
+ "Kinesis Video Signaling": 900000,
+ "Kinesis Video WebRTC Storage": 900000,
+ "Neptune Graph": 900000,
+ "neptunedata": 900000,
+ "Nova Act": 900000,
+ "QApps": 900000,
+ "QConnect": 900000,
+ "QuickSight": 900000,
+ "SageMaker Runtime": 900000,
+ "SagemakerJobRuntime": 900000,
+ "SFN": 900000,
+ "SQS": 900000,
+ "SWF": 900000,
+ "Timestream Query": 900000,
+ "Wisdom": 900000,
+ "API Gateway": 900000,
+ "ApiGatewayV2": 900000,
+ "AppIntegrations": 900000,
+ "AppStream": 900000,
+ "Athena": 900000,
+ "Auto Scaling": 900000,
+ "Batch": 900000,
+ "Bedrock": 900000,
+ "Bedrock Agent": 900000,
+ "Bedrock AgentCore Control": 900000,
+ "CloudFormation": 900000,
+ "CloudWatch": 900000,
+ "CodeBuild": 900000,
+ "CodeCatalyst": 900000,
+ "CodeDeploy": 900000,
+ "Config Service": 900000,
+ "Connect": 900000,
+ "DataBrew": 900000,
+ "DataZone": 900000,
+ "Device Farm": 900000,
+ "EC2": 900000,
+ "Elastic Load Balancing v2": 900000,
+ "EMR Serverless": 900000,
+ "GameLift": 900000,
+ "GameLiftStreams": 900000,
+ "IoT": 900000,
+ "IoT Data Plane": 900000,
+ "IoT Jobs Data Plane": 900000,
+ "IoTSecureTunneling": 900000,
+ "Lex Model Building Service": 900000,
+ "Lex Models V2": 900000,
+ "mgn": 900000,
+ "RDS": 900000,
+ "RDS Data": 900000,
+ "RTBFabric": 900000,
+ "SageMaker": 900000,
+ "SSM": 900000,
+ "Storage Gateway": 900000,
+ "WorkSpaces": 900000,
+ "WorkSpaces Web": 900000
+}
diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/customization/processors/DefaultReadWriteTimeoutExemptionProcessorTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/customization/processors/DefaultReadWriteTimeoutExemptionProcessorTest.java
new file mode 100644
index 000000000000..eb42cc3ca5e8
--- /dev/null
+++ b/codegen/src/test/java/software/amazon/awssdk/codegen/customization/processors/DefaultReadWriteTimeoutExemptionProcessorTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.codegen.customization.processors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
+import software.amazon.awssdk.codegen.model.intermediate.Metadata;
+import software.amazon.awssdk.protocols.jsoncore.JsonNode;
+import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;
+
+class DefaultReadWriteTimeoutExemptionProcessorTest {
+
+ private static final String EXEMPTIONS_RESOURCE = "software/amazon/awssdk/codegen/default-read-write-timeout-exemptions.json";
+ private static final Pattern SERVICE_ID = Pattern.compile("\"serviceId\"\\s*:\\s*\"([^\"]+)\"");
+
+ @ParameterizedTest
+ @MethodSource("exemptionEntries")
+ void postprocess_serviceInArtifact_bakesExpectedTier(String serviceId, long expectedMillis) {
+ IntermediateModel model = modelWithServiceId(serviceId);
+
+ new DefaultReadWriteTimeoutExemptionProcessor().postprocess(model);
+
+ assertThat(model.getMetadata().getDefaultReadWriteTimeoutMillis()).isEqualTo(expectedMillis);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"sqs", "s3", "lambda", "kinesis", "CODEARTIFACT", "MGN", "Sqs"})
+ void postprocess_misCasedServiceId_bakesNothing(String misCasedServiceId) {
+ IntermediateModel model = modelWithServiceId(misCasedServiceId);
+
+ new DefaultReadWriteTimeoutExemptionProcessor().postprocess(model);
+
+ assertThat(model.getMetadata().getDefaultReadWriteTimeoutMillis()).isNull();
+ }
+
+ @Test
+ void postprocess_serviceNotInArtifact_bakesNothing() {
+ IntermediateModel model = modelWithServiceId("Not A Real Service");
+
+ new DefaultReadWriteTimeoutExemptionProcessor().postprocess(model);
+
+ assertThat(model.getMetadata().getDefaultReadWriteTimeoutMillis()).isNull();
+ }
+
+ @Test
+ void validateArtifactKeys_everyKeyMatchesARealServiceId() throws IOException {
+ new DefaultReadWriteTimeoutExemptionProcessor().validateArtifactKeys(realServiceIds());
+ }
+
+ @Test
+ void validateArtifactKeys_keyMatchingNoServiceId_throws() {
+ DefaultReadWriteTimeoutExemptionProcessor processor =
+ new DefaultReadWriteTimeoutExemptionProcessor(Collections.singletonMap("sqs", 900000L));
+
+ assertThatThrownBy(() -> processor.validateArtifactKeys(Collections.singleton("SQS")))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("sqs");
+ }
+
+ private static IntermediateModel modelWithServiceId(String serviceId) {
+ IntermediateModel model = new IntermediateModel();
+ model.setMetadata(new Metadata().withServiceId(serviceId));
+ return model;
+ }
+
+ private static Stream exemptionEntries() throws IOException {
+ try (InputStream stream = DefaultReadWriteTimeoutExemptionProcessorTest.class.getClassLoader()
+ .getResourceAsStream(EXEMPTIONS_RESOURCE)) {
+ Map artifact = JsonNodeParser.create().parse(stream).asObject();
+ return artifact.entrySet().stream()
+ .map(e -> Arguments.of(e.getKey(), Long.parseLong(e.getValue().asNumber())));
+ }
+ }
+
+ private static Set realServiceIds() throws IOException {
+ Path servicesDir = locateServicesDir();
+ Set serviceIds = new HashSet<>();
+ try (DirectoryStream modules = Files.newDirectoryStream(servicesDir)) {
+ for (Path module : modules) {
+ Path model = module.resolve("src/main/resources/codegen-resources/service-2.json");
+ if (Files.isRegularFile(model)) {
+ extractServiceId(model).ifPresent(serviceIds::add);
+ }
+ }
+ }
+ assertThat(serviceIds).as("expected to harvest serviceIds from the service models").isNotEmpty();
+ return serviceIds;
+ }
+
+ private static Optional extractServiceId(Path serviceModel) throws IOException {
+ try (BufferedReader reader = Files.newBufferedReader(serviceModel, StandardCharsets.UTF_8)) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ Matcher matcher = SERVICE_ID.matcher(line);
+ if (matcher.find()) {
+ return Optional.of(matcher.group(1));
+ }
+ }
+ }
+ return Optional.empty();
+ }
+
+ private static Path locateServicesDir() {
+ for (Path candidate : new Path[] {Paths.get("..", "services"), Paths.get("services"), Paths.get("..", "..", "services")}) {
+ if (Files.isDirectory(candidate)) {
+ return candidate;
+ }
+ }
+ throw new IllegalStateException("Could not locate the services/ directory from " + Paths.get("").toAbsolutePath());
+ }
+}
diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassReadWriteTimeoutTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassReadWriteTimeoutTest.java
new file mode 100644
index 000000000000..9dabf03da41e
--- /dev/null
+++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassReadWriteTimeoutTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.codegen.poet.builder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static software.amazon.awssdk.codegen.poet.PoetUtils.buildJavaFile;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
+import software.amazon.awssdk.codegen.poet.ClientTestModels;
+
+/**
+ * Emission cases for the codegen-baked read/write timeout tier.
+ */
+class BaseClientBuilderClassReadWriteTimeoutTest {
+
+ @Test
+ void serviceHttpConfig_fullyExemptTier_bakesDurationZero() {
+ IntermediateModel model = ClientTestModels.queryServiceModels();
+ model.getMetadata().setDefaultReadWriteTimeoutMillis(-1L);
+
+ assertThat(generate(model))
+ .contains(".put(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, Duration.ZERO)");
+ }
+
+ @Test
+ void serviceHttpConfig_partialTier_bakesDurationMillis() {
+ IntermediateModel model = ClientTestModels.queryServiceModels();
+ model.getMetadata().setDefaultReadWriteTimeoutMillis(900000L);
+
+ assertThat(generate(model))
+ .contains(".put(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, Duration.ofMillis(900000L))");
+ }
+
+ @Test
+ void serviceHttpConfig_noTierBaked_omitsFallbackTimeout() {
+ IntermediateModel model = ClientTestModels.queryServiceModels();
+ model.getMetadata().setDefaultReadWriteTimeoutMillis(null);
+
+ assertThat(generate(model)).doesNotContain("SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT");
+ }
+
+ private static String generate(IntermediateModel model) {
+ StringBuilder output = new StringBuilder();
+ try {
+ buildJavaFile(new BaseClientBuilderClass(model)).writeTo(output);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ return output.toString();
+ }
+}
diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassTest.java
index 3a0f184b1462..a48a74c9163c 100644
--- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassTest.java
+++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassTest.java
@@ -107,6 +107,15 @@ void baseClientBuilderClass_noRegionEndpointRules() {
"test-no-region-client-builder-class.java");
}
+ @Test
+ void baseClientBuilderClassWithReadWriteTimeout() {
+ IntermediateModel model = serviceWithH2();
+ // Simulate the exemption processor baking a 15-minute partial tier; verify it composes with the existing H2
+ // serviceHttpConfig content.
+ model.getMetadata().setDefaultReadWriteTimeoutMillis(900000L);
+ validateBaseClientBuilderClassGeneration(model, "test-read-write-timeout-service-client-builder-class.java");
+ }
+
private void validateBaseClientBuilderClassGeneration(IntermediateModel model, String expectedClassName) {
validateGeneration(BaseClientBuilderClass::new, model, expectedClassName);
}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-internal-defaults-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-internal-defaults-class.java
index 9f269bf6e33f..d4a7d48d2b92 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-internal-defaults-class.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-internal-defaults-class.java
@@ -77,6 +77,7 @@ protected final SdkClientConfiguration mergeInternalDefaults(SdkClientConfigurat
c.option(SdkClientOption.INTERNAL_USER_AGENT, "md/foobar");
c.option(SdkClientOption.DEFAULT_RETRY_MODE, RetryMode.STANDARD);
c.option(SdkClientOption.DEFAULT_NEW_RETRIES_2026, true);
+ c.option(SdkClientOption.DEFAULT_ENABLE_SOCKET_TIMEOUT_2026, true);
});
}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-read-write-timeout-service-client-builder-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-read-write-timeout-service-client-builder-class.java
new file mode 100644
index 000000000000..765fa5789e03
--- /dev/null
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-read-write-timeout-service-client-builder-class.java
@@ -0,0 +1,250 @@
+package software.amazon.awssdk.services.h2;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Consumer;
+import software.amazon.awssdk.annotations.Generated;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.awscore.auth.AuthSchemePreferenceResolver;
+import software.amazon.awssdk.awscore.client.builder.AwsDefaultClientBuilder;
+import software.amazon.awssdk.awscore.client.config.AwsClientOption;
+import software.amazon.awssdk.awscore.endpoint.AwsClientEndpointProvider;
+import software.amazon.awssdk.awscore.endpoints.AwsEndpointAttribute;
+import software.amazon.awssdk.awscore.endpoints.authscheme.EndpointAuthScheme;
+import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4AuthScheme;
+import software.amazon.awssdk.awscore.retry.AwsRetryStrategy;
+import software.amazon.awssdk.core.ClientEndpointProvider;
+import software.amazon.awssdk.core.SdkPlugin;
+import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
+import software.amazon.awssdk.core.client.config.SdkClientConfiguration;
+import software.amazon.awssdk.core.client.config.SdkClientOption;
+import software.amazon.awssdk.core.exception.SdkClientException;
+import software.amazon.awssdk.core.interceptor.ClasspathInterceptorChainFactory;
+import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
+import software.amazon.awssdk.core.retry.RetryMode;
+import software.amazon.awssdk.endpoints.Endpoint;
+import software.amazon.awssdk.http.Protocol;
+import software.amazon.awssdk.http.ProtocolNegotiation;
+import software.amazon.awssdk.http.SdkHttpConfigurationOption;
+import software.amazon.awssdk.http.auth.aws.scheme.AwsV4AuthScheme;
+import software.amazon.awssdk.http.auth.scheme.NoAuthAuthScheme;
+import software.amazon.awssdk.http.auth.spi.scheme.AuthScheme;
+import software.amazon.awssdk.identity.spi.IdentityProvider;
+import software.amazon.awssdk.identity.spi.IdentityProviders;
+import software.amazon.awssdk.protocols.json.internal.unmarshall.SdkClientJsonProtocolAdvancedOption;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.retries.api.RetryStrategy;
+import software.amazon.awssdk.services.h2.auth.scheme.H2AuthSchemeProvider;
+import software.amazon.awssdk.services.h2.endpoints.H2EndpointParams;
+import software.amazon.awssdk.services.h2.endpoints.H2EndpointProvider;
+import software.amazon.awssdk.services.h2.internal.H2ServiceClientConfigurationBuilder;
+import software.amazon.awssdk.utils.AttributeMap;
+import software.amazon.awssdk.utils.CollectionUtils;
+import software.amazon.awssdk.utils.CompletableFutureUtils;
+
+/**
+ * Internal base class for {@link DefaultH2ClientBuilder} and {@link DefaultH2AsyncClientBuilder}.
+ */
+@Generated("software.amazon.awssdk:codegen")
+@SdkInternalApi
+abstract class DefaultH2BaseClientBuilder, C> extends AwsDefaultClientBuilder {
+ private final Map> additionalAuthSchemes = new HashMap<>();
+
+ @Override
+ protected final String serviceEndpointPrefix() {
+ return "h2-service";
+ }
+
+ @Override
+ protected final String serviceName() {
+ return "H2";
+ }
+
+ @Override
+ protected final SdkClientConfiguration mergeServiceDefaults(SdkClientConfiguration config) {
+ return config.merge(c -> {
+ c.option(SdkClientOption.ENDPOINT_PROVIDER, defaultEndpointProvider())
+ .option(SdkClientOption.AUTH_SCHEME_PROVIDER, defaultAuthSchemeProvider(config))
+ .option(SdkClientOption.AUTH_SCHEMES, authSchemes())
+ .option(SdkClientOption.CRC32_FROM_COMPRESSED_DATA_ENABLED, false);
+ });
+ }
+
+ @Override
+ protected final SdkClientConfiguration finalizeServiceConfiguration(SdkClientConfiguration config) {
+ List endpointInterceptors = new ArrayList<>();
+ ClasspathInterceptorChainFactory interceptorFactory = new ClasspathInterceptorChainFactory();
+ List interceptors = interceptorFactory
+ .getInterceptors("software/amazon/awssdk/services/h2/execution.interceptors");
+ List additionalInterceptors = new ArrayList<>();
+ interceptors = CollectionUtils.mergeLists(endpointInterceptors, interceptors);
+ interceptors = CollectionUtils.mergeLists(interceptors, additionalInterceptors);
+ interceptors = CollectionUtils.mergeLists(interceptors, config.option(SdkClientOption.EXECUTION_INTERCEPTORS));
+ SdkClientConfiguration.Builder builder = config.toBuilder();
+ builder.lazyOption(SdkClientOption.IDENTITY_PROVIDERS, c -> {
+ IdentityProviders.Builder result = IdentityProviders.builder();
+ IdentityProvider> credentialsIdentityProvider = c.get(AwsClientOption.CREDENTIALS_IDENTITY_PROVIDER);
+ if (credentialsIdentityProvider != null) {
+ result.putIdentityProvider(credentialsIdentityProvider);
+ }
+ return result.build();
+ });
+ builder.option(SdkClientOption.EXECUTION_INTERCEPTORS, interceptors);
+ builder.lazyOptionIfAbsent(
+ SdkClientOption.CLIENT_ENDPOINT_PROVIDER,
+ c -> {
+ Optional overrideEndpoint = AwsClientEndpointProvider.builder()
+ .serviceEndpointOverrideEnvironmentVariable("AWS_ENDPOINT_URL_H2_SERVICE")
+ .serviceEndpointOverrideSystemProperty("aws.endpointUrlH2").serviceProfileProperty("h2_service")
+ .profileFile(c.get(SdkClientOption.PROFILE_FILE_SUPPLIER))
+ .profileName(c.get(SdkClientOption.PROFILE_NAME)).resolveFromOverrides();
+ if (overrideEndpoint.isPresent()) {
+ return ClientEndpointProvider.create(overrideEndpoint.get(), true);
+ }
+ URI clientEndpointUri = null;
+ Region region = c.get(AwsClientOption.AWS_REGION);
+ try {
+ H2EndpointParams endpointParams = H2EndpointParams.builder().region(region).build();
+ Endpoint endpoint = CompletableFutureUtils.joinLikeSync(defaultEndpointProvider().resolveEndpoint(
+ endpointParams));
+ clientEndpointUri = endpoint.url();
+ } catch (Exception e) {
+ // Endpoint resolution failed. This is expected for services with required parameters
+ // beyond region, dualstack, and FIPS. Use a placeholder that will be replaced at request time.
+ return ClientEndpointProvider.create(URI.create("https://localhost"), false);
+ }
+ if (clientEndpointUri.getHost() == null) {
+ throw SdkClientException.create("Configured region (" + region + ") resulted in an invalid URI: "
+ + clientEndpointUri + ". This is usually caused by an invalid region configuration.");
+ }
+ return ClientEndpointProvider.create(clientEndpointUri, false);
+ });
+ builder.lazyOptionIfAbsent(
+ AwsClientOption.SIGNING_REGION,
+ c -> {
+ Region region = c.get(AwsClientOption.AWS_REGION);
+ try {
+ H2EndpointParams endpointParams = H2EndpointParams.builder().region(region).build();
+ Endpoint endpoint = CompletableFutureUtils.joinLikeSync(defaultEndpointProvider().resolveEndpoint(
+ endpointParams));
+ List authSchemes = endpoint.attribute(AwsEndpointAttribute.AUTH_SCHEMES);
+ if (authSchemes != null && !authSchemes.isEmpty()) {
+ EndpointAuthScheme firstScheme = authSchemes.get(0);
+ if (firstScheme instanceof SigV4AuthScheme) {
+ String signingRegion = ((SigV4AuthScheme) firstScheme).signingRegion();
+ if (signingRegion != null) {
+ return Region.of(signingRegion);
+ }
+ }
+ }
+ } catch (Exception e) {
+ // Endpoint resolution failed. Fall back to using the client region as signing region.
+ }
+ return region;
+ });
+ builder.option(SdkClientJsonProtocolAdvancedOption.ENABLE_FAST_UNMARSHALLER, true);
+ return builder.build();
+ }
+
+ @Override
+ protected final String signingName() {
+ return "h2-service";
+ }
+
+ private H2EndpointProvider defaultEndpointProvider() {
+ return H2EndpointProvider.defaultProvider();
+ }
+
+ public B authSchemeProvider(H2AuthSchemeProvider authSchemeProvider) {
+ clientConfiguration.option(SdkClientOption.AUTH_SCHEME_PROVIDER, authSchemeProvider);
+ return thisBuilder();
+ }
+
+ private H2AuthSchemeProvider defaultAuthSchemeProvider(SdkClientConfiguration config) {
+ AuthSchemePreferenceResolver authSchemePreferenceProvider = AuthSchemePreferenceResolver.builder()
+ .profileFile(config.option(SdkClientOption.PROFILE_FILE_SUPPLIER))
+ .profileName(config.option(SdkClientOption.PROFILE_NAME)).build();
+ List preferences = authSchemePreferenceProvider.resolveAuthSchemePreference();
+ if (!preferences.isEmpty()) {
+ return H2AuthSchemeProvider.defaultProvider(preferences);
+ }
+ return H2AuthSchemeProvider.defaultProvider();
+ }
+
+ @Override
+ public B putAuthScheme(AuthScheme> authScheme) {
+ additionalAuthSchemes.put(authScheme.schemeId(), authScheme);
+ return thisBuilder();
+ }
+
+ private Map> authSchemes() {
+ Map> schemes = new HashMap<>(2 + this.additionalAuthSchemes.size());
+ AwsV4AuthScheme awsV4AuthScheme = AwsV4AuthScheme.create();
+ schemes.put(awsV4AuthScheme.schemeId(), awsV4AuthScheme);
+ NoAuthAuthScheme noAuthAuthScheme = NoAuthAuthScheme.create();
+ schemes.put(noAuthAuthScheme.schemeId(), noAuthAuthScheme);
+ schemes.putAll(this.additionalAuthSchemes);
+ return schemes;
+ }
+
+ @Override
+ protected final AttributeMap serviceHttpConfig() {
+ AttributeMap result = AttributeMap.empty();
+ return result.merge(AttributeMap.builder().put(SdkHttpConfigurationOption.PROTOCOL, Protocol.HTTP2)
+ .put(SdkHttpConfigurationOption.PROTOCOL_NEGOTIATION, ProtocolNegotiation.ALPN)
+ .put(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, Duration.ofMillis(900000L)).build());
+ }
+
+ @Override
+ protected SdkClientConfiguration invokePlugins(SdkClientConfiguration config) {
+ List internalPlugins = internalPlugins(config);
+ List externalPlugins = plugins();
+ if (internalPlugins.isEmpty() && externalPlugins.isEmpty()) {
+ return config;
+ }
+ List plugins = CollectionUtils.mergeLists(internalPlugins, externalPlugins);
+ SdkClientConfiguration.Builder configuration = config.toBuilder();
+ H2ServiceClientConfigurationBuilder serviceConfigBuilder = new H2ServiceClientConfigurationBuilder(configuration);
+ for (SdkPlugin plugin : plugins) {
+ plugin.configureClient(serviceConfigBuilder);
+ }
+ updateRetryStrategyClientConfiguration(configuration);
+ return configuration.build();
+ }
+
+ private void updateRetryStrategyClientConfiguration(SdkClientConfiguration.Builder configuration) {
+ ClientOverrideConfiguration.Builder builder = configuration.asOverrideConfigurationBuilder();
+ RetryMode retryMode = builder.retryMode();
+ if (retryMode != null) {
+ configuration.option(SdkClientOption.RETRY_STRATEGY, AwsRetryStrategy.forRetryMode(retryMode));
+ } else {
+ Consumer> configurator = builder.retryStrategyConfigurator();
+ if (configurator != null) {
+ RetryStrategy.Builder, ?> defaultBuilder = AwsRetryStrategy.defaultRetryStrategy().toBuilder();
+ configurator.accept(defaultBuilder);
+ configuration.option(SdkClientOption.RETRY_STRATEGY, defaultBuilder.build());
+ } else {
+ RetryStrategy retryStrategy = builder.retryStrategy();
+ if (retryStrategy != null) {
+ configuration.option(SdkClientOption.RETRY_STRATEGY, retryStrategy);
+ }
+ }
+ }
+ configuration.option(SdkClientOption.CONFIGURED_RETRY_MODE, null);
+ configuration.option(SdkClientOption.CONFIGURED_RETRY_STRATEGY, null);
+ configuration.option(SdkClientOption.CONFIGURED_RETRY_CONFIGURATOR, null);
+ }
+
+ private List internalPlugins(SdkClientConfiguration config) {
+ return Collections.emptyList();
+ }
+
+ protected static void validateClientOptions(SdkClientConfiguration c) {
+ }
+}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/internalconfig/customization.config b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/internalconfig/customization.config
index 594a4aceb54d..f37cc8509cb1 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/internalconfig/customization.config
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/internalconfig/customization.config
@@ -4,5 +4,6 @@
},
"userAgent": "md/foobar",
"defaultRetryMode": "STANDARD",
- "defaultNewRetries2026": "true"
+ "defaultNewRetries2026": "true",
+ "defaultEnableSocketTimeout2026": "true"
}
diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java
index 92036575eeb1..325585dc6d7b 100644
--- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java
+++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java
@@ -21,6 +21,7 @@
import static software.amazon.awssdk.core.client.config.SdkClientOption.RETRY_STRATEGY;
import java.net.URI;
+import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@@ -50,6 +51,7 @@
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
import software.amazon.awssdk.core.client.config.SdkClientConfiguration;
import software.amazon.awssdk.core.client.config.SdkClientOption;
+import software.amazon.awssdk.core.http.EnableDefaultSocketTimeout2026Resolver;
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
import software.amazon.awssdk.core.internal.SdkInternalTestAdvancedClientOption;
import software.amazon.awssdk.core.internal.retry.SdkDefaultRetryStrategy;
@@ -57,6 +59,7 @@
import software.amazon.awssdk.core.retry.RetryMode;
import software.amazon.awssdk.core.retry.RetryPolicy;
import software.amazon.awssdk.http.SdkHttpClient;
+import software.amazon.awssdk.http.SdkHttpConfigurationOption;
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;
import software.amazon.awssdk.identity.spi.IdentityProvider;
@@ -99,6 +102,12 @@ public abstract class AwsDefaultClientBuilder
*/
private AttributeMap resolveHttpClientConfig(LazyValueSource config) {
- AttributeMap attributeMap = serviceHttpConfig();
+ AttributeMap attributeMap = applyDefaultReadWriteTimeout(config, serviceHttpConfig());
return mergeSmartHttpDefaults(config, attributeMap);
}
+ /**
+ * Applies the {@code AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026} rollout gate to the codegen-baked
+ * {@link SdkHttpConfigurationOption#SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT} contributed by {@link #serviceHttpConfig()}.
+ * The gate resolves from the {@code AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026} environment variable/system property, else the
+ * codegen-baked {@link SdkClientOption#DEFAULT_ENABLE_SOCKET_TIMEOUT_2026} default, else off.
+ *
+ * When the gate is on, an unlisted service (nothing baked) gets the flat 5-minute default and a baked tier is kept as-is
+ * ({@link Duration#ZERO} for fully-exempt, 15 minutes for partial). When the gate is off, a baked positive tier (partial)
+ * is forced to {@link Duration#ZERO} so it cannot apply; otherwise the option is left untouched. Leaving it absent for an
+ * unlisted, gated-off service is equivalent to {@link Duration#ZERO}: the gate being off means the environment variable is
+ * not truthy, so the HTTP client's option-absent path applies nothing either way.
+ */
+ private AttributeMap applyDefaultReadWriteTimeout(LazyValueSource config, AttributeMap serviceHttpConfig) {
+ boolean gateEnabled = new EnableDefaultSocketTimeout2026Resolver()
+ .defaultEnableSocketTimeout2026(config.get(SdkClientOption.DEFAULT_ENABLE_SOCKET_TIMEOUT_2026))
+ .resolve();
+
+ Duration bakedTier = serviceHttpConfig.get(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT);
+
+ if (gateEnabled) {
+ if (bakedTier != null) {
+ return serviceHttpConfig;
+ }
+ return serviceHttpConfig.toBuilder()
+ .put(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT,
+ DEFAULT_READ_WRITE_TIMEOUT)
+ .build();
+ }
+
+ if (bakedTier != null && !bakedTier.isZero()) {
+ return serviceHttpConfig.toBuilder()
+ .put(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, Duration.ZERO)
+ .build();
+ }
+ return serviceHttpConfig;
+ }
+
/**
* Optionally overridden by child classes to define service-specific HTTP configuration defaults.
*/
diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilderReadWriteTimeoutTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilderReadWriteTimeoutTest.java
new file mode 100644
index 000000000000..7184bc194fa9
--- /dev/null
+++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilderReadWriteTimeoutTest.java
@@ -0,0 +1,205 @@
+/*
+ * 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.awscore.client.builder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static software.amazon.awssdk.awscore.client.config.AwsAdvancedClientOption.ENABLE_DEFAULT_REGION_DETECTION;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider;
+import software.amazon.awssdk.awscore.client.config.AwsClientOption;
+import software.amazon.awssdk.awscore.internal.defaultsmode.AutoDefaultsModeDiscovery;
+import software.amazon.awssdk.core.ClientEndpointProvider;
+import software.amazon.awssdk.core.SdkSystemSetting;
+import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
+import software.amazon.awssdk.core.client.config.SdkClientConfiguration;
+import software.amazon.awssdk.core.client.config.SdkClientOption;
+import software.amazon.awssdk.http.SdkHttpClient;
+import software.amazon.awssdk.http.SdkHttpConfigurationOption;
+import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.utils.AttributeMap;
+
+/**
+ * Verifies the {@code AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026} rollout gate applied in
+ * {@link AwsDefaultClientBuilder#resolveHttpClientConfig} to the codegen-baked
+ * {@link SdkHttpConfigurationOption#SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT}.
+ */
+@ExtendWith(MockitoExtension.class)
+class AwsDefaultClientBuilderReadWriteTimeoutTest {
+
+ private static final String GATE_PROPERTY = SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.property();
+ private static final Duration PARTIAL_TIER = Duration.ofMinutes(15);
+ private static final Duration FLAT_DEFAULT = Duration.ofMinutes(5);
+
+ @Mock(lenient = true)
+ private SdkHttpClient.Builder defaultHttpClientBuilder;
+
+ @Mock(lenient = true)
+ private SdkAsyncHttpClient.Builder defaultAsyncHttpClientFactory;
+
+ @Mock(lenient = true)
+ private AutoDefaultsModeDiscovery autoModeDiscovery;
+
+ private String savedProperty;
+
+ @BeforeEach
+ void setup() {
+ savedProperty = System.getProperty(GATE_PROPERTY);
+ System.clearProperty(GATE_PROPERTY);
+ }
+
+ @AfterEach
+ void teardown() {
+ if (savedProperty != null) {
+ System.setProperty(GATE_PROPERTY, savedProperty);
+ } else {
+ System.clearProperty(GATE_PROPERTY);
+ }
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("gateScenarios")
+ void resolvesReadWriteTimeout(String scenario, String gateProperty, boolean codegenGateDefault,
+ Duration bakedTier, Duration expected) {
+ if (gateProperty != null) {
+ System.setProperty(GATE_PROPERTY, gateProperty);
+ }
+ AttributeMap serviceHttpConfig = bakedTier == null ? AttributeMap.empty() : bakedServiceHttpConfig(bakedTier);
+
+ AttributeMap resolved = resolvedServiceDefaults(serviceHttpConfig, codegenGateDefault);
+
+ assertThat(resolved.get(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT)).isEqualTo(expected);
+ }
+
+ @Test
+ void unlistedService_gateOff_leavesOptionAbsent() {
+ AttributeMap resolved = resolvedServiceDefaults(AttributeMap.empty(), false);
+
+ assertThat(resolved.containsKey(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT)).isFalse();
+ }
+
+ private static Stream gateScenarios() {
+ return Stream.of(
+ Arguments.of("gate off, partial tier baked -> overridden to ZERO", null, false, PARTIAL_TIER, Duration.ZERO),
+ Arguments.of("gate on, unlisted service -> flat 5-minute default", "true", false, null, FLAT_DEFAULT),
+ Arguments.of("gate on, fully-exempt tier baked -> ZERO", "true", false, Duration.ZERO, Duration.ZERO),
+ Arguments.of("gate on, partial tier baked -> 15 minutes", "true", false, PARTIAL_TIER, PARTIAL_TIER),
+ Arguments.of("gate on via codegen default, unlisted -> flat 5-minute default", null, true, null, FLAT_DEFAULT),
+ Arguments.of("gate property false overrides codegen default -> ZERO", "false", true, PARTIAL_TIER, Duration.ZERO)
+ );
+ }
+
+ private static AttributeMap bakedServiceHttpConfig(Duration tier) {
+ return AttributeMap.builder()
+ .put(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, tier)
+ .build();
+ }
+
+ private AttributeMap resolvedServiceDefaults(AttributeMap bakedServiceHttpConfig, boolean codegenGateDefault) {
+ AtomicReference captured = new AtomicReference<>();
+ ClientOverrideConfiguration overrideConfig =
+ ClientOverrideConfiguration.builder()
+ .putAdvancedOption(ENABLE_DEFAULT_REGION_DETECTION, false)
+ .build();
+
+ new TestClientBuilder(bakedServiceHttpConfig, codegenGateDefault)
+ .credentialsProvider(AnonymousCredentialsProvider.create())
+ .overrideConfiguration(overrideConfig)
+ .region(Region.US_WEST_1)
+ .httpClientBuilder((SdkHttpClient.Builder) serviceDefaults -> {
+ captured.set(serviceDefaults);
+ return mock(SdkHttpClient.class);
+ })
+ .build();
+
+ return captured.get();
+ }
+
+ private static class TestClient {
+ }
+
+ private class TestClientBuilder extends AwsDefaultClientBuilder
+ implements AwsClientBuilder {
+
+ private final AttributeMap bakedServiceHttpConfig;
+ private final boolean codegenGateDefault;
+
+ TestClientBuilder(AttributeMap bakedServiceHttpConfig, boolean codegenGateDefault) {
+ super(defaultHttpClientBuilder, defaultAsyncHttpClientFactory, autoModeDiscovery);
+ this.bakedServiceHttpConfig = bakedServiceHttpConfig;
+ this.codegenGateDefault = codegenGateDefault;
+ }
+
+ @Override
+ protected TestClient buildClient() {
+ syncClientConfiguration();
+ return new TestClient();
+ }
+
+ @Override
+ protected SdkClientConfiguration mergeInternalDefaults(SdkClientConfiguration config) {
+ if (!codegenGateDefault) {
+ return config;
+ }
+ return config.merge(c -> c.option(SdkClientOption.DEFAULT_ENABLE_SOCKET_TIMEOUT_2026, true));
+ }
+
+ @Override
+ protected SdkClientConfiguration finalizeServiceConfiguration(SdkClientConfiguration config) {
+ return config.toBuilder()
+ .lazyOptionIfAbsent(SdkClientOption.CLIENT_ENDPOINT_PROVIDER, c -> {
+ URI endpoint = URI.create("https://" + serviceEndpointPrefix() + "."
+ + c.get(AwsClientOption.AWS_REGION) + ".amazonaws.com");
+ return ClientEndpointProvider.create(endpoint, false);
+ })
+ .build();
+ }
+
+ @Override
+ protected AttributeMap serviceHttpConfig() {
+ return bakedServiceHttpConfig;
+ }
+
+ @Override
+ protected String serviceEndpointPrefix() {
+ return "test";
+ }
+
+ @Override
+ protected String signingName() {
+ return "test";
+ }
+
+ @Override
+ protected String serviceName() {
+ return "test";
+ }
+ }
+}
diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java
index e941ed2fd905..0da073035e64 100644
--- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java
+++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java
@@ -276,7 +276,17 @@ public enum SdkSystemSetting implements SystemSetting {
* defaults including STANDARD as the default retry mode, reduced base backoff delays, differentiated token bucket
* costs, and other v2.1 retry specification changes. When {@code false} (the default), the SDK uses v2.0 retry behavior.
*/
- AWS_NEW_RETRIES_2026("aws.newRetries2026", null);
+ AWS_NEW_RETRIES_2026("aws.newRetries2026", null),
+
+ /**
+ * Configure whether a default read/write inactivity timeout is applied to HTTP clients that do not enforce one of their
+ * own (currently the AWS CRT-based clients). When {@code true}, such clients shut down a connection that transfers no
+ * bytes for the resolved timeout window. When {@code false} (the default), no default read/write timeout is applied.
+ *
+ * This setting is not intended to be used by end users. It gates an interim rollout and is subject to removal in a
+ * future release.
+ */
+ AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026("aws.enableDefaultSocketTimeout2026", null);
private final String systemProperty;
private final String defaultValue;
diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java
index 20ea0d01c3b9..199232a088b3 100644
--- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java
+++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java
@@ -311,6 +311,13 @@ public final class SdkClientOption extends ClientOption {
*/
public static final SdkClientOption DEFAULT_NEW_RETRIES_2026 = new SdkClientOption<>(Boolean.class);
+ /**
+ * Option to specify the default for the {@code AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026} feature gate for the SDK client.
+ * This option is not intended to be set by end users. It gates an interim rollout and is subject to removal in a future
+ * release.
+ */
+ public static final SdkClientOption DEFAULT_ENABLE_SOCKET_TIMEOUT_2026 = new SdkClientOption<>(Boolean.class);
+
/**
* Whether retries 2.1 behavior is enabled.
*/
diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/EnableDefaultSocketTimeout2026Resolver.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/EnableDefaultSocketTimeout2026Resolver.java
new file mode 100644
index 000000000000..4a37c9a80ea7
--- /dev/null
+++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/EnableDefaultSocketTimeout2026Resolver.java
@@ -0,0 +1,57 @@
+/*
+ * 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.core.http;
+
+import java.util.Optional;
+import software.amazon.awssdk.annotations.SdkProtectedApi;
+import software.amazon.awssdk.core.SdkSystemSetting;
+
+/**
+ * Resolver for the {@link SdkSystemSetting#AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026} that supports setting a fallback value if not
+ * defined in the environment or system properties.
+ */
+@SdkProtectedApi
+public final class EnableDefaultSocketTimeout2026Resolver {
+ private Boolean defaultEnableSocketTimeout2026;
+
+ /**
+ * The default value for {@code AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026} if not configured via
+ * {@link SdkSystemSetting#AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026}.
+ *
+ * @return This resolver for method chaining.
+ */
+ public EnableDefaultSocketTimeout2026Resolver defaultEnableSocketTimeout2026(Boolean defaultEnableSocketTimeout2026) {
+ this.defaultEnableSocketTimeout2026 = defaultEnableSocketTimeout2026;
+ return this;
+ }
+
+ /**
+ * Resolve whether a default read/write timeout is applied.
+ */
+ public boolean resolve() {
+ Optional envConfig = SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.getBooleanValue();
+
+ if (envConfig.isPresent()) {
+ return envConfig.get();
+ }
+
+ if (defaultEnableSocketTimeout2026 != null) {
+ return defaultEnableSocketTimeout2026;
+ }
+
+ return false;
+ }
+}
diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/EnableDefaultSocketTimeout2026ResolverTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/EnableDefaultSocketTimeout2026ResolverTest.java
new file mode 100644
index 000000000000..cc1d2757d754
--- /dev/null
+++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/EnableDefaultSocketTimeout2026ResolverTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.core.http;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.stream.Stream;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import software.amazon.awssdk.core.SdkSystemSetting;
+import software.amazon.awssdk.testutils.EnvironmentVariableHelper;
+
+public class EnableDefaultSocketTimeout2026ResolverTest {
+ private static String enableDefaultSocketTimeout2026Save;
+
+ @BeforeAll
+ static void setup() {
+ enableDefaultSocketTimeout2026Save = System.getProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.property());
+ }
+
+ @AfterAll
+ static void teardown() {
+ if (enableDefaultSocketTimeout2026Save != null) {
+ System.setProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.property(),
+ enableDefaultSocketTimeout2026Save);
+ } else {
+ System.clearProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.property());
+ }
+ }
+
+ @BeforeEach
+ void methodSetup() {
+ System.clearProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.property());
+ }
+
+ @Test
+ void systemSetting_usesExpectedEnvironmentVariableAndSystemPropertyNames() {
+ assertThat(SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.environmentVariable())
+ .isEqualTo("AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026");
+ assertThat(SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.property())
+ .isEqualTo("aws.enableDefaultSocketTimeout2026");
+ }
+
+ @ParameterizedTest
+ @MethodSource("params")
+ void resolve_behavesCorrectly(TestParams params) {
+ EnvironmentVariableHelper.run((env) -> {
+ if (params.systemProperty != null) {
+ System.setProperty(SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.property(), params.systemProperty);
+ }
+
+ if (params.envVar != null) {
+ env.set(SdkSystemSetting.AWS_ENABLE_DEFAULT_SOCKET_TIMEOUT_2026.environmentVariable(), params.envVar);
+ }
+
+ EnableDefaultSocketTimeout2026Resolver resolver =
+ new EnableDefaultSocketTimeout2026Resolver().defaultEnableSocketTimeout2026(params.defaultEnableSocketTimeout2026);
+
+ assertThat(resolver.resolve()).isEqualTo(params.expected);
+ });
+ }
+
+ private static Stream params() {
+ return Stream.of(
+ // default
+ new TestParams().expected(false),
+
+ // precedence testing
+ new TestParams().systemProperty("true").defaultEnableSocketTimeout2026(true).expected(true),
+ new TestParams().systemProperty("false").defaultEnableSocketTimeout2026(true).expected(false),
+ new TestParams().envVar("true").defaultEnableSocketTimeout2026(true).expected(true),
+ new TestParams().envVar("false").defaultEnableSocketTimeout2026(true).expected(false),
+ new TestParams().defaultEnableSocketTimeout2026(true).expected(true),
+ new TestParams().defaultEnableSocketTimeout2026(false).expected(false)
+ );
+ }
+
+ private static class TestParams {
+ private String systemProperty;
+ private String envVar;
+ private Boolean defaultEnableSocketTimeout2026;
+ private boolean expected;
+
+ public TestParams systemProperty(String systemProperty) {
+ this.systemProperty = systemProperty;
+ return this;
+ }
+
+ public TestParams envVar(String envVar) {
+ this.envVar = envVar;
+ return this;
+ }
+
+ public TestParams defaultEnableSocketTimeout2026(Boolean defaultEnableSocketTimeout2026) {
+ this.defaultEnableSocketTimeout2026 = defaultEnableSocketTimeout2026;
+ return this;
+ }
+
+ public TestParams expected(boolean expected) {
+ this.expected = expected;
+ return this;
+ }
+ }
+}
diff --git a/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpConfigurationOption.java b/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpConfigurationOption.java
index e9efa8b1814b..69487fb28129 100644
--- a/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpConfigurationOption.java
+++ b/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpConfigurationOption.java
@@ -41,6 +41,16 @@ public final class SdkHttpConfigurationOption extends AttributeMap.Key {
public static final SdkHttpConfigurationOption WRITE_TIMEOUT =
new SdkHttpConfigurationOption<>("WriteTimeout", Duration.class);
+ /**
+ * SDK-internal. A fallback read/write timeout that is honored only by HTTP client implementations which do not
+ * enforce a read/write timeout of their own (currently the AWS CRT-based clients); implementations that already
+ * apply one, such as the Apache and Netty clients, ignore it. Unlike {@link #READ_TIMEOUT}/{@link #WRITE_TIMEOUT},
+ * which carry a caller-configured socket timeout, this value is resolved by the SDK and is not intended to be set by
+ * end users. {@link Duration#ZERO} means no timeout is applied (the service is fully exempt, or the rollout gate is off).
+ */
+ public static final SdkHttpConfigurationOption SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT =
+ new SdkHttpConfigurationOption<>("SdkInternalFallbackReadWriteTimeout", Duration.class);
+
/**
* Timeout for establishing a connection to a remote service.
*/
diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClient.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClient.java
index 1f2a35765893..79a150eabab8 100644
--- a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClient.java
+++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClient.java
@@ -165,17 +165,21 @@ public interface Builder extends SdkAsyncHttpClient.Builder proxyConfigurationBuilderConsumer);
/**
- * Configure the health checks for all connections established by this client.
+ * Configure the health checks for all connections established by this client. This is the CRT client's knob for a
+ * read/write inactivity timeout: a connection whose throughput stays below
+ * {@link ConnectionHealthConfiguration#minimumThroughputInBps()} for
+ * {@link ConnectionHealthConfiguration#minimumThroughputTimeout()} is considered unhealthy and shut down, failing the
+ * in-flight request with a retryable {@code IOException}.
*
- *
- * You can set a throughput threshold for a connection to be considered healthy.
- * If a connection falls below this threshold ({@link ConnectionHealthConfiguration#minimumThroughputInBps()
- * }) for the configurable amount
- * of time ({@link ConnectionHealthConfiguration#minimumThroughputTimeout()}),
- * then the connection is considered unhealthy and will be shut down.
+ *
To set a read/write inactivity timeout, use {@code minimumThroughputInBps(1L)} with
+ * {@code minimumThroughputTimeout(yourDuration)}: any byte moved in either direction resets the window, so the connection
+ * is shut down only after {@code yourDuration} elapses with no bytes transferred. The timeout has whole-second
+ * granularity and must be at least two seconds.
*
- *
- * Disabled by default.
+ *
When this client is created and managed by an AWS SDK service client, a default read/write inactivity timeout
+ * may be applied automatically, resolved per service by the SDK. When this client is built directly and supplied to a
+ * service client, or used standalone, no automatic default is applied; monitoring is enabled only by an explicit
+ * configuration set here, which always takes precedence over any SDK-applied default.
*
* @param healthChecksConfiguration The health checks config to use
* @return The builder of the method chaining.
diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClient.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClient.java
index 6eeab8a20242..9fe64121b2ca 100644
--- a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClient.java
+++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClient.java
@@ -221,17 +221,21 @@ public interface Builder extends SdkHttpClient.Builder
AwsCrtHttpClient.Builder proxyConfiguration(Consumer proxyConfigurationBuilderConsumer);
/**
- * Configure the health checks for all connections established by this client.
+ * Configure the health checks for all connections established by this client. This is the CRT client's knob for a
+ * read/write inactivity timeout: a connection whose throughput stays below
+ * {@link ConnectionHealthConfiguration#minimumThroughputInBps()} for
+ * {@link ConnectionHealthConfiguration#minimumThroughputTimeout()} is considered unhealthy and shut down, failing the
+ * in-flight request with a retryable {@code IOException}.
*
- *
- * You can set a throughput threshold for a connection to be considered healthy.
- * If a connection falls below this threshold ({@link ConnectionHealthConfiguration#minimumThroughputInBps()
- * }) for the configurable amount
- * of time ({@link ConnectionHealthConfiguration#minimumThroughputTimeout()}),
- * then the connection is considered unhealthy and will be shut down.
+ *
To set a read/write inactivity timeout, use {@code minimumThroughputInBps(1L)} with
+ * {@code minimumThroughputTimeout(yourDuration)}: any byte moved in either direction resets the window, so the connection
+ * is shut down only after {@code yourDuration} elapses with no bytes transferred. The timeout has whole-second
+ * granularity and must be at least two seconds.
*
- *
- * Disabled by default.
+ *
When this client is created and managed by an AWS SDK service client, a default read/write inactivity timeout
+ * may be applied automatically, resolved per service by the SDK. When this client is built directly and supplied to a
+ * service client, or used standalone, no automatic default is applied; monitoring is enabled only by an explicit
+ * configuration set here, which always takes precedence over any SDK-applied default.
*
* @param healthChecksConfiguration The health checks config to use
* @return The builder of the method chaining.
diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java
index 4d8dc88ba6f6..703a196dd102 100644
--- a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java
+++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java
@@ -15,13 +15,13 @@
package software.amazon.awssdk.http.crt;
-import static software.amazon.awssdk.crtcore.CrtConfigurationUtils.resolveHttpMonitoringOptions;
import static software.amazon.awssdk.crtcore.CrtConfigurationUtils.resolveProxy;
import static software.amazon.awssdk.http.SdkHttpConfigurationOption.PROTOCOL;
import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.buildSocketOptions;
import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.buildTlsConnectionOptions;
import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.resolveCipherPreference;
import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.resolveMinTlsVersion;
+import static software.amazon.awssdk.http.crt.internal.AwsCrtConfigurationUtils.resolveMonitoringOptions;
import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
import java.net.URI;
@@ -136,9 +136,9 @@ abstract class AwsCrtHttpClientBase implements SdkAutoCloseable {
this.readBufferSize = builder.getReadBufferSizeInBytes() == null ?
DEFAULT_STREAM_WINDOW_SIZE : builder.getReadBufferSizeInBytes();
this.maxStreamsPerEndpoint = config.get(SdkHttpConfigurationOption.MAX_CONNECTIONS);
- this.monitoringOptions =
- resolveHttpMonitoringOptions(builder.getConnectionHealthConfiguration())
- .orElse(null);
+ this.monitoringOptions = resolveMonitoringOptions(
+ builder.getConnectionHealthConfiguration(),
+ config.get(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT));
this.maxConnectionIdleInMilliseconds = config.get(SdkHttpConfigurationOption.CONNECTION_MAX_IDLE_TIMEOUT).toMillis();
this.connectionAcquisitionTimeout = config.get(SdkHttpConfigurationOption.CONNECTION_ACQUIRE_TIMEOUT).toMillis();
this.proxyOptions = resolveProxy(builder.getProxyConfiguration(), tlsContext).orElse(null);
diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtils.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtils.java
index c3de2a1aee2f..ce03372ae257 100644
--- a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtils.java
+++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtils.java
@@ -18,11 +18,14 @@
import java.time.Duration;
import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.crt.http.HttpMonitoringOptions;
import software.amazon.awssdk.crt.io.SocketOptions;
import software.amazon.awssdk.crt.io.TlsCipherPreference;
import software.amazon.awssdk.crt.io.TlsConnectionOptions;
import software.amazon.awssdk.crt.io.TlsContext;
import software.amazon.awssdk.crt.io.TlsContextOptions;
+import software.amazon.awssdk.crtcore.CrtConfigurationUtils;
+import software.amazon.awssdk.http.crt.ConnectionHealthConfiguration;
import software.amazon.awssdk.http.crt.TcpKeepAliveConfiguration;
import software.amazon.awssdk.http.crt.TlsVersion;
import software.amazon.awssdk.utils.Logger;
@@ -32,9 +35,49 @@
public final class AwsCrtConfigurationUtils {
private static final Logger log = Logger.loggerFor(AwsCrtConfigurationUtils.class);
+ // CRT rejects a throughput-failure interval below two seconds (HttpMonitoringOptions).
+ private static final int MIN_MONITORING_FAILURE_INTERVAL_SECONDS = 2;
+
private AwsCrtConfigurationUtils() {
}
+ /**
+ * Resolves the throughput monitor that enforces a connection's read/write inactivity timeout, highest precedence first:
+ *
+ * - an explicit {@link ConnectionHealthConfiguration} always wins;
+ * - otherwise the SDK-resolved {@code fallbackTimeout}: {@link Duration#ZERO} applies nothing, a positive value is
+ * mapped.
+ *
+ * The fallback is only ever supplied to an SDK-managed client.
+ */
+ public static HttpMonitoringOptions resolveMonitoringOptions(ConnectionHealthConfiguration healthConfiguration,
+ Duration fallbackTimeout) {
+ if (healthConfiguration != null) {
+ return CrtConfigurationUtils.resolveHttpMonitoringOptions(healthConfiguration).orElse(null);
+ }
+
+ if (fallbackTimeout == null || fallbackTimeout.isZero()) {
+ return null;
+ }
+ return mapReadWriteTimeout(fallbackTimeout);
+ }
+
+ /**
+ * Maps a read/write inactivity timeout onto the CRT throughput monitor that enforces it: a minimum throughput of one byte
+ * per second measured over a failure interval of {@code readWriteTimeout}. Because the threshold is a single byte, any byte
+ * moved while a stream is pending resets the interval, so the connection is shut down only after {@code readWriteTimeout} of
+ * continuous zero-byte progress. The interval has whole-second granularity, and CRT rejects an interval below two seconds, so
+ * a shorter timeout is raised to that floor.
+ */
+ public static HttpMonitoringOptions mapReadWriteTimeout(Duration readWriteTimeout) {
+ HttpMonitoringOptions httpMonitoringOptions = new HttpMonitoringOptions();
+ httpMonitoringOptions.setMinThroughputBytesPerSecond(1);
+ int seconds = Math.max(MIN_MONITORING_FAILURE_INTERVAL_SECONDS,
+ NumericUtils.saturatedCast(readWriteTimeout.getSeconds()));
+ httpMonitoringOptions.setAllowableThroughputFailureIntervalSeconds(seconds);
+ return httpMonitoringOptions;
+ }
+
public static SocketOptions buildSocketOptions(TcpKeepAliveConfiguration tcpKeepAliveConfiguration,
Duration connectionTimeout) {
SocketOptions clientSocketOptions = new SocketOptions();
diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClientLongRunningRequestTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClientLongRunningRequestTest.java
index 17c8f1f37c8a..5a0a4b9d7ec1 100644
--- a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClientLongRunningRequestTest.java
+++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtAsyncHttpClientLongRunningRequestTest.java
@@ -15,9 +15,21 @@
package software.amazon.awssdk.http.crt;
+import static software.amazon.awssdk.http.LongRunningRequestTestSupport.CONFIGURED_TIMEOUT;
+import static software.amazon.awssdk.http.LongRunningRequestTestSupport.assertFailsWithIoExceptionWithinTimeBound;
+import static software.amazon.awssdk.http.LongRunningRequestTestSupport.stubLongPolling;
+import static software.amazon.awssdk.http.LongRunningRequestTestSupport.stubStreamingWithPauses;
+
+import java.net.URI;
+import java.util.concurrent.CompletableFuture;
import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
import software.amazon.awssdk.crt.Log;
+import software.amazon.awssdk.http.HttpTestUtils;
import software.amazon.awssdk.http.SdkAsyncHttpClientLongRunningRequestTestSuite;
+import software.amazon.awssdk.http.SdkHttpConfigurationOption;
+import software.amazon.awssdk.http.SdkHttpFullRequest;
+import software.amazon.awssdk.http.SdkHttpMethod;
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
import software.amazon.awssdk.utils.AttributeMap;
@@ -34,15 +46,47 @@ protected SdkAsyncHttpClient createSdkAsyncHttpClient(AttributeMap config) {
return AwsCrtAsyncHttpClient.builder().buildWithDefaults(config);
}
- // Empty test; the CRT async client does not currently enforce READ_TIMEOUT. Delete this
- // override when connection health monitoring is re-added.
+ // The CRT client enforces a read/write inactivity timeout through SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT rather than
+ // READ_TIMEOUT (which it ignores), so these two suite cases are re-implemented to configure that option. A stalled or
+ // paused response then trips the CRT throughput monitor (AWS_ERROR_HTTP_CHANNEL_THROUGHPUT_FAILURE, a retryable
+ // IOException) and fails the request within the bound instead of hanging.
+ @Test
@Override
public void executeWhenReadTimeoutAndServerDelaysResponseFailsWithinTimeoutBound() {
+ stubLongPolling(mockServer);
+ SdkAsyncHttpClient client = createSdkAsyncHttpClient(fallbackTimeoutConfig());
+ try {
+ assertFailsWithIoExceptionWithinTimeBound(sendRequest(client), CONFIGURED_TIMEOUT);
+ } finally {
+ client.close();
+ }
}
- // Empty test; the CRT async client does not currently enforce READ_TIMEOUT. Delete this
- // override when connection health monitoring is re-added.
+ @Test
@Override
public void executeWhenReadTimeoutAndStreamingResponsePausesFailsWithinTimeoutBound() {
+ stubStreamingWithPauses(mockServer);
+ SdkAsyncHttpClient client = createSdkAsyncHttpClient(fallbackTimeoutConfig());
+ try {
+ assertFailsWithIoExceptionWithinTimeBound(sendRequest(client), CONFIGURED_TIMEOUT);
+ } finally {
+ client.close();
+ }
+ }
+
+ private static AttributeMap fallbackTimeoutConfig() {
+ return AttributeMap.builder()
+ .put(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, CONFIGURED_TIMEOUT)
+ .build();
+ }
+
+ private CompletableFuture sendRequest(SdkAsyncHttpClient client) {
+ URI uri = URI.create("http://localhost:" + mockServer.getPort());
+ SdkHttpFullRequest request = SdkHttpFullRequest.builder()
+ .uri(uri)
+ .method(SdkHttpMethod.GET)
+ .putHeader("Host", uri.getHost())
+ .build();
+ return HttpTestUtils.sendRequest(client, request);
}
}
diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientLongRunningRequestTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientLongRunningRequestTest.java
index e51ca3aa7e16..3b6049403abc 100644
--- a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientLongRunningRequestTest.java
+++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientLongRunningRequestTest.java
@@ -15,10 +15,27 @@
package software.amazon.awssdk.http.crt;
+import static software.amazon.awssdk.http.LongRunningRequestTestSupport.CONFIGURED_TIMEOUT;
+import static software.amazon.awssdk.http.LongRunningRequestTestSupport.assertFailsWithIoExceptionWithinTimeBound;
+import static software.amazon.awssdk.http.LongRunningRequestTestSupport.stubLongPolling;
+import static software.amazon.awssdk.http.LongRunningRequestTestSupport.stubStreamingWithPauses;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CompletableFuture;
import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
import software.amazon.awssdk.crt.Log;
+import software.amazon.awssdk.http.HttpExecuteRequest;
+import software.amazon.awssdk.http.HttpExecuteResponse;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.SdkHttpClientLongRunningRequestTestSuite;
+import software.amazon.awssdk.http.SdkHttpConfigurationOption;
+import software.amazon.awssdk.http.SdkHttpFullRequest;
+import software.amazon.awssdk.http.SdkHttpMethod;
import software.amazon.awssdk.utils.AttributeMap;
public class AwsCrtHttpClientLongRunningRequestTest extends SdkHttpClientLongRunningRequestTestSuite {
@@ -34,15 +51,77 @@ protected SdkHttpClient createSdkHttpClient(AttributeMap config) {
return AwsCrtHttpClient.builder().buildWithDefaults(config);
}
- // Empty test; the CRT sync client does not currently enforce READ_TIMEOUT. Delete this
- // override when connection health monitoring is re-added.
+ // The CRT client enforces a read/write inactivity timeout through SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT rather than
+ // READ_TIMEOUT (which it ignores), so these two suite cases are re-implemented to configure that option. A stalled or
+ // paused response then trips the CRT throughput monitor (AWS_ERROR_HTTP_CHANNEL_THROUGHPUT_FAILURE, a retryable
+ // IOException) and fails the request within the bound instead of hanging.
+ @Test
@Override
public void executeWhenReadTimeoutAndServerDelaysResponseFailsWithinTimeoutBound() {
+ stubLongPolling(mockServer);
+ SdkHttpClient client = createSdkHttpClient(fallbackTimeoutConfig());
+ try {
+ assertFailsWithIoExceptionWithinTimeBound(executeAsync(client), CONFIGURED_TIMEOUT);
+ } finally {
+ client.close();
+ }
}
- // Empty test; the CRT sync client does not currently enforce READ_TIMEOUT. Delete this
- // override when connection health monitoring is re-added.
+ @Test
@Override
public void executeWhenReadTimeoutAndStreamingResponsePausesFailsWithinTimeoutBound() {
+ stubStreamingWithPauses(mockServer);
+ SdkHttpClient client = createSdkHttpClient(fallbackTimeoutConfig());
+ try {
+ assertFailsWithIoExceptionWithinTimeBound(executeAsync(client), CONFIGURED_TIMEOUT);
+ } finally {
+ client.close();
+ }
+ }
+
+ private static AttributeMap fallbackTimeoutConfig() {
+ return AttributeMap.builder()
+ .put(SdkHttpConfigurationOption.SDK_INTERNAL_FALLBACK_READ_WRITE_TIMEOUT, CONFIGURED_TIMEOUT)
+ .build();
+ }
+
+ private CompletableFuture executeAsync(SdkHttpClient client) {
+ return CompletableFuture.supplyAsync(() -> {
+ executeRequest(client);
+ return null;
+ });
+ }
+
+ private void executeRequest(SdkHttpClient client) {
+ URI uri = URI.create("http://localhost:" + mockServer.getPort());
+ SdkHttpFullRequest request = SdkHttpFullRequest.builder()
+ .uri(uri)
+ .method(SdkHttpMethod.POST)
+ .putHeader("Host", uri.getHost())
+ .putHeader("Content-Length", "4")
+ .contentStreamProvider(() -> new ByteArrayInputStream(
+ "Body".getBytes(StandardCharsets.UTF_8)))
+ .build();
+ try {
+ HttpExecuteResponse response = client.prepareRequest(HttpExecuteRequest.builder()
+ .request(request)
+ .contentStreamProvider(
+ request.contentStreamProvider()
+ .orElse(null))
+ .build())
+ .call();
+ response.responseBody().ifPresent(body -> {
+ try {
+ while (body.read() != -1) {
+ // drain body so mid-body timeouts surface
+ }
+ body.close();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ });
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
}
}
diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtilsTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtilsTest.java
index c2dcfa282a07..1ca5ad6d1173 100644
--- a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtilsTest.java
+++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/AwsCrtConfigurationUtilsTest.java
@@ -30,11 +30,13 @@
import software.amazon.awssdk.crt.io.TlsCipherPreference;
import software.amazon.awssdk.crt.io.TlsContextOptions;
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
+import software.amazon.awssdk.http.crt.ConnectionHealthConfiguration;
import software.amazon.awssdk.http.crt.TcpKeepAliveConfiguration;
import software.amazon.awssdk.http.crt.TlsVersion;
import software.amazon.awssdk.utils.AttributeMap;
class AwsCrtConfigurationUtilsTest {
+
@ParameterizedTest
@MethodSource("cipherPreferences")
void resolveCipherPreference_shouldResolveCorrectly(Boolean postQuantumTlsEnabled,
@@ -120,6 +122,65 @@ void resolveMinTlsVersion_tls13_returnsTLSv1_3() {
.isEqualTo(TlsContextOptions.TlsVersions.TLSv1_3);
}
+ @ParameterizedTest(name = "{0} inactivity timeout -> {1}s failure interval")
+ @MethodSource("readWriteTimeoutMappings")
+ void mapReadWriteTimeout_mapsToOneBytePerSecondMonitor(Duration readWriteTimeout, int expectedIntervalSeconds) {
+ HttpMonitoringOptions options = AwsCrtConfigurationUtils.mapReadWriteTimeout(readWriteTimeout);
+ assertThat(options.getMinThroughputBytesPerSecond()).isEqualTo(1L);
+ assertThat(options.getAllowableThroughputFailureIntervalSeconds()).isEqualTo(expectedIntervalSeconds);
+ }
+
+ private static Stream readWriteTimeoutMappings() {
+ return Stream.of(
+ Arguments.of(Duration.ofMinutes(5), 300),
+ Arguments.of(Duration.ofMinutes(15), 900),
+ Arguments.of(Duration.ofSeconds(2), 2),
+ Arguments.of(Duration.ofSeconds(1), 2),
+ Arguments.of(Duration.ZERO, 2),
+ Arguments.of(Duration.ofMillis(2999), 2),
+ Arguments.of(Duration.ofMillis(3999), 3)
+ );
+ }
+
+ @Test
+ void resolveMonitoringOptions_explicitConnectionHealthConfiguration_winsOverFallback() {
+ HttpMonitoringOptions options = AwsCrtConfigurationUtils.resolveMonitoringOptions(healthConfiguration(),
+ Duration.ofMinutes(15));
+ assertThat(options.getMinThroughputBytesPerSecond()).isEqualTo(500L);
+ assertThat(options.getAllowableThroughputFailureIntervalSeconds()).isEqualTo(10);
+ }
+
+ @Test
+ void resolveMonitoringOptions_explicitConnectionHealthConfigurationWithoutFallback_usesExplicitConfig() {
+ HttpMonitoringOptions options = AwsCrtConfigurationUtils.resolveMonitoringOptions(healthConfiguration(), null);
+ assertThat(options.getMinThroughputBytesPerSecond()).isEqualTo(500L);
+ assertThat(options.getAllowableThroughputFailureIntervalSeconds()).isEqualTo(10);
+ }
+
+ @Test
+ void resolveMonitoringOptions_fallbackZero_appliesNothing() {
+ assertThat(AwsCrtConfigurationUtils.resolveMonitoringOptions(null, Duration.ZERO)).isNull();
+ }
+
+ @Test
+ void resolveMonitoringOptions_fallbackPositive_mapped() {
+ HttpMonitoringOptions options = AwsCrtConfigurationUtils.resolveMonitoringOptions(null, Duration.ofMinutes(15));
+ assertThat(options.getMinThroughputBytesPerSecond()).isEqualTo(1L);
+ assertThat(options.getAllowableThroughputFailureIntervalSeconds()).isEqualTo(900);
+ }
+
+ @Test
+ void resolveMonitoringOptions_fallbackAbsent_appliesNothing() {
+ assertThat(AwsCrtConfigurationUtils.resolveMonitoringOptions(null, null)).isNull();
+ }
+
+ private static ConnectionHealthConfiguration healthConfiguration() {
+ return ConnectionHealthConfiguration.builder()
+ .minimumThroughputInBps(500L)
+ .minimumThroughputTimeout(Duration.ofSeconds(10))
+ .build();
+ }
+
private static Stream defaultConnectionHealthConfigurationCases() {
return Stream.of(