This implementation leverages Apache HttpClient 5.x, offering improved performance characteristics and better compliance + * with HTTP standards compared to the Apache 4.x-based.
+ * + *See software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient for a lighter alternative implementation + * with fewer dependencies but more limited functionality.
+ * + */ +@SdkPublicApi +public class Apache5HttpClient implements SdkHttpClient { + + public static final String CLIENT_NAME = "Apache5"; + + @Override + public ExecutableHttpRequest prepareRequest(HttpExecuteRequest request) { + throw new UnsupportedOperationException("API implementation is in progress"); + } + + @Override + public void close() { + throw new UnsupportedOperationException("API implementation is in progress"); + } + + @Override + public String clientName() { + return CLIENT_NAME; + } + +} diff --git a/http-clients/pom.xml b/http-clients/pom.xml index 3c66caf5f4b0..7ef4045ba8e5 100644 --- a/http-clients/pom.xml +++ b/http-clients/pom.xml @@ -34,6 +34,7 @@This implementation leverages Apache HttpClient 5.x, offering improved performance characteristics and better compliance - * with HTTP standards compared to the Apache 4.x-based.
+ * An implementation of {@link SdkHttpClient} that uses Apache5 HTTP client to communicate with the service. This is the most + * powerful synchronous client that adds an extra dependency and additional startup latency in exchange for more functionality, + * like support for HTTP proxies. * - *See software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient for a lighter alternative implementation - * with fewer dependencies but more limited functionality.
+ *See software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient for an alternative implementation.
* + *This can be created via {@link #builder()}
*/ @SdkPublicApi -public class Apache5HttpClient implements SdkHttpClient { +public final class Apache5HttpClient implements SdkHttpClient { public static final String CLIENT_NAME = "Apache5"; + private static final Logger log = Logger.loggerFor(Apache5HttpClient.class); + + private final Apache5HttpRequestFactory apacheHttpRequestFactory = new Apache5HttpRequestFactory(); + private final ConnectionManagerAwareHttpClient httpClient; + private final Apache5HttpRequestConfig requestConfig; + private final AttributeMap resolvedOptions; + + @SdkTestInternalApi + Apache5HttpClient(ConnectionManagerAwareHttpClient httpClient, + Apache5HttpRequestConfig requestConfig, + AttributeMap resolvedOptions) { + this.httpClient = httpClient; + this.requestConfig = requestConfig; + this.resolvedOptions = resolvedOptions; + } + + private Apache5HttpClient(DefaultBuilder builder, AttributeMap resolvedOptions) { + this.httpClient = createClient(builder, resolvedOptions); + this.requestConfig = createRequestConfig(builder, resolvedOptions); + this.resolvedOptions = resolvedOptions; + } + + public static Builder builder() { + return new DefaultBuilder(); + } + + /** + * Create a {@link Apache5HttpClient} with the default properties + * + * @return an {@link Apache5HttpClient} + */ + public static SdkHttpClient create() { + return new DefaultBuilder().build(); + } + + private ConnectionManagerAwareHttpClient createClient(Apache5HttpClient.DefaultBuilder configuration, + AttributeMap standardOptions) { + ApacheConnectionManagerFactory cmFactory = new ApacheConnectionManagerFactory(); + + HttpClientBuilder builder = HttpClients.custom(); + // Note that it is important we register the original connection manager with the + // IdleConnectionReaper as it's required for the successful deregistration of managers + // from the reaper. See https://github.com/aws/aws-sdk-java/issues/722. + HttpClientConnectionManager cm = cmFactory.create(configuration, standardOptions); + + Registry+ * SdkHttpClient httpClient = + * Apache5HttpClient.builder() + * .socketTimeout(Duration.ofSeconds(10)) + * .build(); + *+ */ + public interface Builder extends SdkHttpClient.Builder
+ * When enabled, connections left idling for longer than {@link #connectionMaxIdleTime(Duration)} will be + * closed. This will not close connections currently in use. By default, this is enabled. + */ + Builder useIdleConnectionReaper(Boolean useConnectionReaper); + + /** + * Configuration that defines a DNS resolver. If no matches are found, the default resolver is used. + */ + Builder dnsResolver(DnsResolver dnsResolver); + + /** + * Configuration that defines a custom Socket factory. If set to a null value, a default factory is used. + *
+ * When set to a non-null value, the use of a custom factory implies the configuration options TRUST_ALL_CERTIFICATES, + * TLS_TRUST_MANAGERS_PROVIDER, and TLS_KEY_MANAGERS_PROVIDER are ignored. + */ + Builder socketFactory(ConnectionSocketFactory socketFactory); + + /** + * Configuration that defines an HTTP route planner that computes the route an HTTP request should take. + * May not be used in conjunction with {@link #proxyConfiguration(ProxyConfiguration)}. + */ + Builder httpRoutePlanner(HttpRoutePlanner proxyConfiguration); + + /** + * Configuration that defines a custom credential provider for HTTP requests. + * May not be used in conjunction with {@link ProxyConfiguration#username()} and {@link ProxyConfiguration#password()}. + */ + Builder credentialsProvider(CredentialsProvider credentialsProvider); + + /** + * Configure whether to enable or disable TCP KeepAlive. + * The configuration will be passed to the socket option {@link java.net.SocketOptions#SO_KEEPALIVE}. + *
+ * By default, this is disabled. + *
+ * When enabled, the actual KeepAlive mechanism is dependent on the Operating System and therefore additional TCP + * KeepAlive values (like timeout, number of packets, etc) must be configured via the Operating System (sysctl on + * Linux/Mac, and Registry values on Windows). + */ + Builder tcpKeepAlive(Boolean keepConnectionAlive); + + /** + * Configure the {@link TlsKeyManagersProvider} that will provide the {@link javax.net.ssl.KeyManager}s to use + * when constructing the SSL context. + *
+ * The default used by the client will be {@link SystemPropertyTlsKeyManagersProvider}. Configure an instance of
+ * {@link software.amazon.awssdk.internal.http.NoneTlsKeyManagersProvider} or another implementation of
+ * {@link TlsKeyManagersProvider} to override it.
+ */
+ Builder tlsKeyManagersProvider(TlsKeyManagersProvider tlsKeyManagersProvider);
+
+ /**
+ * Configure the {@link TlsTrustManagersProvider} that will provide the {@link javax.net.ssl.TrustManager}s to use
+ * when constructing the SSL context.
+ */
+ Builder tlsTrustManagersProvider(TlsTrustManagersProvider tlsTrustManagersProvider);
+
+ /**
+ * Configure the authentication scheme registry that can be used to obtain the corresponding authentication scheme
+ * implementation for a given type of authorization challenge.
+ */
+ Builder authSchemeProviderRegistry(Registry All implementations of this interface are mutable and not thread safe.
+ * This value is set to "true" by default which means SDK will automatically use system property values for options that
+ * are not provided during building the {@link ProxyConfiguration} object. To disable this behavior, set this value to
+ * "false".It is important to note that when this property is set to "true," all proxy settings will exclusively originate
+ * from system properties, and no partial settings will be obtained from EnvironmentVariableValues.
+ */
+ Builder useSystemPropertyValues(Boolean useSystemPropertyValues);
+
+ /**
+ * Option whether to use environment variable values for proxy configuration if any of the config options are missing.
+ *
+ * This value is set to "true" by default, which means the SDK will automatically use environment variable values for
+ * proxy configuration options that are not provided during the building of the {@link ProxyConfiguration} object. To
+ * disable this behavior, set this value to "false". It is important to note that when this property is set to "true," all
+ * proxy settings will exclusively originate from environment variableValues, and no partial settings will be obtained
+ * from SystemPropertyValues.
+ * Comma-separated host names in the NO_PROXY environment variable indicate multiple hosts to exclude from
+ * proxy settings.
+ *
+ * @param useEnvironmentVariableValues The option whether to use environment variable values.
+ * @return This object for method chaining.
+ */
+ Builder useEnvironmentVariableValues(Boolean useEnvironmentVariableValues);
+
+ /**
+ * The HTTP scheme to use for connecting to the proxy. Valid values are {@code http} and {@code https}.
+ *
+ * The client defaults to {@code http} if none is given.
+ *
+ * @param scheme The proxy scheme.
+ * @return This object for method chaining.
+ */
+ Builder scheme(String scheme);
+
+ }
+
+ /**
+ * An SDK-internal implementation of {@link Builder}.
+ */
+ private static final class DefaultClientProxyConfigurationBuilder implements Builder {
+
+ private URI endpoint;
+ private String username;
+ private String password;
+ private String ntlmDomain;
+ private String ntlmWorkstation;
+ private Set
+ * If an error is encountered the first time we try to write the request
+ * entity, we remember the original exception, and report that as the root
+ * cause if we continue to encounter errors, rather than masking the
+ * original error.
+ */
+ @Override
+ public void writeTo(OutputStream output) throws IOException {
+ try {
+ if (!firstAttempt && isRepeatable()) {
+ content.reset();
+ }
+
+ firstAttempt = false;
+ inputStreamRequestEntity.writeTo(output);
+ } catch (IOException ioe) {
+ if (originalException == null) {
+ originalException = ioe;
+ }
+ throw originalException;
+ }
+ }
+
+}
diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/SdkProxyRoutePlanner.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/SdkProxyRoutePlanner.java
new file mode 100644
index 000000000000..64cae495fba2
--- /dev/null
+++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/SdkProxyRoutePlanner.java
@@ -0,0 +1,64 @@
+/*
+ * 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.http.apache5.internal;
+
+import static software.amazon.awssdk.utils.StringUtils.lowerCase;
+
+import java.util.Set;
+import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
+import org.apache.hc.client5.http.impl.routing.DefaultRoutePlanner;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+
+/**
+ * SdkProxyRoutePlanner delegates a Proxy Route Planner from the settings instead of the
+ * system properties. It will use the proxy created from proxyHost and proxyPort and
+ * filter the hosts who matches nonProxyHosts pattern.
+ */
+@SdkInternalApi
+public class SdkProxyRoutePlanner extends DefaultRoutePlanner {
+
+ private HttpHost proxy;
+ private Set
+ * setNormalizeUri is added only in 4.5.8, so customers using the latest version of SDK with old versions (4.5.6 or less)
+ * of Apache httpclient will see NoSuchMethodError. Hence this method will suppress the error.
+ *
+ * Do not use Apache version 4.5.7 as it breaks URI paths with special characters and there is no option
+ * to disable normalization.
+ *
- * setNormalizeUri is added only in 4.5.8, so customers using the latest version of SDK with old versions (4.5.6 or less)
- * of Apache httpclient will see NoSuchMethodError. Hence this method will suppress the error.
- *
- * Do not use Apache version 4.5.7 as it breaks URI paths with special characters and there is no option
- * to disable normalization.
- *
+ * setNormalizeUri is added only in 4.5.8, so customers using the latest version of SDK with old versions (4.5.6 or less)
+ * of Apache httpclient will see NoSuchMethodError. Hence this method will suppress the error.
+ *
+ * Do not use Apache version 4.5.7 as it breaks URI paths with special characters and there is no option
+ * to disable normalization.
+ *
- * setNormalizeUri is added only in 4.5.8, so customers using the latest version of SDK with old versions (4.5.6 or less)
- * of Apache httpclient will see NoSuchMethodError. Hence this method will suppress the error.
- *
- * Do not use Apache version 4.5.7 as it breaks URI paths with special characters and there is no option
- * to disable normalization.
- *
+ * We store the content stream reference to avoid calling {@code getContent()} on the wrapped
+ * entity multiple times, which could potentially create new stream instances or perform
+ * unnecessary operations. This ensures we consistently use the same stream instance for
+ * {@code markSupported()} checks and {@code reset()} operations throughout the entity's lifecycle.
*/
+
+ private static class EntityCreationResult {
+ final InputStreamEntity entity;
+ final InputStream content;
+
+ EntityCreationResult(InputStreamEntity entity, InputStream content) {
+ this.entity = entity;
+ this.content = content;
+ }
+ }
+
public RepeatableInputStreamRequestEntity(HttpExecuteRequest request) {
- super(createInputStreamEntity(request));
+ this(createInputStreamEntityWithMetadata(request), request);
+ }
+
+ private RepeatableInputStreamRequestEntity(EntityCreationResult result, HttpExecuteRequest request) {
+ super(result.entity);
+ this.content = result.content;
+ this.isChunked = request.httpRequest().matchingHeaders(TRANSFER_ENCODING).contains(CHUNKED);
+ }
- isChunked = request.httpRequest().matchingHeaders(TRANSFER_ENCODING).contains(CHUNKED);
+ private static EntityCreationResult createInputStreamEntityWithMetadata(HttpExecuteRequest request) {
+ InputStream content = getContent(request.contentStreamProvider());
/*
* If we don't specify a content length when we instantiate our
@@ -93,35 +104,14 @@ public RepeatableInputStreamRequestEntity(HttpExecuteRequest request) {
.map(RepeatableInputStreamRequestEntity::parseContentLength)
.orElse(-1L);
- content = getContent(request.contentStreamProvider());
-
- // Create InputStreamEntity with proper ContentType handling for HttpClient 5.x
- ContentType contentType = request.httpRequest().firstMatchingHeader("Content-Type")
- .map(RepeatableInputStreamRequestEntity::parseContentType)
- .orElse(null);
-
- if (contentLength >= 0) {
- inputStreamRequestEntity = new InputStreamEntity(content, contentLength, contentType);
- } else {
- inputStreamRequestEntity = new InputStreamEntity(content, contentType);
- }
- }
-
- private static InputStreamEntity createInputStreamEntity(HttpExecuteRequest request) {
- InputStream content = getContent(request.contentStreamProvider());
-
- long contentLength = request.httpRequest().firstMatchingHeader("Content-Length")
- .map(RepeatableInputStreamRequestEntity::parseContentLength)
- .orElse(-1L);
-
ContentType contentType = request.httpRequest().firstMatchingHeader("Content-Type")
.map(RepeatableInputStreamRequestEntity::parseContentType)
.orElse(null);
- if (contentLength >= 0) {
- return new InputStreamEntity(content, contentLength, contentType);
- }
- return new InputStreamEntity(content, contentType);
+ InputStreamEntity entity = contentLength >= 0
+ ? new InputStreamEntity(content, contentLength, contentType)
+ : new InputStreamEntity(content, contentType);
+ return new EntityCreationResult(entity, content);
}
private static long parseContentLength(String contentLength) {
@@ -164,13 +154,9 @@ public boolean isChunked() {
*/
@Override
public boolean isRepeatable() {
- boolean markSupported = content.markSupported();
- boolean entityRepeatable = inputStreamRequestEntity.isRepeatable();
- boolean result = markSupported || entityRepeatable;
- return result;
+ return content.markSupported() || super.isRepeatable();
}
-
/**
* Resets the underlying InputStream if this isn't the first attempt to
* write out the request, otherwise simply delegates to
@@ -189,7 +175,7 @@ public void writeTo(OutputStream output) throws IOException {
}
firstAttempt = false;
- inputStreamRequestEntity.writeTo(output);
+ super.writeTo(output);
} catch (IOException ioe) {
if (originalException == null) {
originalException = ioe;
@@ -200,12 +186,8 @@ public void writeTo(OutputStream output) throws IOException {
@Override
public void close() throws IOException {
- try {
- if (content != null) {
- content.close();
- }
- } finally {
- super.close();
- }
+ // The InputStreamEntity handles closing the stream when it's closed
+ // We don't need to close our reference separately to avoid double-closing
+ super.close();
}
}
diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/RepeatableInputStreamRequestEntityTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/RepeatableInputStreamRequestEntityTest.java
index 0cf37febec8c..d69eb64bff13 100644
--- a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/RepeatableInputStreamRequestEntityTest.java
+++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/RepeatableInputStreamRequestEntityTest.java
@@ -16,6 +16,7 @@
package software.amazon.awssdk.http.apache5.internal;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
@@ -35,12 +36,7 @@
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.URI;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
import java.util.Random;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
@@ -670,51 +666,6 @@ void constructor_WithoutContentType_HandlesGracefully() {
assertEquals(100L, entity.getContentLength());
}
- @Test
- @DisplayName("Entity should handle concurrent write attempts")
- void writeTo_ConcurrentWrites_HandlesCorrectly() throws Exception {
- // Given
- String content = "Concurrent test content";
- ContentStreamProvider provider = () -> new ByteArrayInputStream(content.getBytes());
- SdkHttpRequest httpRequest = httpRequestBuilder.build();
- HttpExecuteRequest request = HttpExecuteRequest.builder()
- .request(httpRequest)
- .contentStreamProvider(provider)
- .build();
-
- entity = new RepeatableInputStreamRequestEntity(request);
-
- // Simulate concurrent writes
- int threadCount = 5;
- CountDownLatch latch = new CountDownLatch(threadCount);
- List This can be created via {@link #builder()}