successResponseHandler,
+ HttpResponseHandler extends SdkException> failureResponseHandler) {
+
+ return new CombinedResponseHandler<>(
+ successResponseHandler == null ? noOpSyncResponseHandler() : successResponseHandler,
+ failureResponseHandler == null ? noOpSyncResponseHandler() : failureResponseHandler);
+ }
+}
diff --git a/test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkAsyncHttpClientApiCallAttemptTimeoutTestSuite.java b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkAsyncHttpClientApiCallAttemptTimeoutTestSuite.java
new file mode 100644
index 000000000000..c3e3c5287272
--- /dev/null
+++ b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkAsyncHttpClientApiCallAttemptTimeoutTestSuite.java
@@ -0,0 +1,81 @@
+/*
+ * 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;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static software.amazon.awssdk.core.internal.util.AsyncResponseHandlerTestUtils.noOpResponseHandler;
+import static utils.HttpTestUtils.executionContext;
+import static utils.HttpTestUtils.testAsyncClientBuilder;
+
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import java.io.ByteArrayInputStream;
+import java.time.Duration;
+import java.util.concurrent.CompletableFuture;
+import org.junit.Rule;
+import org.junit.Test;
+import software.amazon.awssdk.core.exception.ApiCallAttemptTimeoutException;
+import software.amazon.awssdk.core.http.NoopTestRequest;
+import software.amazon.awssdk.core.internal.http.AmazonAsyncHttpClient;
+import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
+import utils.ValidSdkObjects;
+
+/**
+ * Reusable suite that verifies sdk-core's async API-call-attempt timeout aborts a slow real response, exercised over a real
+ * {@link SdkAsyncHttpClient}. Each concrete async HTTP client module subclasses this and provides its client via
+ * {@link #createSdkAsyncHttpClient()}.
+ */
+public abstract class SdkAsyncHttpClientApiCallAttemptTimeoutTestSuite {
+
+ private static final Duration API_CALL_TIMEOUT = Duration.ofSeconds(1);
+
+ @Rule
+ public WireMockRule wireMock = new WireMockRule(0);
+
+ private AmazonAsyncHttpClient httpClient;
+
+ protected abstract SdkAsyncHttpClient createSdkAsyncHttpClient();
+
+ @Test
+ public void slowApiAttempt_ThrowsApiCallAttemptTimeoutException() {
+ httpClient = testAsyncClientBuilder()
+ .asyncHttpClient(createSdkAsyncHttpClient())
+ .apiCallTimeout(API_CALL_TIMEOUT)
+ .apiCallAttemptTimeout(Duration.ofMillis(1))
+ .build();
+
+ stubFor(get(anyUrl())
+ .willReturn(aResponse().withStatus(200).withBody("{}").withFixedDelay(1_000)));
+ CompletableFuture future = requestBuilder().execute(noOpResponseHandler());
+ assertThatThrownBy(future::join).hasCauseInstanceOf(ApiCallAttemptTimeoutException.class);
+ }
+
+ private AmazonAsyncHttpClient.RequestExecutionBuilder requestBuilder() {
+ return httpClient.requestExecutionBuilder()
+ .request(generateRequest())
+ .originalRequest(NoopTestRequest.builder().build())
+ .executionContext(executionContext(null));
+ }
+
+ private SdkHttpFullRequest generateRequest() {
+ return ValidSdkObjects.sdkHttpFullRequest(wireMock.port())
+ .host("localhost")
+ .contentStreamProvider(() -> new ByteArrayInputStream("test".getBytes())).build();
+ }
+}
diff --git a/core/sdk-core/src/it/java/software/amazon/awssdk/core/http/ConnectionPoolMaxConnectionsIntegrationTest.java b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkHttpClientConnectionPoolTestSuite.java
similarity index 59%
rename from core/sdk-core/src/it/java/software/amazon/awssdk/core/http/ConnectionPoolMaxConnectionsIntegrationTest.java
rename to test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkHttpClientConnectionPoolTestSuite.java
index 942b6c957820..ad424615ef96 100644
--- a/core/sdk-core/src/it/java/software/amazon/awssdk/core/http/ConnectionPoolMaxConnectionsIntegrationTest.java
+++ b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkHttpClientConnectionPoolTestSuite.java
@@ -13,55 +13,72 @@
* permissions and limitations under the License.
*/
-package software.amazon.awssdk.core.http;
+package software.amazon.awssdk.http;
-import static software.amazon.awssdk.core.internal.http.timers.ClientExecutionAndRequestTimerTestUtils.executionContext;
+import static org.assertj.core.api.Assertions.assertThat;
import static software.amazon.awssdk.core.internal.util.ResponseHandlerTestUtils.combinedSyncResponseHandler;
+import static utils.HttpTestUtils.executionContext;
import java.time.Duration;
-
-import org.apache.http.conn.ConnectionPoolTimeoutException;
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.BeforeClass;
-import org.junit.Test;
-
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
import software.amazon.awssdk.core.exception.SdkClientException;
-import software.amazon.awssdk.core.http.server.MockServer;
+import software.amazon.awssdk.core.http.NoopTestRequest;
import software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient;
import software.amazon.awssdk.core.internal.http.response.EmptySdkResponseHandler;
-import software.amazon.awssdk.http.SdkHttpFullRequest;
-import software.amazon.awssdk.http.SdkHttpMethod;
-import software.amazon.awssdk.http.apache.ApacheHttpClient;
+import software.amazon.awssdk.http.server.MockServer;
import software.amazon.awssdk.retries.DefaultRetryStrategy;
import utils.HttpTestUtils;
-public class ConnectionPoolMaxConnectionsIntegrationTest {
+/**
+ * Verifies that a client whose connection pool is limited to a single connection fails to lease a second connection while the
+ * first one is still in use.
+ *
+ * This suite applies to sync clients that expose a bounded connection pool through a {@code maxConnections} setting, which
+ * today means {@code apache-client} and {@code apache5-client}. {@code url-connection-client} and {@code aws-crt-client} do not
+ * expose that setting, so they do not subclass this suite.
+ *
+ * The exception raised on pool exhaustion is specific to the underlying HTTP library, so subclasses declare it through
+ * {@link #expectedPoolTimeoutCause()}.
+ */
+public abstract class SdkHttpClientConnectionPoolTestSuite {
private static MockServer server;
- @BeforeClass
+ /**
+ * Returns a client whose connection pool holds at most {@code maxConnections} connections, and which waits no longer than
+ * {@code connectionTimeout} for a connection.
+ */
+ protected abstract SdkHttpClient createSdkHttpClient(int maxConnections, Duration connectionTimeout);
+
+ /**
+ * Returns the exception the underlying HTTP library raises when the pool cannot supply a connection in time.
+ */
+ protected abstract Class extends Exception> expectedPoolTimeoutCause();
+
+ @BeforeAll
public static void setup() {
server = MockServer.createMockServer(MockServer.ServerBehavior.OVERLOADED);
server.startServer();
}
- @AfterClass
+ @AfterAll
public static void tearDown() {
if (server != null) {
server.stopServer();
}
}
- @Test(timeout = 60 * 1000)
+ @Test
+ @Timeout(60)
public void leasing_a_new_connection_fails_with_connection_pool_timeout() {
AmazonSyncHttpClient httpClient = HttpTestUtils.testClientBuilder()
.retryStrategy(DefaultRetryStrategy.doNotRetry())
- .httpClient(ApacheHttpClient.builder()
- .connectionTimeout(Duration.ofMillis(100))
- .maxConnections(1)
- .build())
+ .httpClient(createSdkHttpClient(1, Duration.ofMillis(100)))
.build();
SdkHttpFullRequest request = server.configureHttpEndpoint(SdkHttpFullRequest.builder())
@@ -83,9 +100,9 @@ public void leasing_a_new_connection_fails_with_connection_pool_timeout() {
.originalRequest(NoopTestRequest.builder().build())
.executionContext(executionContext(request))
.execute(combinedSyncResponseHandler(null, null));
- Assert.fail("Connection pool timeout exception is expected!");
+ Assertions.fail("Connection pool timeout exception is expected!");
} catch (SdkClientException e) {
- Assert.assertTrue(e.getCause() instanceof ConnectionPoolTimeoutException);
+ assertThat(e.getCause()).isInstanceOf(expectedPoolTimeoutCause());
}
}
}
diff --git a/test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkHttpClientSdkPipelineBehaviorTestSuite.java b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkHttpClientSdkPipelineBehaviorTestSuite.java
new file mode 100644
index 000000000000..06ef704a0acf
--- /dev/null
+++ b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkHttpClientSdkPipelineBehaviorTestSuite.java
@@ -0,0 +1,230 @@
+/*
+ * 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;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.any;
+import static com.github.tomakehurst.wiremock.client.WireMock.findAll;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.matching;
+import static com.github.tomakehurst.wiremock.client.WireMock.optionsRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import static software.amazon.awssdk.core.internal.util.ResponseHandlerTestUtils.combinedSyncResponseHandler;
+import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
+import static utils.HttpTestUtils.executionContext;
+import static utils.HttpTestUtils.testClientBuilder;
+
+import com.github.tomakehurst.wiremock.verification.LoggedRequest;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import software.amazon.awssdk.core.exception.SdkServiceException;
+import software.amazon.awssdk.core.http.NoopTestRequest;
+import software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient;
+import software.amazon.awssdk.core.internal.http.pipeline.stages.ApplyTransactionIdStage;
+import software.amazon.awssdk.core.internal.http.response.NullErrorResponseHandler;
+import software.amazon.awssdk.core.io.SdkFilterInputStream;
+import utils.http.WireMockTestBase;
+
+/**
+ * Reusable suite that exercises sdk-core's {@link AmazonSyncHttpClient} execution pipeline (client-configuration header
+ * injection, request-header precedence, OPTIONS pass-through, transaction-id consistency across retries, and closing of
+ * every {@link ContentStreamProvider}-created stream) over a real {@link SdkHttpClient}. Each concrete HTTP client module
+ * subclasses this and provides its client via {@link #createSdkHttpClient()}.
+ */
+public abstract class SdkHttpClientSdkPipelineBehaviorTestSuite extends WireMockTestBase {
+
+ private static final String OPERATION = "/some-operation";
+ private static final String HEADER = "Some-Header";
+ private static final String CONFIG_HEADER_VALUE = "client config header value";
+ private static final String REQUEST_HEADER_VALUE = "request header value";
+ private static final String RESOURCE_PATH = "/transaction-id/";
+
+ protected abstract SdkHttpClient createSdkHttpClient();
+
+ @Test
+ public void headersSpecifiedInClientConfigurationArePutOnRequest() {
+ stubFor(any(urlPathEqualTo(OPERATION)).willReturn(aResponse()));
+ SdkHttpFullRequest request = newGetRequest(OPERATION).build();
+
+ AmazonSyncHttpClient sut = createClient(HEADER, CONFIG_HEADER_VALUE);
+ sendRequest(request, sut);
+
+ verify(getRequestedFor(urlPathEqualTo(OPERATION)).withHeader(HEADER, matching(CONFIG_HEADER_VALUE)));
+ }
+
+ @Test
+ public void headersOnRequestsWinOverClientConfigurationHeaders() {
+ stubFor(any(urlPathEqualTo(OPERATION)).willReturn(aResponse()));
+ SdkHttpFullRequest request = newGetRequest(OPERATION)
+ .putHeader(HEADER, REQUEST_HEADER_VALUE)
+ .build();
+
+ AmazonSyncHttpClient sut = createClient(HEADER, CONFIG_HEADER_VALUE);
+ sendRequest(request, sut);
+
+ verify(getRequestedFor(urlPathEqualTo(OPERATION)).withHeader(HEADER, matching(REQUEST_HEADER_VALUE)));
+ }
+
+ @Test
+ public void canHandleOptionsRequest() {
+ stubFor(any(urlPathEqualTo(OPERATION)).willReturn(aResponse()));
+ SdkHttpFullRequest request = newRequest(OPERATION)
+ .method(SdkHttpMethod.OPTIONS)
+ .build();
+
+ AmazonSyncHttpClient sut = amazonSyncHttpClient();
+ sendRequest(request, sut);
+
+ verify(optionsRequestedFor(urlPathEqualTo(OPERATION)));
+ }
+
+ @Test
+ public void retriedRequest_HasSameTransactionIdForAllRetries() throws Exception {
+ stubFor(get(urlEqualTo(RESOURCE_PATH)).willReturn(aResponse().withStatus(500)));
+ executeRequest();
+ assertTransactionIdIsUnchangedAcrossRetries();
+ }
+
+ @Test
+ public void closesAllCreatedInputStreamsFromProvider() {
+ stubFor(any(urlPathEqualTo(OPERATION)).willReturn(aResponse().withStatus(500)));
+
+ TestContentStreamProvider provider = new TestContentStreamProvider();
+ SdkHttpFullRequest request = newRequest(OPERATION)
+ .contentStreamProvider(provider)
+ .method(SdkHttpMethod.PUT)
+ .build();
+
+ AmazonSyncHttpClient testClient = amazonSyncHttpClient();
+ try {
+ sendRequest(request, testClient);
+ fail("Should have thrown SdkServiceException");
+ } catch (SdkServiceException ignored) {
+ // Ignored or expected.
+ }
+
+ // The test client uses the default retry policy so there should be 4
+ // total attempts and an equal number created streams
+ assertThat(provider.getCreatedStreams().size()).isEqualTo(4);
+ for (CloseTrackingInputStream is : provider.getCreatedStreams()) {
+ assertThat(is.isClosed()).isTrue();
+ }
+ }
+
+ private void executeRequest() throws Exception {
+ AmazonSyncHttpClient httpClient = amazonSyncHttpClient();
+ try {
+ SdkHttpFullRequest request = newGetRequest(RESOURCE_PATH).build();
+ httpClient.requestExecutionBuilder()
+ .request(request)
+ .originalRequest(NoopTestRequest.builder().build())
+ .executionContext(executionContext(request))
+ .execute(combinedSyncResponseHandler(null, stubErrorHandler()));
+ fail("Expected exception");
+ } catch (SdkServiceException expected) {
+ // Ignored or expected.
+ }
+ }
+
+ private void assertTransactionIdIsUnchangedAcrossRetries() {
+ String previousTransactionId = null;
+ for (LoggedRequest request : findAll(getRequestedFor(urlEqualTo(RESOURCE_PATH)))) {
+ String currentTransactionId = request.getHeader(ApplyTransactionIdStage.HEADER_SDK_TRANSACTION_ID);
+ // Transaction ID should always be set
+ assertNotNull(currentTransactionId);
+ // Transaction ID should be the same across retries
+ if (previousTransactionId != null) {
+ assertEquals(previousTransactionId, currentTransactionId);
+ }
+ previousTransactionId = currentTransactionId;
+ }
+ }
+
+ private void sendRequest(SdkHttpFullRequest request, AmazonSyncHttpClient sut) {
+ sut.requestExecutionBuilder()
+ .request(request)
+ .originalRequest(NoopTestRequest.builder().build())
+ .executionContext(executionContext(request))
+ .execute(combinedSyncResponseHandler(null, new NullErrorResponseHandler()));
+ }
+
+ private AmazonSyncHttpClient createClient(String headerName, String headerValue) {
+ return testClientBuilder().httpClient(createSdkHttpClient()).additionalHeader(headerName, headerValue).build();
+ }
+
+ private AmazonSyncHttpClient amazonSyncHttpClient() {
+ return testClientBuilder().httpClient(createSdkHttpClient()).build();
+ }
+
+ private static class TestContentStreamProvider implements ContentStreamProvider {
+ private static final byte[] CONTENT_BYTES = "Hello".getBytes(StandardCharsets.UTF_8);
+ private List createdStreams = new ArrayList<>();
+
+ @Override
+ public InputStream newStream() {
+ closeCurrentStream();
+ CloseTrackingInputStream s = newContentStream();
+ createdStreams.add(s);
+ return s;
+ }
+
+ List getCreatedStreams() {
+ return createdStreams;
+ }
+
+ private CloseTrackingInputStream newContentStream() {
+ return new CloseTrackingInputStream(new ByteArrayInputStream(CONTENT_BYTES));
+ }
+
+ private void closeCurrentStream() {
+ if (createdStreams.isEmpty()) {
+ return;
+ }
+ invokeSafely(() -> createdStreams.get(createdStreams.size() - 1).close());
+ }
+ }
+
+ private static class CloseTrackingInputStream extends SdkFilterInputStream {
+ private boolean isClosed = false;
+
+ CloseTrackingInputStream(InputStream in) {
+ super(in);
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ isClosed = true;
+ }
+
+ boolean isClosed() {
+ return isClosed;
+ }
+ }
+}
diff --git a/core/sdk-core/src/it/java/software/amazon/awssdk/core/http/AmazonHttpClientSslHandshakeTimeoutTest.java b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkHttpClientSslHandshakeTimeoutTestSuite.java
similarity index 63%
rename from core/sdk-core/src/it/java/software/amazon/awssdk/core/http/AmazonHttpClientSslHandshakeTimeoutTest.java
rename to test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkHttpClientSslHandshakeTimeoutTestSuite.java
index 3b82be2be2c8..fa64b4728b06 100644
--- a/core/sdk-core/src/it/java/software/amazon/awssdk/core/http/AmazonHttpClientSslHandshakeTimeoutTest.java
+++ b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkHttpClientSslHandshakeTimeoutTestSuite.java
@@ -13,49 +13,67 @@
* permissions and limitations under the License.
*/
-package software.amazon.awssdk.core.http;
+package software.amazon.awssdk.http;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.fail;
-import static software.amazon.awssdk.core.internal.http.timers.ClientExecutionAndRequestTimerTestUtils.executionContext;
+import static org.junit.jupiter.api.Assertions.fail;
import static software.amazon.awssdk.core.internal.util.ResponseHandlerTestUtils.combinedSyncResponseHandler;
+import static utils.HttpTestUtils.executionContext;
import java.io.IOException;
import java.time.Duration;
-
-import org.junit.Test;
-
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
import software.amazon.awssdk.core.exception.SdkClientException;
+import software.amazon.awssdk.core.http.NoopTestRequest;
import software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient;
import software.amazon.awssdk.core.internal.http.response.NullErrorResponseHandler;
-import software.amazon.awssdk.http.SdkHttpFullRequest;
-import software.amazon.awssdk.http.SdkHttpMethod;
-import software.amazon.awssdk.http.apache.ApacheHttpClient;
+import software.amazon.awssdk.http.server.MockServer;
import software.amazon.awssdk.retries.DefaultRetryStrategy;
import utils.HttpTestUtils;
/**
- * This test is to verify that the apache-httpclient library has fixed the bug where socket timeout configuration is
- * incorrectly ignored during SSL handshake. This test is expected to hang (and fail after the junit timeout) if run
- * against the problematic httpclient version (e.g. 4.3).
+ * Verifies that a client honors its configured socket timeout during the SSL handshake. The client is pointed at a server that
+ * accepts the connection and then sends nothing, so a client that ignores the socket timeout hangs until the test times out.
+ *
+ * This suite applies to sync clients that expose a socket timeout, which today means {@code apache-client},
+ * {@code apache5-client} and {@code url-connection-client}. It started as a regression test for a bug in Apache HttpClient
+ * 4.3, where the socket timeout was ignored during the SSL handshake.
*
* @link https://issues.apache.org/jira/browse/HTTPCLIENT-1478
*/
-public class AmazonHttpClientSslHandshakeTimeoutTest extends UnresponsiveMockServerTestBase {
+public abstract class SdkHttpClientSslHandshakeTimeoutTestSuite {
private static final Duration CLIENT_SOCKET_TO = Duration.ofSeconds(1);
- @Test(timeout = 60 * 1000)
+ private MockServer server;
+
+ /**
+ * Returns a client that waits no longer than {@code socketTimeout} for data from the server.
+ */
+ protected abstract SdkHttpClient createSdkHttpClient(Duration socketTimeout);
+
+ @BeforeEach
+ public void setupBaseFixture() {
+ server = MockServer.createMockServer(MockServer.ServerBehavior.UNRESPONSIVE);
+ server.startServer();
+ }
+
+ @AfterEach
+ public void tearDownBaseFixture() {
+ server.stopServer();
+ }
+
+ @Test
+ @Timeout(60)
public void testSslHandshakeTimeout() {
AmazonSyncHttpClient httpClient = HttpTestUtils.testClientBuilder()
.retryStrategy(DefaultRetryStrategy.doNotRetry())
- .httpClient(ApacheHttpClient.builder()
- .socketTimeout(CLIENT_SOCKET_TO)
- .build())
+ .httpClient(createSdkHttpClient(CLIENT_SOCKET_TO))
.build();
- System.out.println("Sending request to localhost...");
-
try {
SdkHttpFullRequest request = server.configureHttpsEndpoint(SdkHttpFullRequest.builder())
.method(SdkHttpMethod.GET)
diff --git a/test/http-client-tests/src/main/java/software/amazon/awssdk/http/server/MockServer.java b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/server/MockServer.java
index 71e6c0c3909a..f1e1986c232f 100644
--- a/test/http-client-tests/src/main/java/software/amazon/awssdk/http/server/MockServer.java
+++ b/test/http-client-tests/src/main/java/software/amazon/awssdk/http/server/MockServer.java
@@ -58,6 +58,10 @@ public static MockServer createMockServer(ServerBehavior serverBehavior) {
return new MockServer(new FullCloseInBetweenServerBehavior());
case FULL_CLOSE_AT_THE_END:
return new MockServer(new FullCloseAtTheEndServerBehavior());
+ case UNRESPONSIVE:
+ return new MockServer(new UnresponsiveServerBehavior());
+ case OVERLOADED:
+ return new MockServer(new OverloadedServerBehavior());
default:
throw new IllegalArgumentException("Unsupported implementation for server issue: " + serverBehavior);
}
@@ -94,6 +98,18 @@ public void startServer(TlsKeyManagersProvider keyManagersProvider) {
listenerThread.start();
}
+ public void startServer() {
+ try {
+ serverSocket = new ServerSocket(0); // auto-assign a port at localhost
+ logger.info(() -> "Listening on port " + serverSocket.getLocalPort());
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to start the server socket.", e);
+ }
+ listenerThread = new MockServerListenerThread(serverSocket, serverBehaviorStrategy);
+ listenerThread.setDaemon(true);
+ listenerThread.start();
+ }
+
public void stopServer() {
listenerThread.interrupt();
try {
@@ -127,7 +143,9 @@ public SdkHttpFullRequest.Builder configureHttpEndpoint(SdkHttpFullRequest.Build
public enum ServerBehavior {
HALF_CLOSE,
FULL_CLOSE_IN_BETWEEN,
- FULL_CLOSE_AT_THE_END
+ FULL_CLOSE_AT_THE_END,
+ UNRESPONSIVE,
+ OVERLOADED
}
public interface ServerBehaviorStrategy {
@@ -293,6 +311,73 @@ public void runServer(ServerSocket serverSocket) {
}
}
+ /**
+ * A server behavior which accepts a single connection and then holds it open without writing any bytes. The test client
+ * talking to this server is expected to timeout appropriately, instead of hanging and waiting for the response forever.
+ */
+ public static class UnresponsiveServerBehavior implements ServerBehaviorStrategy {
+
+ @Override
+ public void runServer(ServerSocket serverSocket) {
+ Socket socket = null;
+ try {
+ socket = serverSocket.accept();
+ Socket acceptedSocket = socket;
+ logger.info(() -> "Socket created on port " + acceptedSocket.getLocalPort());
+ while (true) {
+ logger.debug(() -> "Holding the connection open without responding.");
+ Thread.sleep(10 * 1000);
+ }
+ } catch (InterruptedException e) {
+ // Stop server will interrupt to stop this thread.
+ return;
+ } catch (IOException e) {
+ throw new RuntimeException("Error when waiting for new socket connection.", e);
+ } finally {
+ closeQuietly(socket);
+ }
+ }
+ }
+
+ /**
+ * A server behavior which accepts a connection, writes a partial HTTP response (with a Content-Length larger than the bytes
+ * actually sent) and then keeps holding the connection open while periodically writing a few more bytes. The test client
+ * talking to this server is expected to timeout appropriately, instead of hanging and waiting for the response forever.
+ */
+ public static class OverloadedServerBehavior implements ServerBehaviorStrategy {
+
+ @Override
+ public void runServer(ServerSocket serverSocket) {
+ try {
+ while (true) {
+ Socket socket = null;
+ try {
+ socket = serverSocket.accept();
+ try (DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {
+ out.writeBytes("HTTP/1.1 200 OK\r\n");
+ out.writeBytes("Content-Type: text/html\r\n");
+ out.writeBytes("Content-Length: 500\r\n\r\n");
+ out.writeBytes("Hello.");
+ while (true) {
+ Thread.sleep(1000);
+ out.writeBytes("Hi.");
+ }
+ }
+ } catch (SocketException se) {
+ // Ignored or expected.
+ } finally {
+ closeQuietly(socket);
+ }
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Error when waiting for new socket connection.", e);
+ } catch (InterruptedException e) {
+ // Stop server will interrupt to stop this thread.
+ return;
+ }
+ }
+ }
+
// TLS1.3 enbaled by default for 8u341 onwards.
// https://www.oracle.com/java/technologies/javase/8u341-relnotes.html
diff --git a/test/http-client-tests/src/main/java/utils/HttpTestUtils.java b/test/http-client-tests/src/main/java/utils/HttpTestUtils.java
new file mode 100644
index 000000000000..928146962343
--- /dev/null
+++ b/test/http-client-tests/src/main/java/utils/HttpTestUtils.java
@@ -0,0 +1,221 @@
+/*
+ * 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 utils;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+import software.amazon.awssdk.core.ClientEndpointProvider;
+import software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption;
+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.ExecutionContext;
+import software.amazon.awssdk.core.http.NoopTestRequest;
+import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
+import software.amazon.awssdk.core.interceptor.ExecutionInterceptorChain;
+import software.amazon.awssdk.core.interceptor.InterceptorContext;
+import software.amazon.awssdk.core.internal.http.AmazonAsyncHttpClient;
+import software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient;
+import software.amazon.awssdk.core.internal.retry.SdkDefaultRetryStrategy;
+import software.amazon.awssdk.core.signer.NoOpSigner;
+import software.amazon.awssdk.http.SdkHttpClient;
+import software.amazon.awssdk.http.SdkHttpFullRequest;
+import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
+import software.amazon.awssdk.metrics.MetricCollector;
+import software.amazon.awssdk.retries.api.RetryStrategy;
+import software.amazon.awssdk.utils.Validate;
+
+/**
+ * Test helpers for building {@link AmazonSyncHttpClient} / {@link AmazonAsyncHttpClient} around a caller-supplied
+ * {@link SdkHttpClient} / {@link SdkAsyncHttpClient}. Unlike the sdk-core copy of this class, the builders here require an
+ * explicit transport (there is no ServiceLoader-based default), so that per-client test suites can drive the sdk-core
+ * execution pipeline over each real HTTP client.
+ */
+public class HttpTestUtils {
+
+ private HttpTestUtils() {
+ }
+
+ public static TestClientBuilder testClientBuilder() {
+ return new TestClientBuilder();
+ }
+
+ public static TestAsyncClientBuilder testAsyncClientBuilder() {
+ return new TestAsyncClientBuilder();
+ }
+
+ /**
+ * Builds a minimal {@link ExecutionContext} for driving {@link AmazonSyncHttpClient} / {@link AmazonAsyncHttpClient}
+ * directly.
+ */
+ public static ExecutionContext executionContext(SdkHttpFullRequest request) {
+ InterceptorContext interceptorContext =
+ InterceptorContext.builder()
+ .request(NoopTestRequest.builder().build())
+ .httpRequest(request)
+ .build();
+ return ExecutionContext.builder()
+ .signer(new NoOpSigner())
+ .interceptorChain(new ExecutionInterceptorChain(Collections.emptyList()))
+ .executionAttributes(new ExecutionAttributes())
+ .interceptorContext(interceptorContext)
+ .metricCollector(MetricCollector.create("ApiCall"))
+ .build();
+ }
+
+ public static SdkClientConfiguration testClientConfiguration() {
+ return SdkClientConfiguration.builder()
+ .option(SdkClientOption.EXECUTION_INTERCEPTORS, new ArrayList<>())
+ .option(SdkClientOption.CLIENT_ENDPOINT_PROVIDER,
+ ClientEndpointProvider.forEndpointOverride(URI.create("http://localhost:8080")))
+ .option(SdkClientOption.RETRY_STRATEGY,
+ SdkDefaultRetryStrategy.defaultRetryStrategy())
+ .option(SdkClientOption.ADDITIONAL_HTTP_HEADERS, new HashMap<>())
+ .option(SdkClientOption.CRC32_FROM_COMPRESSED_DATA_ENABLED, false)
+ .option(SdkAdvancedClientOption.SIGNER, new NoOpSigner())
+ .option(SdkAdvancedClientOption.USER_AGENT_PREFIX, "")
+ .option(SdkAdvancedClientOption.USER_AGENT_SUFFIX, "")
+ .option(SdkClientOption.SCHEDULED_EXECUTOR_SERVICE, Executors.newScheduledThreadPool(1))
+ .option(SdkAdvancedAsyncClientOption.FUTURE_COMPLETION_EXECUTOR, Runnable::run)
+ .build();
+ }
+
+ public static class TestClientBuilder {
+ private RetryStrategy retryStrategy;
+ private SdkHttpClient httpClient;
+ private Map additionalHeaders = new HashMap<>();
+ private Duration apiCallTimeout;
+ private Duration apiCallAttemptTimeout;
+
+ public TestClientBuilder retryStrategy(RetryStrategy retryStrategy) {
+ this.retryStrategy = retryStrategy;
+ return this;
+ }
+
+ public TestClientBuilder httpClient(SdkHttpClient sdkHttpClient) {
+ this.httpClient = sdkHttpClient;
+ return this;
+ }
+
+ public TestClientBuilder additionalHeader(String key, String value) {
+ this.additionalHeaders.put(key, value);
+ return this;
+ }
+
+ public TestClientBuilder apiCallTimeout(Duration duration) {
+ this.apiCallTimeout = duration;
+ return this;
+ }
+
+ public TestClientBuilder apiCallAttemptTimeout(Duration timeout) {
+ this.apiCallAttemptTimeout = timeout;
+ return this;
+ }
+
+ public AmazonSyncHttpClient build() {
+ SdkHttpClient sdkHttpClient = Validate.paramNotNull(this.httpClient, "httpClient");
+ return new AmazonSyncHttpClient(testClientConfiguration().toBuilder()
+ .option(SdkClientOption.SYNC_HTTP_CLIENT, sdkHttpClient)
+ .applyMutation(this::configureRetryStrategy)
+ .applyMutation(this::configureAdditionalHeaders)
+ .option(SdkClientOption.API_CALL_TIMEOUT, apiCallTimeout)
+ .option(SdkClientOption.API_CALL_ATTEMPT_TIMEOUT,
+ apiCallAttemptTimeout)
+ .build());
+ }
+
+ private void configureAdditionalHeaders(SdkClientConfiguration.Builder builder) {
+ Map> headers =
+ this.additionalHeaders.entrySet().stream()
+ .collect(Collectors.toMap(Map.Entry::getKey, e -> Arrays.asList(e.getValue())));
+
+ builder.option(SdkClientOption.ADDITIONAL_HTTP_HEADERS, headers);
+ }
+
+ private void configureRetryStrategy(SdkClientConfiguration.Builder builder) {
+ if (retryStrategy != null) {
+ builder.option(SdkClientOption.RETRY_STRATEGY, retryStrategy);
+ }
+ }
+ }
+
+ public static class TestAsyncClientBuilder {
+ private RetryStrategy retryStrategy;
+ private SdkAsyncHttpClient asyncHttpClient;
+ private Duration apiCallTimeout;
+ private Duration apiCallAttemptTimeout;
+ private Map additionalHeaders = new HashMap<>();
+
+ public TestAsyncClientBuilder retryStrategy(RetryStrategy retryStrategy) {
+ this.retryStrategy = retryStrategy;
+ return this;
+ }
+
+ public TestAsyncClientBuilder asyncHttpClient(SdkAsyncHttpClient asyncHttpClient) {
+ this.asyncHttpClient = asyncHttpClient;
+ return this;
+ }
+
+ public TestAsyncClientBuilder additionalHeader(String key, String value) {
+ this.additionalHeaders.put(key, value);
+ return this;
+ }
+
+ public TestAsyncClientBuilder apiCallTimeout(Duration duration) {
+ this.apiCallTimeout = duration;
+ return this;
+ }
+
+ public TestAsyncClientBuilder apiCallAttemptTimeout(Duration timeout) {
+ this.apiCallAttemptTimeout = timeout;
+ return this;
+ }
+
+ public AmazonAsyncHttpClient build() {
+ SdkAsyncHttpClient asyncHttpClient = Validate.paramNotNull(this.asyncHttpClient, "asyncHttpClient");
+ return new AmazonAsyncHttpClient(testClientConfiguration().toBuilder()
+ .option(SdkClientOption.ASYNC_HTTP_CLIENT, asyncHttpClient)
+ .option(SdkClientOption.API_CALL_TIMEOUT, apiCallTimeout)
+ .option(SdkClientOption.API_CALL_ATTEMPT_TIMEOUT,
+ apiCallAttemptTimeout)
+ .applyMutation(this::configureRetryStrategy)
+ .applyMutation(this::configureAdditionalHeaders)
+ .build());
+ }
+
+ private void configureAdditionalHeaders(SdkClientConfiguration.Builder builder) {
+ Map> headers =
+ this.additionalHeaders.entrySet().stream()
+ .collect(Collectors.toMap(Map.Entry::getKey, e -> Arrays.asList(e.getValue())));
+
+ builder.option(SdkClientOption.ADDITIONAL_HTTP_HEADERS, headers);
+ }
+
+ private void configureRetryStrategy(SdkClientConfiguration.Builder builder) {
+ if (retryStrategy != null) {
+ builder.option(SdkClientOption.RETRY_STRATEGY, retryStrategy);
+ }
+ }
+ }
+}
diff --git a/test/http-client-tests/src/main/java/utils/ValidSdkObjects.java b/test/http-client-tests/src/main/java/utils/ValidSdkObjects.java
new file mode 100644
index 000000000000..221ea778c1a8
--- /dev/null
+++ b/test/http-client-tests/src/main/java/utils/ValidSdkObjects.java
@@ -0,0 +1,71 @@
+/*
+ * 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 utils;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Optional;
+import software.amazon.awssdk.core.RequestOverrideConfiguration;
+import software.amazon.awssdk.core.SdkField;
+import software.amazon.awssdk.core.SdkRequest;
+import software.amazon.awssdk.http.SdkHttpFullRequest;
+import software.amazon.awssdk.http.SdkHttpFullResponse;
+import software.amazon.awssdk.http.SdkHttpMethod;
+
+/**
+ * A collection of objects (or object builder) pre-populated with all required fields. This allows tests to focus on what data
+ * they care about, not necessarily what data is required.
+ */
+public final class ValidSdkObjects {
+ private ValidSdkObjects() {
+ }
+
+ public static SdkRequest sdkRequest() {
+ return new SdkRequest() {
+ @Override
+ public Optional extends RequestOverrideConfiguration> overrideConfiguration() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Builder toBuilder() {
+ return null;
+ }
+
+ @Override
+ public List> sdkFields() {
+ return null;
+ }
+ };
+ }
+
+ public static SdkHttpFullRequest.Builder sdkHttpFullRequest() {
+ return sdkHttpFullRequest(80);
+ }
+
+ public static SdkHttpFullRequest.Builder sdkHttpFullRequest(int port) {
+ return SdkHttpFullRequest.builder()
+ .uri(URI.create("http://localhost"))
+ .port(port)
+ .putHeader("Host", "localhost")
+ .method(SdkHttpMethod.GET);
+ }
+
+ public static SdkHttpFullResponse.Builder sdkHttpFullResponse() {
+ return SdkHttpFullResponse.builder()
+ .statusCode(200);
+ }
+}
diff --git a/test/http-client-tests/src/main/java/utils/http/WireMockTestBase.java b/test/http-client-tests/src/main/java/utils/http/WireMockTestBase.java
new file mode 100644
index 000000000000..1df3d1fea019
--- /dev/null
+++ b/test/http-client-tests/src/main/java/utils/http/WireMockTestBase.java
@@ -0,0 +1,62 @@
+/*
+ * 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 utils.http;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import java.net.URI;
+import org.junit.Rule;
+import software.amazon.awssdk.core.exception.SdkServiceException;
+import software.amazon.awssdk.core.http.HttpResponseHandler;
+import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
+import software.amazon.awssdk.http.SdkHttpFullRequest;
+import software.amazon.awssdk.http.SdkHttpFullResponse;
+import software.amazon.awssdk.http.SdkHttpMethod;
+
+/**
+ * Base class for tests that use a WireMock server
+ */
+public abstract class WireMockTestBase {
+
+ @Rule
+ public WireMockRule mockServer = new WireMockRule(0);
+
+ protected SdkHttpFullRequest.Builder newGetRequest(String resourcePath) {
+ return newRequest(resourcePath)
+ .method(SdkHttpMethod.GET);
+ }
+
+ protected SdkHttpFullRequest.Builder newRequest(String resourcePath) {
+ return SdkHttpFullRequest.builder()
+ .uri(URI.create("http://localhost"))
+ .port(mockServer.port())
+ .encodedPath(resourcePath);
+ }
+
+ protected HttpResponseHandler stubErrorHandler() throws Exception {
+ HttpResponseHandler errorHandler = mock(HttpResponseHandler.class);
+ when(errorHandler.handle(any(SdkHttpFullResponse.class), any(ExecutionAttributes.class))).thenReturn(mockException());
+ return errorHandler;
+ }
+
+ private SdkServiceException mockException() {
+ SdkServiceException exception = SdkServiceException.builder().message("Dummy error response").statusCode(500).build();
+ return exception;
+ }
+}