From a209aaea7190c6cd34c8a94560d3c15b814ea288 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Fri, 13 Mar 2026 14:51:01 -0500 Subject: [PATCH 1/2] Add HttpTransportConfig for transport settings Add HttpTransportConfig to support configuring connect/request timeouts and HTTP version through the transport factory's Document-based configuration. Update JavaHttpClientTransport.Factory to use it instead of manually parsing the document. --- .../java/client/http/HttpTransportConfig.java | 99 +++++++++++++++++++ .../client/http/JavaHttpClientTransport.java | 33 +++++-- 2 files changed, 122 insertions(+), 10 deletions(-) create mode 100644 client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpTransportConfig.java diff --git a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpTransportConfig.java b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpTransportConfig.java new file mode 100644 index 000000000..2fe1eb364 --- /dev/null +++ b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpTransportConfig.java @@ -0,0 +1,99 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.client.http; + +import java.time.Duration; +import software.amazon.smithy.java.core.serde.document.Document; +import software.amazon.smithy.java.http.api.HttpVersion; + +/** + * Common HTTP transport configuration shared across transport implementations. + * + *

Subclass this to add transport-specific settings. + */ +public class HttpTransportConfig { + + private Duration requestTimeout; + private Duration connectTimeout; + private HttpVersion httpVersion; + + /** + * Per-request timeout. Null means no timeout (use client default). + * + *

Defined in the document data as milliseconds. + * + * @return request timeout. + */ + public Duration requestTimeout() { + return requestTimeout; + } + + public HttpTransportConfig requestTimeout(Duration requestTimeout) { + this.requestTimeout = requestTimeout; + return this; + } + + /** + * TCP connect timeout. Null means use transport default. + */ + public Duration connectTimeout() { + return connectTimeout; + } + + public HttpTransportConfig connectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + /** + * HTTP version preference. Null means use transport default. + * + *

Defined in the document data as "http/1.0", "http/1.1", or "h2". + */ + public HttpVersion httpVersion() { + return httpVersion; + } + + public HttpTransportConfig httpVersion(HttpVersion httpVersion) { + this.httpVersion = httpVersion; + return this; + } + + /** + * Populate fields from a Document config. + * + *

{@code
+     * {
+     *   "requestTimeoutMs": 5000,
+     *   "connectTimeoutMs": 3000,
+     *   "httpVersion": "HTTP/2.0"
+     * }
+     * }
+ * + * @param doc configuration document + * @return this config for chaining + */ + public HttpTransportConfig fromDocument(Document doc) { + var config = doc.asStringMap(); + + var timeout = config.get("requestTimeoutMs"); + if (timeout != null) { + this.requestTimeout = Duration.ofMillis(timeout.asLong()); + } + + var connTimeout = config.get("connectTimeoutMs"); + if (connTimeout != null) { + this.connectTimeout = Duration.ofMillis(connTimeout.asLong()); + } + + var version = config.get("httpVersion"); + if (version != null) { + this.httpVersion = HttpVersion.from(version.asString()); + } + + return this; + } +} diff --git a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/JavaHttpClientTransport.java b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/JavaHttpClientTransport.java index 4713a7087..0edfc45c8 100644 --- a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/JavaHttpClientTransport.java +++ b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/JavaHttpClientTransport.java @@ -39,6 +39,7 @@ public class JavaHttpClientTransport implements ClientTransport Date: Fri, 13 Mar 2026 15:50:58 -0500 Subject: [PATCH 2/2] Make a shared httpConfig setting This new shared httpConfig setting makes it possible to tweak the common HTTP settings across any dynamically resolved transport. Transport-factory-specific settings override these, if they choose to (e.g., if they're http-specific). --- .../client/core/ClientTransportFactory.java | 11 +++++---- .../client/http/JavaHttpClientTransport.java | 7 ++++-- .../java/codegen/JavaCodegenSettings.java | 17 +++++++++++++- .../generators/ClientInterfaceGenerator.java | 23 +++++++++++++++++-- .../java/core/serde/document/Document.java | 6 +++++ 5 files changed, 54 insertions(+), 10 deletions(-) diff --git a/client/client-api/src/main/java/software/amazon/smithy/java/client/core/ClientTransportFactory.java b/client/client-api/src/main/java/software/amazon/smithy/java/client/core/ClientTransportFactory.java index c019828ca..bd0cb0ac9 100644 --- a/client/client-api/src/main/java/software/amazon/smithy/java/client/core/ClientTransportFactory.java +++ b/client/client-api/src/main/java/software/amazon/smithy/java/client/core/ClientTransportFactory.java @@ -6,7 +6,6 @@ package software.amazon.smithy.java.client.core; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.ServiceLoader; @@ -52,15 +51,17 @@ default byte priority() { *

Transports must be able to be instantiated without any arguments for use in dynamic clients. */ default ClientTransport createTransport() { - return createTransport(Document.of(Collections.emptyMap())); + return createTransport(Document.EMPTY_MAP, Document.EMPTY_MAP); } /** - * Create a {@link ClientTransport} with a user-provided configuration. + * Create a {@link ClientTransport} with transport-specific settings and the full plugin settings. * - *

Configurations are typically specified in the configuration of the client-codegen plugin. + *

The {@code pluginSettings} document contains the full codegen plugin configuration, allowing + * transports to read shared settings (e.g., {@code httpConfig}) that are not transport-specific. + * Transport-specific settings should take precedence over shared settings. */ - ClientTransport createTransport(Document settings); + ClientTransport createTransport(Document settings, Document pluginSettings); /** * Loads all {@link ClientTransportFactory} implementations and sorts them by priority. diff --git a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/JavaHttpClientTransport.java b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/JavaHttpClientTransport.java index 0edfc45c8..52be2b7af 100644 --- a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/JavaHttpClientTransport.java +++ b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/JavaHttpClientTransport.java @@ -227,9 +227,12 @@ public String name() { } @Override - public JavaHttpClientTransport createTransport(Document node) { + public JavaHttpClientTransport createTransport(Document node, Document pluginSettings) { setHostProperties(); - var config = new HttpTransportConfig().fromDocument(node); + // Start with httpConfig from plugin settings as baseline, then apply transport-specific settings on top. + var config = new HttpTransportConfig().fromDocument(pluginSettings.asStringMap() + .getOrDefault("httpConfig", Document.EMPTY_MAP)); + config.fromDocument(node); var builder = HttpClient.newBuilder(); if (config.httpVersion() != null) { builder.version(smithyToHttpVersion(config.httpVersion())); diff --git a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/JavaCodegenSettings.java b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/JavaCodegenSettings.java index eaa580ac3..7733aff08 100644 --- a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/JavaCodegenSettings.java +++ b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/JavaCodegenSettings.java @@ -43,6 +43,7 @@ public final class JavaCodegenSettings { private static final String TRANSPORT = "transport"; private static final String DEFAULT_PLUGINS = "defaultPlugins"; private static final String DEFAULT_SETTINGS = "defaultSettings"; + private static final String HTTP_CONFIG = "httpConfig"; private static final String RELATIVE_DATE = "relativeDate"; private static final String RELATIVE_VERSION = "relativeVersion"; private static final String EDITION = "edition"; @@ -60,6 +61,7 @@ public final class JavaCodegenSettings { TRANSPORT, DEFAULT_PLUGINS, DEFAULT_SETTINGS, + HTTP_CONFIG, RELATIVE_DATE, RELATIVE_VERSION, EDITION, @@ -84,6 +86,7 @@ public final class JavaCodegenSettings { private final boolean useExternalTypes; private final List runtimeTraits; private final Selector runtimeTraitsSelector; + private final ObjectNode httpConfig; private final Map> generatedSymbols = new HashMap<>(); private JavaCodegenSettings(Builder builder) { @@ -103,6 +106,7 @@ private JavaCodegenSettings(Builder builder) { this.useExternalTypes = builder.useExternalTypes; this.runtimeTraits = Collections.unmodifiableList(builder.runtimeTraits); this.runtimeTraitsSelector = builder.runtimeTraitsSelector; + this.httpConfig = builder.httpConfig; } /** @@ -128,7 +132,8 @@ public static JavaCodegenSettings fromNode(ObjectNode settingsNode) { .getStringMember(EDITION, builder::edition) .getBooleanMember(USE_EXTERNAL_TYPES, builder::useExternalTypes) .getArrayMember(RUNTIME_TRAITS, n -> n.expectStringNode().expectShapeId(), builder::runtimeTraits) - .getStringMember(RUNTIME_TRAITS_SELECTOR, builder::runtimeTraitsSelector); + .getStringMember(RUNTIME_TRAITS_SELECTOR, builder::runtimeTraitsSelector) + .getObjectMember(HTTP_CONFIG, builder::httpConfig); builder.sourceLocation(settingsNode.getSourceLocation().getFilename()); @@ -203,6 +208,10 @@ public Selector runtimeTraitsSelector() { return runtimeTraitsSelector; } + public ObjectNode httpConfig() { + return httpConfig; + } + @SmithyInternalApi public void addSymbol(Symbol symbol) { var symbols = generatedSymbols.computeIfAbsent(symbol.getNamespace(), k -> new HashSet<>()); @@ -262,6 +271,7 @@ public static final class Builder { private final List runtimeTraits = new ArrayList<>(); private Selector runtimeTraitsSelector; private boolean useExternalTypes; + private ObjectNode httpConfig; public Builder service(String string) { this.service = ShapeId.from(string); @@ -366,6 +376,11 @@ public Builder runtimeTraitsSelector(String selector) { return this; } + public Builder httpConfig(ObjectNode httpConfig) { + this.httpConfig = httpConfig; + return this; + } + public JavaCodegenSettings build() { return new JavaCodegenSettings(this); } diff --git a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/ClientInterfaceGenerator.java b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/ClientInterfaceGenerator.java index 418f8ec91..a27f43bd2 100644 --- a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/ClientInterfaceGenerator.java +++ b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/ClientInterfaceGenerator.java @@ -133,7 +133,8 @@ final class Builder extends ${client:T}.Builder<${interface:T}, Builder>${?setti ${?hasDefaults}${defaultPlugins:C|} ${/hasDefaults}${?hasDefaultProtocol}${defaultProtocol:C|} ${/hasDefaultProtocol}${?hasTransportSettings}${transportSettings:C|} - ${/hasTransportSettings}${?defaultSchemes} + ${/hasTransportSettings}${?hasHttpConfig}${httpConfig:C|} + ${/hasHttpConfig}${?defaultSchemes} ${defaultAuth:C|}${/defaultSchemes} private Builder() { @@ -151,7 +152,7 @@ private Builder() { configBuilder().protocol(${protocolFactory:C}.createProtocol(protocolSettings, protocolTrait)); } ${/hasDefaultProtocol}${?hasDefaultTransport}if (configBuilder().transport() == null) { - configBuilder().transport(${transportFactory:C}.createTransport(${?hasTransportSettings}transportSettings${/hasTransportSettings})); + configBuilder().transport(${transportFactory:C}.createTransport(${?hasTransportSettings}transportSettings${/hasTransportSettings}${^hasTransportSettings}${document:T}.EMPTY_MAP${/hasTransportSettings}, ${?hasHttpConfig}httpConfig${/hasHttpConfig}${^hasHttpConfig}${document:T}.EMPTY_MAP${/hasHttpConfig})); }${/hasDefaultTransport} ${?hasBdd}${loadBddInfo:C|}${/hasBdd} return new ${impl:T}(this); @@ -199,6 +200,10 @@ final class RequestOverrideBuilder extends ${requestOverride:T}.OverrideBuilder< writer.putContext( "transportSettings", new TransportSettingsGenerator(writer, settings.transportSettings())); + var hasHttpConfig = settings.httpConfig() != null && !settings.httpConfig().isEmpty(); + writer.putContext("hasHttpConfig", hasHttpConfig); + writer.putContext("httpConfig", new HttpConfigGenerator(writer, settings.httpConfig())); + writer.putContext("document", Document.class); writer.putContext( "operations", new OperationMethodGenerator( @@ -249,6 +254,20 @@ public void run() { } } + private record HttpConfigGenerator(JavaWriter writer, ObjectNode settings) implements Runnable { + @Override + public void run() { + if (settings == null) { + return; + } + writer.pushState(); + writer.putContext("document", Document.class); + writer.putContext("nodeWriter", new NodeDocumentWriter(writer, settings)); + writer.write("private static final ${document:T} httpConfig = ${nodeWriter:C};"); + writer.popState(); + } + } + private record NodeDocumentWriter(JavaWriter writer, ObjectNode node) implements NodeVisitor, Runnable { @Override diff --git a/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java b/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java index 04b7eccd8..b0e04836e 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java +++ b/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java @@ -10,6 +10,7 @@ import java.nio.ByteBuffer; import java.time.Instant; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -68,6 +69,11 @@ * itself was serialized or deserialized directly. */ public interface Document extends SerializableShape { + /** + * An empty map document. + */ + Document EMPTY_MAP = of(Collections.emptyMap()); + /** * Get the Smithy data model type for the underlying contents of the document. *