diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttpAsyncTlsRequired.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttpAsyncTlsRequired.java
new file mode 100644
index 0000000000..cc8be513ac
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttpAsyncTlsRequired.java
@@ -0,0 +1,190 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.testing.async;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.hc.client5.http.UnsupportedSchemeException;
+import org.apache.hc.client5.http.async.AsyncExecChainHandler;
+import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
+import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
+import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
+import org.apache.hc.client5.http.impl.ChainElement;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
+import org.apache.hc.client5.testing.extension.async.ClientProtocolLevel;
+import org.apache.hc.client5.testing.extension.async.ServerProtocolLevel;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.pool.PoolStats;
+import org.apache.hc.core5.util.Deadline;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestHttpAsyncTlsRequired extends AbstractIntegrationTestBase {
+
+ private final AtomicInteger serverRequests = new AtomicInteger();
+
+ private PoolingAsyncClientConnectionManager connManager;
+ private CloseableHttpAsyncClient plainClient;
+ private CloseableHttpAsyncClient tlsClient;
+
+ public TestHttpAsyncTlsRequired() {
+ super(URIScheme.HTTP, ClientProtocolLevel.STANDARD, ServerProtocolLevel.STANDARD);
+ }
+
+ @AfterEach
+ void cleanup() {
+ if (plainClient != null) {
+ plainClient.close(CloseMode.IMMEDIATE);
+ }
+ if (tlsClient != null) {
+ tlsClient.close(CloseMode.IMMEDIATE);
+ }
+ if (connManager != null) {
+ connManager.close(CloseMode.IMMEDIATE);
+ }
+ }
+
+ private HttpHost startCountingServer() throws Exception {
+ configureServer(bootstrap -> bootstrap.register("*", () -> new AbstractSimpleServerExchangeHandler() {
+
+ @Override
+ protected SimpleHttpResponse handle(
+ final SimpleHttpRequest request,
+ final HttpCoreContext context) {
+ serverRequests.incrementAndGet();
+ final SimpleHttpResponse response = new SimpleHttpResponse(HttpStatus.SC_OK);
+ response.setBody("Whatever", ContentType.TEXT_PLAIN);
+ return response;
+ }
+ }));
+ return startServer();
+ }
+
+ private static AsyncExecChainHandler recorder(final AtomicInteger transmitted) {
+ return (request, entityProducer, scope, chain, asyncExecCallback) -> {
+ transmitted.incrementAndGet();
+ chain.proceed(request, entityProducer, scope, asyncExecCallback);
+ };
+ }
+
+ private static CloseableHttpAsyncClient newClient(
+ final PoolingAsyncClientConnectionManager connManager,
+ final boolean tlsRequired,
+ final boolean recordRightAfterTlsCheck,
+ final AtomicInteger transmitted) {
+ final HttpAsyncClientBuilder builder = HttpAsyncClients.custom()
+ .setConnectionManager(connManager)
+ .setConnectionManagerShared(true)
+ .setTlsRequired(tlsRequired);
+ if (recordRightAfterTlsCheck) {
+ builder.addExecInterceptorAfter(ChainElement.TLS_REQUIRED.name(), "recorder", recorder(transmitted));
+ } else {
+ builder.addExecInterceptorLast("recorder", recorder(transmitted));
+ }
+ final CloseableHttpAsyncClient client = builder.build();
+ client.start();
+ return client;
+ }
+
+ // The result future completes before the connection is returned to the pool.
+ private static PoolStats awaitRelease(final PoolingAsyncClientConnectionManager connManager) throws InterruptedException {
+ final Deadline deadline = Deadline.calculate(TIMEOUT);
+ for (;;) {
+ final PoolStats stats = connManager.getTotalStats();
+ if (stats.getLeased() == 0) {
+ return stats;
+ }
+ Assertions.assertFalse(deadline.isExpired(), "Connection not released: " + stats);
+ Thread.sleep(10);
+ }
+ }
+
+ private static SimpleHttpRequest get(final HttpHost target) {
+ return SimpleRequestBuilder.get()
+ .setHttpHost(target)
+ .setPath("/")
+ .build();
+ }
+
+ @Test
+ void rejectedCleartextRequestIsNeverTransmitted() throws Exception {
+ final HttpHost target = startCountingServer();
+ final AtomicInteger transmitted = new AtomicInteger();
+ connManager = PoolingAsyncClientConnectionManagerBuilder.create().build();
+ tlsClient = newClient(connManager, true, true, transmitted);
+
+ final Future future = tlsClient.execute(get(target), null);
+ final ExecutionException ex = Assertions.assertThrows(ExecutionException.class,
+ () -> future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
+ Assertions.assertInstanceOf(UnsupportedSchemeException.class, ex.getCause());
+
+ Assertions.assertEquals(0, transmitted.get());
+ Assertions.assertEquals(0, serverRequests.get());
+ }
+
+ @Test
+ void rejectedCleartextRequestDoesNotUseSharedWarmPool() throws Exception {
+ final HttpHost target = startCountingServer();
+ final AtomicInteger plainTransmitted = new AtomicInteger();
+ final AtomicInteger tlsTransmitted = new AtomicInteger();
+ connManager = PoolingAsyncClientConnectionManagerBuilder.create().build();
+ plainClient = newClient(connManager, false, false, plainTransmitted);
+ tlsClient = newClient(connManager, true, false, tlsTransmitted);
+
+ final SimpleHttpResponse first = plainClient.execute(get(target), null)
+ .get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+ Assertions.assertEquals(HttpStatus.SC_OK, first.getCode());
+ Assertions.assertEquals(1, awaitRelease(connManager).getAvailable());
+
+ final Future rejected = tlsClient.execute(get(target), null);
+ final ExecutionException ex = Assertions.assertThrows(ExecutionException.class,
+ () -> rejected.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
+ Assertions.assertInstanceOf(UnsupportedSchemeException.class, ex.getCause());
+ Assertions.assertEquals(0, tlsTransmitted.get());
+
+ final SimpleHttpResponse second = plainClient.execute(get(target), null)
+ .get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+ Assertions.assertEquals(HttpStatus.SC_OK, second.getCode());
+
+ Assertions.assertEquals(2, plainTransmitted.get());
+ Assertions.assertEquals(2, serverRequests.get());
+ Assertions.assertEquals(1, awaitRelease(connManager).getAvailable());
+ }
+
+}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/TlsRequiredAsyncExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/TlsRequiredAsyncExec.java
index 360fbd65ec..e70ec49a03 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/TlsRequiredAsyncExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/TlsRequiredAsyncExec.java
@@ -56,6 +56,7 @@ public void execute(
final HttpRoute route = scope.route;
if (route != null && !route.isSecure()) {
asyncExecCallback.failed(new UnsupportedSchemeException("Cleartext HTTP is disabled (TLS required)"));
+ return;
}
chain.proceed(request, entityProducer, scope, asyncExecCallback);
}
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/TestTlsRequiredAsyncExec.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/TestTlsRequiredAsyncExec.java
new file mode 100644
index 0000000000..74e179deae
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/TestTlsRequiredAsyncExec.java
@@ -0,0 +1,88 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.impl.async;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.hc.client5.http.HttpRoute;
+import org.apache.hc.client5.http.UnsupportedSchemeException;
+import org.apache.hc.client5.http.async.AsyncExecCallback;
+import org.apache.hc.client5.http.async.AsyncExecChain;
+import org.apache.hc.client5.http.async.AsyncExecRuntime;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.core5.concurrent.CancellableDependency;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.nio.AsyncEntityProducer;
+import org.apache.hc.core5.http.support.BasicRequestBuilder;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class TestTlsRequiredAsyncExec {
+
+ @Mock
+ private AsyncExecChain chain;
+ @Mock
+ private AsyncExecCallback callback;
+ @Mock
+ private AsyncExecRuntime execRuntime;
+ @Mock
+ private AsyncEntityProducer entityProducer;
+
+ private AsyncExecChain.Scope scope(final HttpRoute route, final HttpRequest request) {
+ return new AsyncExecChain.Scope(
+ "test", route, request, Mockito.mock(CancellableDependency.class),
+ HttpClientContext.create(), execRuntime, null, new AtomicInteger(1));
+ }
+
+ @Test
+ void cleartextRouteIsRejectedWithoutProceeding() throws Exception {
+ final HttpRequest request = BasicRequestBuilder.get("http://localhost/").build();
+ final AsyncExecChain.Scope scope = scope(new HttpRoute(new HttpHost("http", "localhost", 80)), request);
+
+ new TlsRequiredAsyncExec().execute(request, entityProducer, scope, chain, callback);
+
+ Mockito.verify(callback, Mockito.times(1)).failed(Mockito.any(UnsupportedSchemeException.class));
+ Mockito.verify(chain, Mockito.never()).proceed(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
+ }
+
+ @Test
+ void secureRouteProceeds() throws Exception {
+ final HttpRequest request = BasicRequestBuilder.get("https://localhost/").build();
+ final AsyncExecChain.Scope scope = scope(new HttpRoute(new HttpHost("https", "localhost", 443), null, true), request);
+
+ new TlsRequiredAsyncExec().execute(request, entityProducer, scope, chain, callback);
+
+ Mockito.verify(chain, Mockito.times(1)).proceed(request, entityProducer, scope, callback);
+ Mockito.verify(callback, Mockito.never()).failed(Mockito.any());
+ }
+
+}