diff --git a/README.md b/README.md
index 80e48173c8..fcf3f5580c 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,22 @@ Other dependencies are optional.
(for detailed information on external dependencies please see [pom.xml](./pom.xml))
+Protocol conformance
+--------------------
+
+- [RFC 9110](https://datatracker.ietf.org/doc/html/rfc9110) - HTTP Semantics
+- [RFC 9111](https://datatracker.ietf.org/doc/html/rfc9111) - HTTP Caching
+- [RFC 9112](https://datatracker.ietf.org/doc/html/rfc9112) - Hypertext Transfer Protocol Version 1.1 (HTTP/1.1)
+- [RFC 7540](https://datatracker.ietf.org/doc/html/rfc7540) - Hypertext Transfer Protocol Version 2 (HTTP/2)
+- [RFC 7541](https://datatracker.ietf.org/doc/html/rfc7541) - HPACK: Header Compression for HTTP/2
+- [RFC 1945](https://datatracker.ietf.org/doc/html/rfc1945) - Hypertext Transfer Protocol -- HTTP/1.0
+- [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986) - Uniform Resource Identifier (URI): Generic Syntax
+- [RFC 6265](https://datatracker.ietf.org/doc/html/rfc6265) - HTTP State Management Mechanism (Cookies)
+- [RFC 7616](https://datatracker.ietf.org/doc/html/rfc7616) - HTTP Digest Access Authentication
+- [RFC 7617](https://datatracker.ietf.org/doc/html/rfc7617) - HTTP 'Basic' Authentication Scheme
+- [RFC 5861](https://datatracker.ietf.org/doc/html/rfc5861) - HTTP Cache-Control Extensions for Stale Content
+- [RFC 2817](https://datatracker.ietf.org/doc/html/rfc2817) - Upgrading to TLS Within HTTP/1.1
+
Licensing
---------
diff --git a/RELEASE_NOTES.txt b/RELEASE_NOTES.txt
index ef49b3a9f0..9c592ad278 100644
--- a/RELEASE_NOTES.txt
+++ b/RELEASE_NOTES.txt
@@ -1,9 +1,83 @@
+Release 5.5.2
+------------------
+
+This is a maintenance release that fixes incompatibility with the 5.4 branch
+of HttpCore.
+
+
+Change Log
+-------------------
+
+* Limit the length of the content codec list that can be processed automatically.
+ Contributed by Oleg Kalnichevski
+
+* Fixed incompatibility with HttpCore 5.4
+ Contributed by Oleg Kalnichevski
+
+* HTTPCLIENT-2403: Mutual authentication check not performed for proxies (#745).
+ Contributed by Istvan Toth
+
+
+
Release 5.5.1
------------------
+This is a maintenance release that fixes several defects in the connection management
+code and a regression in the DIGEST authentication reported since the previous release.
+It also upgrades HttpCore to version 5.3.6.
+
+
Change Log
-------------------
+* HTTPCLIENT-2391: Improved GRACEGUL shutdown of ExecutorService used internally by async
+ clients.
+ Contributed by Oleg Kalnichevski
+
+* Bug fix: Use a 1-second timeout when closing out connections inside a connection pool lock.
+ Contributed by Oleg Kalnichevski
+
+* Clarified the behavior of the protocol-level responseTimeout and the connection management
+ level socketTimeout and their interrelation.
+ Contributed by ChangYong
+
+* Bug fix: Connection managers to ensure open connections have a socket timeout set based on
+ ConnectionConfig upon lease.
+ Contributed by Oleg Kalnichevski
+
+* HTTPCLIENT-2393: Remove `rspauth` attribute from `Authorization` DIGEST header (#716)
+ RFC 7616 compliance: rspauth is server-side (Authentication-Info 3.5) only.
+ Contributed by Arturo Bernal
+
+* HTTPCLIENT-2386: Classic transport to use the connect timeout as a default if the TLS timeout has
+ not been explicitly set.
+ Contributed by Oleg Kalnichevski
+
+* HTTPCLIENT-2384: Socket options related to TcpKeepAlive are ignored.
+ Contributed by Oleg Kalnichevski
+
+* HTTPCLIENT-2371: Logging of request re-execution at INFO priority.
+ Contributed by Oleg Kalnichevski
+
+* HTTPCLIENT-2379: Fixed a defect in H2SharingConnPool causing an IllegalStateException
+ when releasing the same connection from multiple threads. (#663)
+ Contributed by Arturo Bernal
+
+* Fixed the behavior of the `validateAfterInactivity` connection setting by the async
+ connection manager.
+ Contributed by Ryan Schmitt
+
+* HTTPCLIENT-2376: Fixed the problem with ContentCompressionExec not taking `acceptEncoding`
+ parameter into account.
+ Contributed by Oleg Kalnichevski
+
+* HTTPCLIENT-2372: Normalize HttpHost port comparison to treat implicit default ports as
+ equal (#643).
+ Contributed by Arturo Bernal
+
+* Maven wrapper
+ Contributed by Ryan Schmitt
+
* Bump org.junit:junit-bom from 5.12.2 to 5.13.0 #639.
Contributed by Gary Gregory
@@ -93,7 +167,7 @@ Release 5.5 ALPHA1
------------------
This is the first ALPHA release in the 5.5 release series. It adds several experimental
-features and improvements such as request multiplexing over a shared HTTP/2 connection
+features and improvements, such as request multiplexing over a shared HTTP/2 connection
and the Classic API facade acting as a compatibility bridge between classic I/O client
services and the asynchronous message transport used internally.
diff --git a/httpclient5-cache/pom.xml b/httpclient5-cache/pom.xml
index 5d19f7ec41..3ab32dc44b 100644
--- a/httpclient5-cache/pom.xml
+++ b/httpclient5-cache/pom.xml
@@ -28,7 +28,7 @@
org.apache.httpcomponents.client5
httpclient5-parent
- 5.5.1-SNAPSHOT
+ 5.5.3-SNAPSHOT
httpclient5-cache
Apache HttpClient Cache
diff --git a/httpclient5-fluent/pom.xml b/httpclient5-fluent/pom.xml
index e98aecda40..01eb1a1212 100644
--- a/httpclient5-fluent/pom.xml
+++ b/httpclient5-fluent/pom.xml
@@ -28,7 +28,7 @@
org.apache.httpcomponents.client5
httpclient5-parent
- 5.5.1-SNAPSHOT
+ 5.5.3-SNAPSHOT
httpclient5-fluent
Apache HttpClient Fluent
diff --git a/httpclient5-testing/pom.xml b/httpclient5-testing/pom.xml
index 88baa4329f..7be2f43cbc 100644
--- a/httpclient5-testing/pom.xml
+++ b/httpclient5-testing/pom.xml
@@ -28,7 +28,7 @@
org.apache.httpcomponents.client5
httpclient5-parent
- 5.5.1-SNAPSHOT
+ 5.5.3-SNAPSHOT
httpclient5-testing
Apache HttpClient Integration Tests
@@ -79,7 +79,7 @@
org.junit.jupiter
- junit-jupiter-params
+ junit-jupiter
test
diff --git a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/classic/ServiceUnavailableDecorator.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/classic/ServiceUnavailableDecorator.java
new file mode 100644
index 0000000000..3a00cbfd56
--- /dev/null
+++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/classic/ServiceUnavailableDecorator.java
@@ -0,0 +1,80 @@
+/*
+ * ====================================================================
+ * 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.classic;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.hc.core5.function.Resolver;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.HttpVersion;
+import org.apache.hc.core5.http.ProtocolVersion;
+import org.apache.hc.core5.http.io.HttpServerRequestHandler;
+import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.TimeValue;
+
+public class ServiceUnavailableDecorator implements HttpServerRequestHandler {
+
+ private final HttpServerRequestHandler requestHandler;
+ private final Resolver serviceAvailabilityResolver;
+ private final AtomicBoolean serviceUnavailable;
+
+ public ServiceUnavailableDecorator(final HttpServerRequestHandler requestHandler,
+ final Resolver serviceAvailabilityResolver) {
+ this.requestHandler = Args.notNull(requestHandler, "Request handler");
+ this.serviceAvailabilityResolver = Args.notNull(serviceAvailabilityResolver, "Service availability resolver");
+ this.serviceUnavailable = new AtomicBoolean();
+ }
+
+ @Override
+ public void handle(final ClassicHttpRequest request,
+ final ResponseTrigger responseTrigger,
+ final HttpContext context) throws HttpException, IOException {
+ final TimeValue retryAfter = serviceAvailabilityResolver.resolve(request);
+ serviceUnavailable.set(TimeValue.isPositive(retryAfter));
+ if (serviceUnavailable.get()) {
+ final ClassicHttpResponse response = new BasicClassicHttpResponse(HttpStatus.SC_SERVICE_UNAVAILABLE);
+ response.addHeader(HttpHeaders.RETRY_AFTER, Long.toString(retryAfter.toSeconds()));
+ final ProtocolVersion version = request.getVersion();
+ if (version != null && version.compareToVersion(HttpVersion.HTTP_2) < 0) {
+ response.addHeader(HttpHeaders.CONNECTION, "Close");
+ }
+ responseTrigger.submitResponse(response);
+ } else {
+ requestHandler.handle(request, responseTrigger, context);
+ }
+ }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestAsyncConnectionManagement.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestAsyncConnectionManagement.java
new file mode 100644
index 0000000000..b44df5d5c5
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestAsyncConnectionManagement.java
@@ -0,0 +1,359 @@
+/*
+ * ====================================================================
+ * 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.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.apache.hc.client5.http.HttpRoute;
+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.async.methods.SimpleRequestProducer;
+import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer;
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.impl.ConnectionHolder;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.nio.AsyncConnectionEndpoint;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.testing.extension.async.ClientProtocolLevel;
+import org.apache.hc.client5.testing.extension.async.ServerProtocolLevel;
+import org.apache.hc.client5.testing.extension.async.TestAsyncClient;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpConnection;
+import org.apache.hc.core5.http.HttpHeaders;
+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.pool.PoolConcurrencyPolicy;
+import org.apache.hc.core5.pool.PoolReusePolicy;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class TestAsyncConnectionManagement extends AbstractIntegrationTestBase {
+
+ static final Timeout TIMEOUT = Timeout.ofSeconds(30);
+ static final Timeout LEASE_TIMEOUT = Timeout.ofSeconds(5);
+
+ public TestAsyncConnectionManagement() {
+ super(URIScheme.HTTP, ClientProtocolLevel.STANDARD, ServerProtocolLevel.STANDARD);
+ }
+
+ @BeforeEach
+ void setup() {
+ configureServer(bootstrap -> bootstrap.register("*", () -> new AbstractSimpleServerExchangeHandler() {
+
+ @Override
+ protected SimpleHttpResponse handle(
+ final SimpleHttpRequest request,
+ final HttpCoreContext context) {
+ final SimpleHttpResponse response = new SimpleHttpResponse(HttpStatus.SC_OK);
+ response.setBody("Whatever", ContentType.TEXT_PLAIN);
+ return response;
+ }
+ }));
+ }
+
+ /**
+ * Tests releasing and re-using a connection after a response is read.
+ */
+ @Test
+ void testReleaseConnection() throws Exception {
+ final HttpHost target = startServer();
+
+ final TestAsyncClient client = startClient();
+
+ final PoolingAsyncClientConnectionManager connManager = client.getConnectionManager();
+ connManager.setMaxTotal(1);
+
+ final HttpRoute route = new HttpRoute(target, null, false);
+ final HttpClientContext context = HttpClientContext.create();
+
+ final Future endpointFuture1 = connManager.lease("id1", route, null, TIMEOUT, null);
+ final AsyncConnectionEndpoint endpoint1 = endpointFuture1.get(LEASE_TIMEOUT.getDuration(), LEASE_TIMEOUT.getTimeUnit());
+
+ final Future connectFuture1 = connManager.connect(endpoint1, client.getImplementation(), TIMEOUT, null, context, null);
+ final AsyncConnectionEndpoint openEndpoint1 = connectFuture1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+
+ final SimpleHttpRequest request = SimpleRequestBuilder.get()
+ .setHttpHost(target)
+ .setPath("/")
+ .addHeader(HttpHeaders.HOST, target.toHostString())
+ .build();
+
+ final Future responseFuture1 = openEndpoint1.execute("ex-1", SimpleRequestProducer.create(request), SimpleResponseConsumer.create(), null);
+ final SimpleHttpResponse response1 = responseFuture1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+ Assertions.assertEquals(HttpStatus.SC_OK, response1.getCode());
+
+ // this should fail quickly, connection has not been released
+ final Future endpointFuture2 = connManager.lease("id2", route, null, TIMEOUT, null);
+ Assertions.assertThrows(TimeoutException.class, () -> endpointFuture2.get(10, TimeUnit.MILLISECONDS));
+ endpointFuture2.cancel(true);
+
+ // close and release the connection
+ // expect the next connection obtained to be closed
+ openEndpoint1.close();
+ connManager.release(openEndpoint1, null, TimeValue.NEG_ONE_MILLISECOND);
+
+ final Future endpointFuture3 = connManager.lease("id3", route, null, TIMEOUT, null);
+ final AsyncConnectionEndpoint endpoint2 = endpointFuture3.get(LEASE_TIMEOUT.getDuration(), LEASE_TIMEOUT.getTimeUnit());
+ Assertions.assertFalse(endpoint2.isConnected());
+
+ final Future connectFuture2 = connManager.connect(endpoint2, client.getImplementation(), TIMEOUT, null, context, null);
+ final AsyncConnectionEndpoint openEndpoint2 = connectFuture2.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+
+ final Future responseFuture2 = openEndpoint2.execute("ex-2", SimpleRequestProducer.create(request), SimpleResponseConsumer.create(), null);
+ final SimpleHttpResponse response2 = responseFuture2.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+ Assertions.assertEquals(HttpStatus.SC_OK, response2.getCode());
+
+ // release connection keeping it open
+ // expect the next connection obtained to be open
+ connManager.release(openEndpoint2, null, TimeValue.NEG_ONE_MILLISECOND);
+
+ final Future endpointFuture4 = connManager.lease("id4", route, null, TIMEOUT, null);
+ final AsyncConnectionEndpoint openEndpoint3 = endpointFuture4.get(LEASE_TIMEOUT.getDuration(), LEASE_TIMEOUT.getTimeUnit());
+ Assertions.assertTrue(openEndpoint3.isConnected());
+
+ final Future responseFuture3 = openEndpoint3.execute("ex-3", SimpleRequestProducer.create(request), SimpleResponseConsumer.create(), null);
+ final SimpleHttpResponse response3 = responseFuture3.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+ Assertions.assertEquals(HttpStatus.SC_OK, response3.getCode());
+
+ connManager.release(openEndpoint3, null, TimeValue.NEG_ONE_MILLISECOND);
+ connManager.close();
+ }
+
+ /**
+ * Tests releasing with time limits.
+ */
+ @Test
+ void testReleaseConnectionWithTimeLimits() throws Exception {
+ final HttpHost target = startServer();
+
+ final TestAsyncClient client = startClient();
+
+ final PoolingAsyncClientConnectionManager connManager = client.getConnectionManager();
+ connManager.setMaxTotal(1);
+
+ final HttpRoute route = new HttpRoute(target, null, false);
+ final HttpClientContext context = HttpClientContext.create();
+
+ final Future endpointFuture1 = connManager.lease("id1", route, null, TIMEOUT, null);
+ final AsyncConnectionEndpoint endpoint1 = endpointFuture1.get(LEASE_TIMEOUT.getDuration(), LEASE_TIMEOUT.getTimeUnit());
+
+ final Future connectFuture1 = connManager.connect(endpoint1, client.getImplementation(), TIMEOUT, null, context, null);
+ final AsyncConnectionEndpoint openEndpoint1 = connectFuture1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+
+ final SimpleHttpRequest request = SimpleRequestBuilder.get()
+ .setHttpHost(target)
+ .setPath("/")
+ .addHeader(HttpHeaders.HOST, target.toHostString())
+ .build();
+
+ final Future responseFuture1 = openEndpoint1.execute("ex-1", SimpleRequestProducer.create(request), SimpleResponseConsumer.create(), null);
+ final SimpleHttpResponse response1 = responseFuture1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+ Assertions.assertEquals(HttpStatus.SC_OK, response1.getCode());
+
+ final Future endpointFuture2 = connManager.lease("id2", route, null, TIMEOUT, null);
+ Assertions.assertThrows(TimeoutException.class, () -> endpointFuture2.get(10, TimeUnit.MILLISECONDS));
+ endpointFuture2.cancel(true);
+
+ openEndpoint1.close();
+ connManager.release(openEndpoint1, null, TimeValue.NEG_ONE_MILLISECOND);
+
+ final Future endpointFuture3 = connManager.lease("id3", route, null, TIMEOUT, null);
+ final AsyncConnectionEndpoint endpoint2 = endpointFuture3.get(LEASE_TIMEOUT.getDuration(), LEASE_TIMEOUT.getTimeUnit());
+ Assertions.assertFalse(endpoint2.isConnected());
+
+ final Future connectFuture2 = connManager.connect(endpoint2, client.getImplementation(), TIMEOUT, null, context, null);
+ final AsyncConnectionEndpoint openEndpoint2 = connectFuture2.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+
+ final Future responseFuture2 = openEndpoint2.execute("ex-2", SimpleRequestProducer.create(request), SimpleResponseConsumer.create(), null);
+ final SimpleHttpResponse response2 = responseFuture2.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+ Assertions.assertEquals(HttpStatus.SC_OK, response2.getCode());
+
+ connManager.release(openEndpoint2, null, TimeValue.ofMilliseconds(100));
+ Thread.sleep(150);
+
+ final Future endpointFuture4 = connManager.lease("id4", route, null, TIMEOUT, null);
+ final AsyncConnectionEndpoint openEndpoint3 = endpointFuture4.get(LEASE_TIMEOUT.getDuration(), LEASE_TIMEOUT.getTimeUnit());
+ Assertions.assertFalse(openEndpoint3.isConnected());
+ connManager.release(openEndpoint3, null, TimeValue.ofMilliseconds(100));
+
+ connManager.close();
+ }
+
+ @Test
+ void testCloseExpiredIdleConnections() throws Exception {
+ final HttpHost target = startServer();
+
+ final TestAsyncClient client = startClient();
+
+ final PoolingAsyncClientConnectionManager connManager = client.getConnectionManager();
+ connManager.setMaxTotal(1);
+
+ final HttpRoute route = new HttpRoute(target, null, false);
+ final HttpClientContext context = HttpClientContext.create();
+
+ final Future endpointFuture1 = connManager.lease("id1", route, null, TIMEOUT, null);
+ final AsyncConnectionEndpoint endpoint1 = endpointFuture1.get(LEASE_TIMEOUT.getDuration(), LEASE_TIMEOUT.getTimeUnit());
+
+ connManager.connect(endpoint1, client.getImplementation(), TIMEOUT, null, context, null)
+ .get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+
+ Assertions.assertEquals(1, connManager.getTotalStats().getLeased());
+ Assertions.assertEquals(1, connManager.getStats(route).getLeased());
+
+ connManager.release(endpoint1, null, TimeValue.ofMilliseconds(100));
+
+ // Released, still active.
+ Assertions.assertEquals(1, connManager.getTotalStats().getAvailable());
+ Assertions.assertEquals(1, connManager.getStats(route).getAvailable());
+
+ connManager.closeExpired();
+
+ // Time has not expired yet.
+ Assertions.assertEquals(1, connManager.getTotalStats().getAvailable());
+ Assertions.assertEquals(1, connManager.getStats(route).getAvailable());
+
+ Thread.sleep(150);
+
+ connManager.closeExpired();
+
+ // Time expired now, connections are destroyed.
+ Assertions.assertEquals(0, connManager.getTotalStats().getAvailable());
+ Assertions.assertEquals(0, connManager.getStats(route).getAvailable());
+
+ connManager.close();
+ }
+
+ @Test
+ void testCloseExpiredTTLConnections() throws Exception {
+ final HttpHost target = startServer();
+
+ final PoolingAsyncClientConnectionManager connManager = PoolingAsyncClientConnectionManagerBuilder.create()
+ .setPoolConcurrencyPolicy(PoolConcurrencyPolicy.STRICT)
+ .setConnPoolPolicy(PoolReusePolicy.LIFO)
+ .setDefaultConnectionConfig(ConnectionConfig.custom()
+ .setTimeToLive(TimeValue.ofMilliseconds(100))
+ .build())
+ .build();
+ configureClient(builder -> builder.setConnectionManager(connManager));
+
+ final TestAsyncClient client = startClient();
+
+ connManager.setMaxTotal(1);
+
+ final HttpRoute route = new HttpRoute(target, null, false);
+ final HttpClientContext context = HttpClientContext.create();
+
+ final Future endpointFuture1 = connManager.lease("id1", route, null, TIMEOUT, null);
+ final AsyncConnectionEndpoint endpoint1 = endpointFuture1.get(LEASE_TIMEOUT.getDuration(), LEASE_TIMEOUT.getTimeUnit());
+
+ connManager.connect(endpoint1, client.getImplementation(), TIMEOUT, null, context, null)
+ .get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+
+ Assertions.assertEquals(1, connManager.getTotalStats().getLeased());
+ Assertions.assertEquals(1, connManager.getStats(route).getLeased());
+ // Release, let remain idle for forever
+ connManager.release(endpoint1, null, TimeValue.NEG_ONE_MILLISECOND);
+
+ // Released, still active.
+ Assertions.assertEquals(1, connManager.getTotalStats().getAvailable());
+ Assertions.assertEquals(1, connManager.getStats(route).getAvailable());
+
+ connManager.closeExpired();
+
+ // Time has not expired yet.
+ Assertions.assertEquals(1, connManager.getTotalStats().getAvailable());
+ Assertions.assertEquals(1, connManager.getStats(route).getAvailable());
+
+ Thread.sleep(150);
+
+ connManager.closeExpired();
+
+ // TTL expired now, connections are destroyed.
+ Assertions.assertEquals(0, connManager.getTotalStats().getAvailable());
+ Assertions.assertEquals(0, connManager.getStats(route).getAvailable());
+
+ connManager.close();
+ }
+
+ @Test
+ void testConnectionTimeoutSetting() throws Exception {
+ final HttpHost target = startServer();
+
+ final TestAsyncClient client = startClient();
+
+ final Timeout connectionSocketTimeout = Timeout.ofMinutes(5);
+
+ final PoolingAsyncClientConnectionManager connManager = client.getConnectionManager();
+ connManager.setMaxTotal(1);
+ connManager.setDefaultConnectionConfig(ConnectionConfig.custom()
+ .setSocketTimeout(connectionSocketTimeout)
+ .build());
+
+ final HttpRoute route = new HttpRoute(target, null, false);
+
+ final SimpleHttpRequest request = SimpleRequestBuilder.get()
+ .setHttpHost(target)
+ .setPath("/")
+ .addHeader(HttpHeaders.HOST, target.toHostString())
+ .build();
+ final HttpClientContext context = HttpClientContext.create();
+
+ final Future endpointFuture1 = connManager.lease("id1", route, null, TIMEOUT, null);
+ final AsyncConnectionEndpoint endpoint1 = endpointFuture1.get(LEASE_TIMEOUT.getDuration(), LEASE_TIMEOUT.getTimeUnit());
+
+ final Future connectFuture1 = connManager.connect(endpoint1, client.getImplementation(), TIMEOUT, null, context, null);
+ final AsyncConnectionEndpoint openEndpoint1 = connectFuture1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+
+ // Modify socket timeout of the endpoint
+ endpoint1.setSocketTimeout(Timeout.ofSeconds(30));
+
+ final Future responseFuture1 = openEndpoint1.execute("ex-1", SimpleRequestProducer.create(request), SimpleResponseConsumer.create(), null);
+ final SimpleHttpResponse response1 = responseFuture1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+ Assertions.assertEquals(HttpStatus.SC_OK, response1.getCode());
+
+ connManager.release(endpoint1, null, TimeValue.NEG_ONE_MILLISECOND);
+
+ final Future endpointFuture2 = connManager.lease("id2", route, null, TIMEOUT, null);
+ final AsyncConnectionEndpoint endpoint2 = endpointFuture2.get(LEASE_TIMEOUT.getDuration(), LEASE_TIMEOUT.getTimeUnit());
+ Assertions.assertTrue(endpoint2.isConnected());
+
+ final HttpConnection connection = ((ConnectionHolder) endpoint2).get();
+ Assertions.assertEquals(connectionSocketTimeout, connection.getSocketTimeout());
+
+ connManager.close();
+ }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/MinimalTestClientBuilder.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/MinimalTestClientBuilder.java
index 5c8b1c0a9f..e99a78c679 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/MinimalTestClientBuilder.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/MinimalTestClientBuilder.java
@@ -31,8 +31,8 @@
import org.apache.hc.client5.http.config.TlsConfig;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
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.http.nio.AsyncClientConnectionManager;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.testing.SSLTestContexts;
import org.apache.hc.core5.http.config.Http1Config;
@@ -45,6 +45,7 @@ final class MinimalTestClientBuilder implements TestAsyncClientBuilder {
private final PoolingAsyncClientConnectionManagerBuilder connectionManagerBuilder;
+ private AsyncClientConnectionManager connectionManager;
private Timeout timeout;
private TlsStrategy tlsStrategy;
private Http1Config http1Config;
@@ -65,6 +66,12 @@ public TestAsyncClientBuilder setTimeout(final Timeout timeout) {
return this;
}
+ @Override
+ public TestAsyncClientBuilder setConnectionManager(final AsyncClientConnectionManager connectionManager) {
+ this.connectionManager = connectionManager;
+ return this;
+ }
+
@Override
public TestAsyncClientBuilder setTlsStrategy(final TlsStrategy tlsStrategy) {
this.tlsStrategy = tlsStrategy;
@@ -97,21 +104,21 @@ public TestAsyncClientBuilder setH2Config(final H2Config h2Config) {
@Override
public TestAsyncClient build() throws Exception {
- final PoolingAsyncClientConnectionManager connectionManager = connectionManagerBuilder
+ final AsyncClientConnectionManager connectionManagerCopy = connectionManager == null ? connectionManagerBuilder
.setTlsStrategy(tlsStrategy != null ? tlsStrategy : new DefaultClientTlsStrategy(SSLTestContexts.createClientSSLContext()))
.setDefaultConnectionConfig(ConnectionConfig.custom()
.setSocketTimeout(timeout)
.setConnectTimeout(timeout)
.build())
- .build();
+ .build() : connectionManager;
final CloseableHttpAsyncClient client = HttpAsyncClients.createMinimal(
- h2Config,
- http1Config,
- IOReactorConfig.custom()
- .setSoTimeout(timeout)
- .build(),
- connectionManager);
- return new TestAsyncClient(client, connectionManager);
+ h2Config,
+ http1Config,
+ IOReactorConfig.custom()
+ .setSoTimeout(timeout)
+ .build(),
+ connectionManagerCopy);
+ return new TestAsyncClient(client, connectionManagerCopy);
}
}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java
index 1df164a8c6..11c291686a 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java
@@ -38,8 +38,8 @@
import org.apache.hc.client5.http.config.TlsConfig;
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.nio.PoolingAsyncClientConnectionManager;
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionManager;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.testing.SSLTestContexts;
import org.apache.hc.core5.http.Header;
@@ -57,6 +57,7 @@ final class StandardTestClientBuilder implements TestAsyncClientBuilder {
private final PoolingAsyncClientConnectionManagerBuilder connectionManagerBuilder;
private final HttpAsyncClientBuilder clientBuilder;
+ private AsyncClientConnectionManager connectionManager;
private Timeout timeout;
private TlsStrategy tlsStrategy;
@@ -76,6 +77,12 @@ public TestAsyncClientBuilder setTimeout(final Timeout timeout) {
return this;
}
+ @Override
+ public TestAsyncClientBuilder setConnectionManager(final AsyncClientConnectionManager connectionManager) {
+ this.connectionManager = connectionManager;
+ return this;
+ }
+
@Override
public TestAsyncClientBuilder addResponseInterceptorFirst(final HttpResponseInterceptor interceptor) {
this.clientBuilder.addResponseInterceptorFirst(interceptor);
@@ -168,20 +175,20 @@ public TestAsyncClientBuilder setDefaultCredentialsProvider(final CredentialsPro
@Override
public TestAsyncClient build() throws Exception {
- final PoolingAsyncClientConnectionManager connectionManager = connectionManagerBuilder
+ final AsyncClientConnectionManager connectionManagerCopy = connectionManager == null ? connectionManagerBuilder
.setTlsStrategy(tlsStrategy != null ? tlsStrategy : new DefaultClientTlsStrategy(SSLTestContexts.createClientSSLContext()))
.setDefaultConnectionConfig(ConnectionConfig.custom()
.setSocketTimeout(timeout)
.setConnectTimeout(timeout)
.build())
- .build();
+ .build() : connectionManager;
final CloseableHttpAsyncClient client = clientBuilder
.setIOReactorConfig(IOReactorConfig.custom()
.setSoTimeout(timeout)
.build())
- .setConnectionManager(connectionManager)
+ .setConnectionManager(connectionManagerCopy)
.build();
- return new TestAsyncClient(client, connectionManager);
+ return new TestAsyncClient(client, connectionManagerCopy);
}
}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncClient.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncClient.java
index adefc45602..30563ebf2d 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncClient.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncClient.java
@@ -31,7 +31,7 @@
import java.util.concurrent.Future;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
-import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionManager;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.function.Supplier;
import org.apache.hc.core5.http.HttpHost;
@@ -43,16 +43,15 @@
import org.apache.hc.core5.io.CloseMode;
import org.apache.hc.core5.reactor.IOReactorStatus;
import org.apache.hc.core5.util.Args;
-import org.apache.hc.core5.util.Asserts;
import org.apache.hc.core5.util.TimeValue;
public class TestAsyncClient extends CloseableHttpAsyncClient {
private final CloseableHttpAsyncClient client;
- private final PoolingAsyncClientConnectionManager connectionManager;
+ private final AsyncClientConnectionManager connectionManager;
public TestAsyncClient(final CloseableHttpAsyncClient client,
- final PoolingAsyncClientConnectionManager connectionManager) {
+ final AsyncClientConnectionManager connectionManager) {
this.client = Args.notNull(client, "Client");
this.connectionManager = connectionManager;
}
@@ -113,9 +112,9 @@ public T getImplementation() {
return (T) client;
}
- public PoolingAsyncClientConnectionManager getConnectionManager() {
- Asserts.check(connectionManager != null, "Connection manager is not available");
- return connectionManager;
+ @SuppressWarnings("unchecked")
+ public T getConnectionManager() {
+ return (T) connectionManager;
}
}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncClientBuilder.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncClientBuilder.java
index 3f12b77cf0..cbb2878eff 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncClientBuilder.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncClientBuilder.java
@@ -35,6 +35,7 @@
import org.apache.hc.client5.http.auth.AuthSchemeFactory;
import org.apache.hc.client5.http.auth.CredentialsProvider;
import org.apache.hc.client5.http.config.TlsConfig;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionManager;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpRequestInterceptor;
import org.apache.hc.core5.http.HttpResponseInterceptor;
@@ -50,8 +51,12 @@ public interface TestAsyncClientBuilder {
TestAsyncClientBuilder setTimeout(Timeout soTimeout);
- default TestAsyncClientBuilder addResponseInterceptorFirst(final HttpResponseInterceptor interceptor) {
- return this;
+ default TestAsyncClientBuilder setConnectionManager(AsyncClientConnectionManager connManager) {
+ throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
+ }
+
+ default TestAsyncClientBuilder addResponseInterceptorFirst(HttpResponseInterceptor interceptor) {
+ throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
}
default TestAsyncClientBuilder addResponseInterceptorLast(HttpResponseInterceptor interceptor) {
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/sync/TestClientBuilder.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/sync/TestClientBuilder.java
index 459124693f..57526ef2c1 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/sync/TestClientBuilder.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/sync/TestClientBuilder.java
@@ -51,8 +51,8 @@ public interface TestClientBuilder {
TestClientBuilder setConnectionManager(HttpClientConnectionManager connManager);
- default TestClientBuilder addResponseInterceptorFirst(final HttpResponseInterceptor interceptor) {
- return this;
+ default TestClientBuilder addResponseInterceptorFirst(HttpResponseInterceptor interceptor) {
+ throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
}
default TestClientBuilder addResponseInterceptorLast(HttpResponseInterceptor interceptor) {
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/HttpIntegrationTests.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/HttpIntegrationTests.java
index f162795f67..9b0b191f77 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/HttpIntegrationTests.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/HttpIntegrationTests.java
@@ -112,4 +112,24 @@ public RedirectsTls() {
}
+ @Nested
+ @DisplayName("Request re-execution (HTTP/1.1)")
+ class RequestReExecution extends TestClientRequestReExecution {
+
+ public RequestReExecution() {
+ super(URIScheme.HTTP);
+ }
+
+ }
+
+ @Nested
+ @DisplayName("Request re-execution (HTTP/1.1)")
+ class RequestReExecutionTls extends TestClientRequestReExecution {
+
+ public RequestReExecutionTls() {
+ super(URIScheme.HTTPS);
+ }
+
+ }
+
}
\ No newline at end of file
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientRequestReExecution.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientRequestReExecution.java
new file mode 100644
index 0000000000..30b36b82d4
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientRequestReExecution.java
@@ -0,0 +1,118 @@
+/*
+ * ====================================================================
+ * 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.sync;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.testing.classic.RandomHandler;
+import org.apache.hc.client5.testing.classic.ServiceUnavailableDecorator;
+import org.apache.hc.client5.testing.extension.sync.ClientProtocolLevel;
+import org.apache.hc.client5.testing.extension.sync.TestClient;
+import org.apache.hc.core5.function.Resolver;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
+import org.apache.hc.core5.util.TimeValue;
+import org.hamcrest.CoreMatchers;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+abstract class TestClientRequestReExecution extends AbstractIntegrationTestBase {
+
+ public TestClientRequestReExecution(final URIScheme scheme) {
+ super(scheme, ClientProtocolLevel.STANDARD);
+ }
+
+ @BeforeEach
+ void setup() {
+ final Resolver serviceAvailabilityResolver = new Resolver() {
+
+ private final AtomicInteger count = new AtomicInteger(0);
+
+ @Override
+ public TimeValue resolve(final HttpRequest request) {
+ final int n = count.incrementAndGet();
+ return n <= 3 ? TimeValue.ofSeconds(1) : null;
+ }
+
+ };
+
+ configureServer(bootstrap -> bootstrap.setExchangeHandlerDecorator(handler
+ -> new ServiceUnavailableDecorator(handler, serviceAvailabilityResolver)));
+ }
+
+ @Test
+ void testGiveUpAfterOneRetry() throws Exception {
+ configureServer(bootstrap -> bootstrap.register("/random/*", new RandomHandler()));
+ final HttpHost target = startServer();
+
+ configureClient(builder -> builder
+ .setRetryStrategy(new DefaultHttpRequestRetryStrategy(1, TimeValue.ofSeconds(1))));
+ final TestClient client = client();
+
+ final HttpClientContext context = HttpClientContext.create();
+ final ClassicHttpRequest request = ClassicRequestBuilder.get()
+ .setHttpHost(target)
+ .setPath("/random/2048")
+ .build();
+ final HttpResponse response = client.execute(target, request, context, r -> {
+ EntityUtils.consume(r.getEntity());
+ return r;
+ });
+ assertThat(response.getCode(), CoreMatchers.equalTo(HttpStatus.SC_SERVICE_UNAVAILABLE));
+ }
+
+ @Test
+ void testDoNotGiveUpEasily() throws Exception {
+ configureServer(bootstrap -> bootstrap.register("/random/*", new RandomHandler()));
+ final HttpHost target = startServer();
+
+ configureClient(builder -> builder
+ .setRetryStrategy(new DefaultHttpRequestRetryStrategy(5, TimeValue.ofSeconds(1))));
+ final TestClient client = client();
+
+ final HttpClientContext context = HttpClientContext.create();
+ final ClassicHttpRequest request = ClassicRequestBuilder.get()
+ .setHttpHost(target)
+ .setPath("/random/2048")
+ .build();
+ final HttpResponse response = client.execute(target, request, context, r -> {
+ EntityUtils.consume(r.getEntity());
+ return r;
+ });
+ assertThat(response.getCode(), CoreMatchers.equalTo(HttpStatus.SC_OK));
+ }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestConnectionManagement.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestConnectionManagement.java
index 8c215b2a88..9380ad25a3 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestConnectionManagement.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestConnectionManagement.java
@@ -32,6 +32,7 @@
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.impl.ConnectionHolder;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.io.ConnectionEndpoint;
@@ -42,6 +43,7 @@
import org.apache.hc.client5.testing.extension.sync.TestClient;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpConnection;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStatus;
@@ -129,6 +131,8 @@ void testReleaseConnection() throws Exception {
final LeaseRequest leaseRequest2 = connManager.lease("id2", route, null);
Assertions.assertThrows(TimeoutException.class, () -> leaseRequest2.get(Timeout.ofMilliseconds(10)));
+ // close and release the connection
+ // expect the next connection obtained to be closed
endpoint1.close();
connManager.release(endpoint1, null, TimeValue.NEG_ONE_MILLISECOND);
final LeaseRequest leaseRequest3 = connManager.lease("id2", route, null);
@@ -141,7 +145,7 @@ void testReleaseConnection() throws Exception {
Assertions.assertEquals(HttpStatus.SC_OK, response2.getCode());
}
- // release connection after marking it for re-use
+ // release connection keeping it open
// expect the next connection obtained to be open
connManager.release(endpoint2, null, TimeValue.NEG_ONE_MILLISECOND);
@@ -324,4 +328,50 @@ void testCloseExpiredTTLConnections() throws Exception {
connManager.close();
}
+ @Test
+ void testConnectionTimeoutSetting() throws Exception {
+ configureServer(bootstrap -> bootstrap
+ .register("/random/*", new RandomHandler()));
+ final HttpHost target = startServer();
+
+ final Timeout connectionSocketTimeout = Timeout.ofMinutes(5);
+
+ final TestClient client = client();
+ final PoolingHttpClientConnectionManager connManager = client.getConnectionManager();
+ connManager.setMaxTotal(1);
+ connManager.setDefaultConnectionConfig(ConnectionConfig.custom()
+ .setSocketTimeout(connectionSocketTimeout)
+ .build());
+
+ final HttpRoute route = new HttpRoute(target, null, false);
+ final int rsplen = 8;
+ final String uri = "/random/" + rsplen;
+
+ final ClassicHttpRequest request = new BasicClassicHttpRequest("GET", target, uri);
+ final HttpClientContext context = HttpClientContext.create();
+
+ final LeaseRequest leaseRequest1 = connManager.lease("id1", route, null);
+ final ConnectionEndpoint endpoint1 = leaseRequest1.get(Timeout.ZERO_MILLISECONDS);
+
+ connManager.connect(endpoint1, null, context);
+
+ // Modify socket timeout of the endpoint
+ endpoint1.setSocketTimeout(Timeout.ofSeconds(30));
+
+ try (final ClassicHttpResponse response1 = endpoint1.execute("id1", request, exec, context)) {
+ Assertions.assertEquals(HttpStatus.SC_OK, response1.getCode());
+ }
+
+ connManager.release(endpoint1, null, TimeValue.NEG_ONE_MILLISECOND);
+
+ final LeaseRequest leaseRequest2 = connManager.lease("id2", route, null);
+ final ConnectionEndpoint endpoint2 = leaseRequest2.get(Timeout.ZERO_MILLISECONDS);
+ Assertions.assertTrue(endpoint2.isConnected());
+
+ final HttpConnection connection = ((ConnectionHolder) endpoint2).get();
+ Assertions.assertEquals(connectionSocketTimeout, connection.getSocketTimeout());
+
+ connManager.close();
+ }
+
}
diff --git a/httpclient5/pom.xml b/httpclient5/pom.xml
index 2cd51e18f2..c175c761b6 100644
--- a/httpclient5/pom.xml
+++ b/httpclient5/pom.xml
@@ -28,7 +28,7 @@
org.apache.httpcomponents.client5
httpclient5-parent
- 5.5.1-SNAPSHOT
+ 5.5.3-SNAPSHOT
httpclient5
Apache HttpClient
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/config/ConnectionConfig.java b/httpclient5/src/main/java/org/apache/hc/client5/http/config/ConnectionConfig.java
index b372d461aa..4be79544a8 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/config/ConnectionConfig.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/config/ConnectionConfig.java
@@ -149,7 +149,15 @@ public Builder setSocketTimeout(final int soTimeout, final TimeUnit timeUnit) {
}
/**
- * Determines the default socket timeout value for I/O operations.
+ * Determines the default socket timeout value for I/O operations on
+ * connections created by this configuration.
+ * A timeout value of zero is interpreted as an infinite timeout.
+ *
+ * This value acts as a baseline at the connection management layer.
+ * This parameter overrides the socket timeout setting applied at the I/O layer
+ * and in its tuen can overridden by settings applied at the protocol layer
+ * for the duration of a message exchange.
+ *
*
* Default: {@code null} (undefined)
*
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/config/RequestConfig.java b/httpclient5/src/main/java/org/apache/hc/client5/http/config/RequestConfig.java
index d0d70fb482..e5d791c56e 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/config/RequestConfig.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/config/RequestConfig.java
@@ -517,6 +517,16 @@ public Builder setConnectTimeout(final long connectTimeout, final TimeUnit timeU
* HTTP transports with message multiplexing.
*
*
+ * This parameter overrides the socket timeout setting applied at the connection
+ * management or I/O layers for the duration of a message exchange.
+ *
+ *
+ * Please note that response timeout is not a deadline. Its absolute value
+ * can be exceeded, for example, in case of automatic request re-execution.
+ * Please make sure the automatic request re-execution policy has been
+ * configured appropriately.
+ *
+ *
* Default: {@code null}
*
*
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ConnectionHolder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ConnectionHolder.java
new file mode 100644
index 0000000000..33a1d3d04f
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ConnectionHolder.java
@@ -0,0 +1,41 @@
+/*
+ * ====================================================================
+ * 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;
+
+import org.apache.hc.core5.annotation.Internal;
+import org.apache.hc.core5.http.HttpConnection;
+
+/**
+ * Internal interface to expose the underlying connection
+ * @since 5.5
+ */
+@Internal
+public interface ConnectionHolder {
+
+ HttpConnection get();
+
+}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultHttpRequestRetryStrategy.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultHttpRequestRetryStrategy.java
index b8dfcb535b..47c9bab567 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultHttpRequestRetryStrategy.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultHttpRequestRetryStrategy.java
@@ -41,6 +41,8 @@
import javax.net.ssl.SSLException;
import org.apache.hc.client5.http.HttpRequestRetryStrategy;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
@@ -55,6 +57,7 @@
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.util.Args;
import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
/**
* Default implementation of the {@link HttpRequestRetryStrategy} interface.
@@ -95,7 +98,8 @@ protected DefaultHttpRequestRetryStrategy(
final Collection> clazzes,
final Collection codes) {
Args.notNegative(maxRetries, "maxRetries");
- Args.notNegative(defaultRetryInterval.getDuration(), "defaultRetryInterval");
+ Args.notNull(defaultRetryInterval, "defaultRetryInterval");
+ Args.check(TimeValue.isNonNegative(defaultRetryInterval), "Default retry interval is negative");
this.maxRetries = maxRetries;
this.defaultRetryInterval = defaultRetryInterval;
this.nonRetriableIOExceptionClasses = new HashSet<>(clazzes);
@@ -199,6 +203,14 @@ public boolean retryRequest(
final HttpContext context) {
Args.notNull(response, "response");
+ if (context != null) {
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final RequestConfig requestConfig = clientContext.getRequestConfigOrDefault();
+ final Timeout responseTimeout = requestConfig.getResponseTimeout();
+ if (responseTimeout != null && defaultRetryInterval.compareTo(responseTimeout) > 0) {
+ return false;
+ }
+ }
return execCount <= this.maxRetries && retriableCodes.contains(response.getCode());
}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultRedirectStrategy.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultRedirectStrategy.java
index dbfaed4c44..e794c7b508 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultRedirectStrategy.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultRedirectStrategy.java
@@ -32,9 +32,11 @@
import java.util.Iterator;
import java.util.Locale;
+import org.apache.hc.client5.http.SchemePortResolver;
import org.apache.hc.client5.http.protocol.RedirectStrategy;
import org.apache.hc.client5.http.utils.URIUtils;
import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.Internal;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpException;
@@ -56,18 +58,31 @@
@Contract(threading = ThreadingBehavior.STATELESS)
public class DefaultRedirectStrategy implements RedirectStrategy {
+ private final SchemePortResolver schemePortResolver;
+
/**
* Default instance of {@link DefaultRedirectStrategy}.
*/
public static final DefaultRedirectStrategy INSTANCE = new DefaultRedirectStrategy();
+ @Internal
+ public DefaultRedirectStrategy(final SchemePortResolver schemePortResolver) {
+ this.schemePortResolver = schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE;
+ }
+
+ public DefaultRedirectStrategy() {
+ this(null);
+ }
+
@Override
public boolean isRedirectAllowed(
final HttpHost currentTarget,
final HttpHost newTarget,
final HttpRequest redirect,
final HttpContext context) {
- if (!currentTarget.equals(newTarget)) {
+
+ // If authority (host + effective port) differs, strip sensitive headers
+ if (!isSameAuthority(currentTarget, newTarget)) {
for (final Iterator it = redirect.headerIterator(); it.hasNext(); ) {
final Header header = it.next();
if (header.isSensitive()
@@ -80,6 +95,20 @@ public boolean isRedirectAllowed(
return true;
}
+ private boolean isSameAuthority(final HttpHost h1, final HttpHost h2) {
+ if (h1 == null || h2 == null) {
+ return false;
+ }
+ final String host1 = h1.getHostName();
+ final String host2 = h2.getHostName();
+ if (!host1.equalsIgnoreCase(host2)) {
+ return false;
+ }
+ final int port1 = schemePortResolver.resolve(h1);
+ final int port2 = schemePortResolver.resolve(h2);
+ return port1 == port2;
+ }
+
@Override
public boolean isRedirected(
final HttpRequest request,
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AbstractHttpAsyncClientBase.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AbstractHttpAsyncClientBase.java
index 264faa2c70..275b82b1c9 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AbstractHttpAsyncClientBase.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AbstractHttpAsyncClientBase.java
@@ -26,22 +26,29 @@
*/
package org.apache.hc.client5.http.impl.async;
+import java.net.SocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
+import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.function.Supplier;
import org.apache.hc.core5.http.nio.AsyncPushConsumer;
import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.net.NamedEndpoint;
import org.apache.hc.core5.reactor.ConnectionInitiator;
import org.apache.hc.core5.reactor.DefaultConnectingIOReactor;
import org.apache.hc.core5.reactor.IOReactorStatus;
+import org.apache.hc.core5.reactor.IOSession;
import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-abstract class AbstractHttpAsyncClientBase extends CloseableHttpAsyncClient {
+abstract class AbstractHttpAsyncClientBase extends CloseableHttpAsyncClient implements ConnectionInitiator {
enum Status { READY, RUNNING, TERMINATED }
@@ -84,6 +91,17 @@ boolean isRunning() {
return status.get() == Status.RUNNING;
}
+ @Override
+ public Future connect(
+ final NamedEndpoint remoteEndpoint,
+ final SocketAddress remoteAddress,
+ final SocketAddress localAddress,
+ final Timeout timeout,
+ final Object attachment,
+ final FutureCallback callback) {
+ return ioReactor.connect(remoteEndpoint, remoteAddress, localAddress, timeout, attachment, callback);
+ }
+
ConnectionInitiator getConnectionInitiator() {
return ioReactor;
}
@@ -116,7 +134,18 @@ public final void close(final CloseMode closeMode) {
}
ioReactor.initiateShutdown();
ioReactor.close(closeMode);
- executorService.shutdownNow();
+ if (closeMode == CloseMode.GRACEFUL) {
+ executorService.shutdown();
+ try {
+ if (!executorService.awaitTermination(1, TimeUnit.SECONDS)) {
+ executorService.shutdownNow();
+ }
+ } catch (final InterruptedException ignore) {
+ Thread.currentThread().interrupt();
+ }
+ } else {
+ executorService.shutdownNow();
+ }
internalClose(closeMode);
}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncHttpRequestRetryExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncHttpRequestRetryExec.java
index 292b6ed070..325567cd6e 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncHttpRequestRetryExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncHttpRequestRetryExec.java
@@ -29,7 +29,6 @@
import java.io.IOException;
import org.apache.hc.client5.http.HttpRequestRetryStrategy;
-import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.async.AsyncExecCallback;
import org.apache.hc.client5.http.async.AsyncExecChain;
import org.apache.hc.client5.http.async.AsyncExecChainHandler;
@@ -40,6 +39,7 @@
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.EntityDetails;
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.HttpResponse;
import org.apache.hc.core5.http.nio.AsyncDataConsumer;
@@ -96,6 +96,7 @@ public AsyncHttpRequestRetryExec(final HttpRequestRetryStrategy retryStrategy) {
private static class State {
volatile boolean retrying;
+ volatile int status;
volatile TimeValue delay;
}
@@ -125,10 +126,8 @@ public AsyncDataConsumer handleResponse(
}
state.retrying = retryStrategy.retryRequest(response, scope.execCount.get(), clientContext);
if (state.retrying) {
+ state.status = response.getCode();
state.delay = retryStrategy.getRetryInterval(response, scope.execCount.get(), clientContext);
- if (LOG.isDebugEnabled()) {
- LOG.debug("{} retrying request in {}", exchangeId, state.delay);
- }
return new DiscardingEntityConsumer<>();
}
return asyncExecCallback.handleResponse(response, entityDetails);
@@ -142,17 +141,24 @@ public void handleInformationResponse(final HttpResponse response) throws HttpEx
@Override
public void completed() {
if (state.retrying) {
- scope.execCount.incrementAndGet();
+ final int execCount = scope.execCount.incrementAndGet();
if (entityProducer != null) {
entityProducer.releaseResources();
}
+ final HttpHost target = scope.route.getTargetHost();
+ final TimeValue delay = TimeValue.isPositive(state.delay) ? state.delay : TimeValue.ZERO_MILLISECONDS;
+ if (LOG.isInfoEnabled()) {
+ LOG.info("{} {} responded with status {}; " +
+ "request will be automatically re-executed in {} (exec count {})",
+ exchangeId, target, state.status, delay, execCount);
+ }
scope.scheduler.scheduleExecution(
request,
entityProducer,
scope,
(r, e, s, c) -> execute(r, e, s, chain, c),
asyncExecCallback,
- state.delay);
+ delay);
} else {
asyncExecCallback.completed();
}
@@ -161,7 +167,7 @@ public void completed() {
@Override
public void failed(final Exception cause) {
if (cause instanceof IOException) {
- final HttpRoute route = scope.route;
+ final HttpHost target = scope.route.getTargetHost();
final HttpClientContext clientContext = scope.clientContext;
if (entityProducer != null && !entityProducer.isRepeatable()) {
if (LOG.isDebugEnabled()) {
@@ -171,10 +177,6 @@ public void failed(final Exception cause) {
if (LOG.isDebugEnabled()) {
LOG.debug("{} {}", exchangeId, cause.getMessage(), cause);
}
- if (LOG.isInfoEnabled()) {
- LOG.info("Recoverable I/O exception ({}) caught when processing request to {}",
- cause.getClass().getName(), route);
- }
scope.execRuntime.discardEndpoint();
if (entityProducer != null) {
entityProducer.releaseResources();
@@ -182,13 +184,19 @@ public void failed(final Exception cause) {
state.retrying = true;
final int execCount = scope.execCount.incrementAndGet();
state.delay = retryStrategy.getRetryInterval(request, (IOException) cause, execCount - 1, clientContext);
+ final TimeValue delay = TimeValue.isPositive(state.delay) ? state.delay : TimeValue.ZERO_MILLISECONDS;
+ if (LOG.isInfoEnabled()) {
+ LOG.info("{} recoverable I/O exception ({}) caught when sending request to {};" +
+ "request will be automatically re-executed in {} (exec count {})",
+ exchangeId, cause.getClass().getName(), target, delay, execCount);
+ }
scope.scheduler.scheduleExecution(
request,
entityProducer,
scope,
(r, e, s, c) -> execute(r, e, s, chain, c),
asyncExecCallback,
- state.delay);
+ delay);
return;
}
}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java
index 0d8c504627..1b35f8cb34 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java
@@ -334,26 +334,25 @@ private boolean needAuthentication(
}
}
+ boolean targetNeedsAuth = false;
+ boolean proxyNeedsAuth = false;
if (targetAuthRequested || targetMutualAuthRequired) {
- final boolean updated = authenticator.handleResponse(target, ChallengeType.TARGET, response,
+ targetNeedsAuth = authenticator.handleResponse(target, ChallengeType.TARGET, response,
targetAuthStrategy, targetAuthExchange, context);
if (authCacheKeeper != null) {
authCacheKeeper.updateOnResponse(target, pathPrefix, targetAuthExchange, context);
}
-
- return updated;
}
if (proxyAuthRequested || proxyMutualAuthRequired) {
- final boolean updated = authenticator.handleResponse(proxy, ChallengeType.PROXY, response,
+ proxyNeedsAuth = authenticator.handleResponse(proxy, ChallengeType.PROXY, response,
proxyAuthStrategy, proxyAuthExchange, context);
if (authCacheKeeper != null) {
authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context);
}
-
- return updated;
}
+ return targetNeedsAuth || proxyNeedsAuth;
}
return false;
}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java
index 1e85bc955c..56de4b0401 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java
@@ -836,7 +836,7 @@ public CloseableHttpAsyncClient build() {
if (!redirectHandlingDisabled) {
RedirectStrategy redirectStrategyCopy = this.redirectStrategy;
if (redirectStrategyCopy == null) {
- redirectStrategyCopy = DefaultRedirectStrategy.INSTANCE;
+ redirectStrategyCopy = schemePortResolver != null ? new DefaultRedirectStrategy(schemePortResolver) : DefaultRedirectStrategy.INSTANCE;
}
execChainDefinition.addFirst(
new AsyncRedirectExec(routePlannerCopy, redirectStrategyCopy),
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java
index 933b8ae781..4f56b46804 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java
@@ -1023,7 +1023,7 @@ public CloseableHttpAsyncClient build() {
if (!redirectHandlingDisabled) {
RedirectStrategy redirectStrategyCopy = this.redirectStrategy;
if (redirectStrategyCopy == null) {
- redirectStrategyCopy = DefaultRedirectStrategy.INSTANCE;
+ redirectStrategyCopy = schemePortResolver != null ? new DefaultRedirectStrategy(schemePortResolver) : DefaultRedirectStrategy.INSTANCE;
}
execChainDefinition.addFirst(
new AsyncRedirectExec(routePlannerCopy, redirectStrategyCopy),
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthenticationHandler.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthenticationHandler.java
index 8d38ec8e75..fc7e4a22dd 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthenticationHandler.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthenticationHandler.java
@@ -121,8 +121,8 @@ public boolean isChallenged(
}
/**
- * Determines whether the given response represents an authentication challenge, without
- * changing the {@link AuthExchange} state.
+ * Determines whether the given response represents an authentication challenge
+ * of challangeType, without changing the {@link AuthExchange} state.
*
* @param challengeType the challenge type (target or proxy).
* @param response the response message head.
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java
index 0771f9bd3b..7f57e2f78a 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java
@@ -471,7 +471,6 @@ private String createDigestResponse(final HttpRequest request) throws Authentica
params.add(new BasicNameValuePair("qop", qop == QualityOfProtection.AUTH_INT ? "auth-int" : "auth"));
params.add(new BasicNameValuePair("nc", nc));
params.add(new BasicNameValuePair("cnonce", cnonce));
- params.add(new BasicNameValuePair("rspauth", hasha2));
}
if (algorithm != null) {
params.add(new BasicNameValuePair("algorithm", algorithm));
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java
index acb62b2ccf..0d88979479 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java
@@ -54,6 +54,7 @@
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.ProtocolException;
import org.apache.hc.core5.http.config.Lookup;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.http.message.BasicHeaderValueParser;
@@ -77,25 +78,32 @@
@Internal
public final class ContentCompressionExec implements ExecChainHandler {
+ public static final int MAX_CODEC_LIST_LEN = 5;
+
private final Header acceptEncoding;
private final Lookup decoderRegistry;
private final boolean ignoreUnknown;
+ private final int maxCodecListLen;
public ContentCompressionExec(
final List acceptEncoding,
final Lookup decoderRegistry,
- final boolean ignoreUnknown) {
+ final boolean ignoreUnknown,
+ final int maxCodecListLen) {
- final boolean brotliSupported = BrotliDecompressingEntity.isAvailable();
- final List encodings = new ArrayList<>(4);
- encodings.add("gzip");
- encodings.add("x-gzip");
- encodings.add("deflate");
- if (brotliSupported) {
- encodings.add("br");
+ final boolean brotliSupported = decoderRegistry == null && BrotliDecompressingEntity.isAvailable();
+ if (acceptEncoding != null) {
+ this.acceptEncoding = MessageSupport.headerOfTokens(HttpHeaders.ACCEPT_ENCODING, acceptEncoding);
+ } else {
+ final List encodings = new ArrayList<>(4);
+ encodings.add("gzip");
+ encodings.add("x-gzip");
+ encodings.add("deflate");
+ if (brotliSupported) {
+ encodings.add("br");
+ }
+ this.acceptEncoding = MessageSupport.headerOfTokens(HttpHeaders.ACCEPT_ENCODING, encodings);
}
- this.acceptEncoding = MessageSupport.headerOfTokens(HttpHeaders.ACCEPT_ENCODING, encodings);
-
if (decoderRegistry != null) {
this.decoderRegistry = decoderRegistry;
} else {
@@ -108,13 +116,23 @@ public ContentCompressionExec(
}
this.decoderRegistry = builder.build();
}
-
-
this.ignoreUnknown = ignoreUnknown;
+ this.maxCodecListLen = maxCodecListLen;
+ }
+
+ public ContentCompressionExec(
+ final List acceptEncoding,
+ final Lookup decoderRegistry,
+ final boolean ignoreUnknown) {
+ this(acceptEncoding, decoderRegistry, ignoreUnknown, MAX_CODEC_LIST_LEN);
}
public ContentCompressionExec(final boolean ignoreUnknown) {
- this(null, null, ignoreUnknown);
+ this(null, null, ignoreUnknown, MAX_CODEC_LIST_LEN);
+ }
+
+ public ContentCompressionExec(final int maxCodecListLen) {
+ this(null, null, true, maxCodecListLen);
}
/**
@@ -127,7 +145,7 @@ public ContentCompressionExec(final boolean ignoreUnknown) {
*
*/
public ContentCompressionExec() {
- this(null, null, true);
+ this(null, null, true, MAX_CODEC_LIST_LEN);
}
@@ -157,6 +175,9 @@ public ClassicHttpResponse execute(
if (contentEncoding != null) {
final ParserCursor cursor = new ParserCursor(0, contentEncoding.length());
final HeaderElement[] codecs = BasicHeaderValueParser.INSTANCE.parseElements(contentEncoding, cursor);
+ if (maxCodecListLen > 0 && codecs.length > maxCodecListLen) {
+ throw new ProtocolException("Codec list exceeds maximum of " + maxCodecListLen + " elements");
+ }
for (final HeaderElement codec : codecs) {
final String codecname = codec.getName().toLowerCase(Locale.ROOT);
final InputStreamFactory decoderFactory = decoderRegistry.lookup(codecname);
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java
index 4ba68b9192..3f871ff902 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java
@@ -1011,7 +1011,7 @@ public CloseableHttpClient build() {
if (!redirectHandlingDisabled) {
RedirectStrategy redirectStrategyCopy = this.redirectStrategy;
if (redirectStrategyCopy == null) {
- redirectStrategyCopy = DefaultRedirectStrategy.INSTANCE;
+ redirectStrategyCopy = schemePortResolver != null ? new DefaultRedirectStrategy(schemePortResolver) : DefaultRedirectStrategy.INSTANCE;
}
execChainDefinition.addFirst(
new RedirectExec(routePlannerCopy, redirectStrategyCopy),
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpRequestRetryExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpRequestRetryExec.java
index 2ab32b6644..3dc801dd36 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpRequestRetryExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpRequestRetryExec.java
@@ -34,7 +34,6 @@
import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecChain.Scope;
import org.apache.hc.client5.http.classic.ExecChainHandler;
-import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.ChainElement;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.annotation.Contract;
@@ -44,11 +43,11 @@
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.NoHttpResponseException;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.util.Args;
import org.apache.hc.core5.util.TimeValue;
-import org.apache.hc.core5.util.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -104,13 +103,38 @@ public ClassicHttpResponse execute(
Args.notNull(scope, "scope");
final String exchangeId = scope.exchangeId;
final HttpRoute route = scope.route;
+ final HttpHost target = route.getTargetHost();
final HttpClientContext context = scope.clientContext;
ClassicHttpRequest currentRequest = request;
for (int execCount = 1;; execCount++) {
- final ClassicHttpResponse response;
try {
- response = chain.proceed(currentRequest, scope);
+ final ClassicHttpResponse response = chain.proceed(currentRequest, scope);
+ try {
+ final HttpEntity entity = request.getEntity();
+ if (entity != null && !entity.isRepeatable()) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("{} cannot retry non-repeatable request", exchangeId);
+ }
+ return response;
+ }
+ if (retryStrategy.retryRequest(response, execCount, context)) {
+ response.close();
+ final TimeValue delay = retryStrategy.getRetryInterval(response, execCount, context);
+ if (LOG.isInfoEnabled()) {
+ LOG.info("{} {} responded with status {}; " +
+ "request will be automatically re-executed in {} (exec count {})",
+ exchangeId, target, response.getCode(), delay, execCount + 1);
+ }
+ pause(delay);
+ currentRequest = ClassicRequestBuilder.copy(scope.originalRequest).build();
+ } else {
+ return response;
+ }
+ } catch (final RuntimeException ex) {
+ response.close();
+ throw ex;
+ }
} catch (final IOException ex) {
if (scope.execRuntime.isExecutionAborted()) {
throw new RequestFailedException("Request aborted");
@@ -126,71 +150,34 @@ public ClassicHttpResponse execute(
if (LOG.isDebugEnabled()) {
LOG.debug("{} {}", exchangeId, ex.getMessage(), ex);
}
+ final TimeValue delay = retryStrategy.getRetryInterval(request, ex, execCount, context);
if (LOG.isInfoEnabled()) {
- LOG.info("Recoverable I/O exception ({}) caught when processing request to {}",
- ex.getClass().getName(), route);
- }
- final TimeValue nextInterval = retryStrategy.getRetryInterval(request, ex, execCount, context);
- if (TimeValue.isPositive(nextInterval)) {
- try {
- if (LOG.isDebugEnabled()) {
- LOG.debug("{} wait for {}", exchangeId, nextInterval);
- }
- nextInterval.sleep();
- } catch (final InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new InterruptedIOException();
- }
+ LOG.info("{} recoverable I/O exception ({}) caught when sending request to {};" +
+ "request will be automatically re-executed in {} (exec count {})",
+ exchangeId, ex.getClass().getName(), target, delay, execCount + 1);
}
+ pause(delay);
currentRequest = ClassicRequestBuilder.copy(scope.originalRequest).build();
continue;
}
if (ex instanceof NoHttpResponseException) {
final NoHttpResponseException updatedex = new NoHttpResponseException(
- route.getTargetHost().toHostString() + " failed to respond");
+ target.toHostString() + " failed to respond");
updatedex.setStackTrace(ex.getStackTrace());
throw updatedex;
}
throw ex;
}
+ }
+ }
+ private static void pause(final TimeValue delay) throws InterruptedIOException {
+ if (TimeValue.isPositive(delay)) {
try {
- final HttpEntity entity = request.getEntity();
- if (entity != null && !entity.isRepeatable()) {
- if (LOG.isDebugEnabled()) {
- LOG.debug("{} cannot retry non-repeatable request", exchangeId);
- }
- return response;
- }
- if (retryStrategy.retryRequest(response, execCount, context)) {
- final TimeValue nextInterval = retryStrategy.getRetryInterval(response, execCount, context);
- // Make sure the retry interval does not exceed the response timeout
- if (TimeValue.isPositive(nextInterval)) {
- final RequestConfig requestConfig = context.getRequestConfigOrDefault();
- final Timeout responseTimeout = requestConfig.getResponseTimeout();
- if (responseTimeout != null && nextInterval.compareTo(responseTimeout) > 0) {
- return response;
- }
- }
- response.close();
- if (TimeValue.isPositive(nextInterval)) {
- try {
- if (LOG.isDebugEnabled()) {
- LOG.debug("{} wait for {}", exchangeId, nextInterval);
- }
- nextInterval.sleep();
- } catch (final InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new InterruptedIOException();
- }
- }
- currentRequest = ClassicRequestBuilder.copy(scope.originalRequest).build();
- } else {
- return response;
- }
- } catch (final RuntimeException ex) {
- response.close();
- throw ex;
+ delay.sleep();
+ } catch (final InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new InterruptedIOException();
}
}
}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java
index fe3d35281f..b927dbc798 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java
@@ -295,26 +295,25 @@ private boolean needAuthentication(
}
}
+ boolean targetNeedsAuth = false;
+ boolean proxyNeedsAuth = false;
if (targetAuthRequested || targetMutualAuthRequired) {
- final boolean updated = authenticator.handleResponse(target, ChallengeType.TARGET, response,
+ targetNeedsAuth = authenticator.handleResponse(target, ChallengeType.TARGET, response,
targetAuthStrategy, targetAuthExchange, context);
if (authCacheKeeper != null) {
authCacheKeeper.updateOnResponse(target, pathPrefix, targetAuthExchange, context);
}
-
- return updated;
}
if (proxyAuthRequested || proxyMutualAuthRequired) {
- final boolean updated = authenticator.handleResponse(proxy, ChallengeType.PROXY, response,
+ proxyNeedsAuth = authenticator.handleResponse(proxy, ChallengeType.PROXY, response,
proxyAuthStrategy, proxyAuthExchange, context);
if (authCacheKeeper != null) {
authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context);
}
-
- return updated;
}
+ return targetNeedsAuth || proxyNeedsAuth;
}
return false;
}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/BasicHttpClientConnectionManager.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/BasicHttpClientConnectionManager.java
index ef99e78725..d634d94682 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/BasicHttpClientConnectionManager.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/BasicHttpClientConnectionManager.java
@@ -44,6 +44,7 @@
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.TlsConfig;
import org.apache.hc.client5.http.impl.ConnPoolSupport;
+import org.apache.hc.client5.http.impl.ConnectionHolder;
import org.apache.hc.client5.http.impl.ConnectionShutdownException;
import org.apache.hc.client5.http.io.ConnectionEndpoint;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
@@ -56,6 +57,7 @@
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpConnection;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.URIScheme;
@@ -399,6 +401,9 @@ ManagedHttpClientConnection getConnection(final HttpRoute route, final Object st
this.created = System.currentTimeMillis();
} else {
this.conn.activate();
+ if (connectionConfig.getSocketTimeout() != null) {
+ conn.setSocketTimeout(connectionConfig.getSocketTimeout());
+ }
}
this.leased = true;
if (LOG.isDebugEnabled()) {
@@ -590,7 +595,7 @@ public void setValidateAfterInactivity(final TimeValue validateAfterInactivity)
.build();
}
- class InternalConnectionEndpoint extends ConnectionEndpoint implements Identifiable {
+ class InternalConnectionEndpoint extends ConnectionEndpoint implements ConnectionHolder, Identifiable {
private final HttpRoute route;
private final AtomicReference connRef;
@@ -703,6 +708,11 @@ public EndpointInfo getInfo() {
return null;
}
+ @Override
+ public HttpConnection get() {
+ return this.connRef.get();
+ }
+
}
/**
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/DefaultHttpClientConnectionOperator.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/DefaultHttpClientConnectionOperator.java
index f81bd797db..a10c436b88 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/DefaultHttpClientConnectionOperator.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/DefaultHttpClientConnectionOperator.java
@@ -31,16 +31,20 @@
import java.net.Proxy;
import java.net.Socket;
import java.net.SocketAddress;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.net.ssl.SSLSocket;
+import jdk.net.ExtendedSocketOptions;
+import jdk.net.Sockets;
import org.apache.hc.client5.http.ConnectExceptionSupport;
import org.apache.hc.client5.http.DnsResolver;
import org.apache.hc.client5.http.SchemePortResolver;
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
import org.apache.hc.client5.http.UnsupportedSchemeException;
+import org.apache.hc.client5.http.config.TlsConfig;
import org.apache.hc.client5.http.impl.ConnPoolSupport;
import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
import org.apache.hc.client5.http.io.DetachedSocketFactory;
@@ -77,6 +81,11 @@ public class DefaultHttpClientConnectionOperator implements HttpClientConnection
private static final Logger LOG = LoggerFactory.getLogger(DefaultHttpClientConnectionOperator.class);
+ @SuppressWarnings("Since15")
+ private static final boolean SUPPORTS_KEEPALIVE_OPTIONS = Sockets.supportedOptions(Socket.class)
+ .containsAll(Arrays.asList(ExtendedSocketOptions.TCP_KEEPIDLE, ExtendedSocketOptions.TCP_KEEPINTERVAL,
+ ExtendedSocketOptions.TCP_KEEPCOUNT));
+
static final DetachedSocketFactory PLAIN_SOCKET_FACTORY = socksProxy -> socksProxy == null ? new Socket() : new Socket(socksProxy);
private final DetachedSocketFactory detachedSocketFactory;
@@ -143,6 +152,7 @@ public void connect(
connect(conn, host, null, localAddress, timeout, socketConfig, null, context);
}
+ @SuppressWarnings("Since15")
@Override
public void connect(
final ManagedHttpClientConnection conn,
@@ -159,7 +169,6 @@ public void connect(
Args.notNull(socketConfig, "Socket config");
Args.notNull(context, "Context");
- final Timeout soTimeout = socketConfig.getSoTimeout();
final SocketAddress socksProxyAddress = socketConfig.getSocksProxyAddress();
final Proxy socksProxy = socksProxyAddress != null ? new Proxy(Proxy.Type.SOCKS, socksProxyAddress) : null;
@@ -185,8 +194,9 @@ public void connect(
socket.bind(localAddress);
}
conn.bind(socket);
- if (soTimeout != null) {
- socket.setSoTimeout(soTimeout.toMillisecondsIntBound());
+ final Timeout socketTimeout = socketConfig.getSoTimeout();
+ if (socketTimeout != null) {
+ socket.setSoTimeout(socketTimeout.toMillisecondsIntBound());
}
socket.setReuseAddress(socketConfig.isSoReuseAddress());
socket.setTcpNoDelay(socketConfig.isTcpNoDelay());
@@ -197,7 +207,18 @@ public void connect(
if (socketConfig.getSndBufSize() > 0) {
socket.setSendBufferSize(socketConfig.getSndBufSize());
}
-
+ if (SUPPORTS_KEEPALIVE_OPTIONS) {
+ if (socketConfig.getTcpKeepIdle() > 0) {
+ Sockets.setOption(socket, ExtendedSocketOptions.TCP_KEEPIDLE, socketConfig.getTcpKeepIdle());
+ }
+ if (socketConfig.getTcpKeepInterval() > 0) {
+ Sockets.setOption(socket, ExtendedSocketOptions.TCP_KEEPINTERVAL,
+ socketConfig.getTcpKeepInterval());
+ }
+ if (socketConfig.getTcpKeepCount() > 0) {
+ Sockets.setOption(socket, ExtendedSocketOptions.TCP_KEEPCOUNT, socketConfig.getTcpKeepCount());
+ }
+ }
final int linger = socketConfig.getSoLinger().toMillisecondsIntBound();
if (linger >= 0) {
socket.setSoLinger(true, linger);
@@ -208,7 +229,6 @@ public void connect(
if (LOG.isDebugEnabled()) {
LOG.debug("{} {} connected {}->{}", ConnPoolSupport.getId(conn), endpointHost, conn.getLocalAddress(), conn.getRemoteAddress());
}
- conn.setSocketTimeout(soTimeout);
final TlsSocketStrategy tlsSocketStrategy = tlsSocketStrategyLookup != null ? tlsSocketStrategyLookup.lookup(endpointHost.getSchemeName()) : null;
if (tlsSocketStrategy != null) {
final NamedEndpoint tlsName = endpointName != null ? endpointName : endpointHost;
@@ -216,8 +236,15 @@ public void connect(
if (LOG.isDebugEnabled()) {
LOG.debug("{} {} upgrading to TLS", ConnPoolSupport.getId(conn), tlsName);
}
+ final TlsConfig tlsConfig = attachment instanceof TlsConfig ? (TlsConfig) attachment : TlsConfig.DEFAULT;
+ final int soTimeout = socket.getSoTimeout();
+ final Timeout handshakeTimeout = tlsConfig.getHandshakeTimeout() != null ? tlsConfig.getHandshakeTimeout() : connectTimeout;
+ if (handshakeTimeout != null) {
+ socket.setSoTimeout(handshakeTimeout.toMillisecondsIntBound());
+ }
final SSLSocket sslSocket = tlsSocketStrategy.upgrade(socket, tlsName.getHostName(), tlsName.getPort(), attachment, context);
conn.bind(sslSocket, socket);
+ socket.setSoTimeout(soTimeout);
onAfterTlsHandshake(context, endpointHost);
if (LOG.isDebugEnabled()) {
LOG.debug("{} {} upgraded to TLS", ConnPoolSupport.getId(conn), tlsName);
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManager.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManager.java
index 8d509209a5..c16eddce0c 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManager.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManager.java
@@ -42,6 +42,7 @@
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.TlsConfig;
import org.apache.hc.client5.http.impl.ConnPoolSupport;
+import org.apache.hc.client5.http.impl.ConnectionHolder;
import org.apache.hc.client5.http.impl.ConnectionShutdownException;
import org.apache.hc.client5.http.impl.PrefixedIncrementingId;
import org.apache.hc.client5.http.io.ConnectionEndpoint;
@@ -57,6 +58,7 @@
import org.apache.hc.core5.function.Resolver;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpConnection;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.URIScheme;
@@ -68,6 +70,7 @@
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.io.CloseMode;
import org.apache.hc.core5.pool.ConnPoolControl;
+import org.apache.hc.core5.pool.DefaultDisposalCallback;
import org.apache.hc.core5.pool.LaxConnPool;
import org.apache.hc.core5.pool.ManagedConnPool;
import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
@@ -84,15 +87,15 @@
import org.slf4j.LoggerFactory;
/**
- * {@code ClientConnectionPoolManager} maintains a pool of
+ * {@code PoolingHttpClientConnectionManager} maintains a pool of
* {@link ManagedHttpClientConnection}s and is able to service connection requests
* from multiple execution threads. Connections are pooled on a per route
* basis. A request for a route which already the manager has persistent
* connections for available in the pool will be serviced by leasing
* a connection from the pool rather than creating a new connection.
*
- * {@code ClientConnectionPoolManager} maintains a maximum limit of connection
- * on a per route basis and in total. Connection limits, however, can be adjusted
+ * {@code PoolingHttpClientConnectionManager} maintains a maximum limit of connections
+ * per route and in total. Connection limits, however, can be adjusted
* using {@link ConnPoolControl} methods.
*
* Total time to live (TTL) set at construction time defines maximum life span
@@ -221,6 +224,7 @@ public PoolingHttpClientConnectionManager(
DEFAULT_MAX_TOTAL_CONNECTIONS,
timeToLive,
poolReusePolicy,
+ new DefaultDisposalCallback<>(),
null) {
@Override
@@ -384,6 +388,9 @@ public ConnectionEndpoint get(
final ManagedHttpClientConnection conn = poolEntry.getConnection();
if (conn != null) {
conn.activate();
+ if (connectionConfig.getSocketTimeout() != null) {
+ conn.setSocketTimeout(connectionConfig.getSocketTimeout());
+ }
} else {
poolEntry.assignConnection(connFactory.createConnection(null));
}
@@ -689,7 +696,7 @@ public void setValidateAfterInactivity(final TimeValue validateAfterInactivity)
private static final PrefixedIncrementingId INCREMENTING_ID = new PrefixedIncrementingId("ep-");
- static class InternalConnectionEndpoint extends ConnectionEndpoint implements Identifiable {
+ static class InternalConnectionEndpoint extends ConnectionEndpoint implements ConnectionHolder, Identifiable {
private final AtomicReference> poolEntryRef;
private final String id;
@@ -806,6 +813,12 @@ public EndpointInfo getInfo() {
return null;
}
+ @Override
+ public HttpConnection get() {
+ final PoolEntry poolEntry = poolEntryRef.get();
+ return poolEntry != null ? poolEntry.getConnection() : null;
+ }
+
}
/**
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/DefaultAsyncClientConnectionOperator.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/DefaultAsyncClientConnectionOperator.java
index 1d3531373f..4a90e45a40 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/DefaultAsyncClientConnectionOperator.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/DefaultAsyncClientConnectionOperator.java
@@ -69,7 +69,15 @@ public class DefaultAsyncClientConnectionOperator implements AsyncClientConnecti
private final MultihomeIOSessionRequester sessionRequester;
private final Lookup tlsStrategyLookup;
- DefaultAsyncClientConnectionOperator(
+ /**
+ * Constructs a new {@code DefaultAsyncClientConnectionOperator}.
+ *
+ * Note: this class is marked {@code @Internal}; rely on it
+ * only if you are prepared for incompatible changes in a future major
+ * release. Typical client code should use the high-level builders in
+ * {@code HttpAsyncClients} instead.
+ */
+ protected DefaultAsyncClientConnectionOperator(
final Lookup tlsStrategyLookup,
final SchemePortResolver schemePortResolver,
final DnsResolver dnsResolver) {
@@ -133,7 +141,7 @@ public void completed(final IOSession session) {
if (tlsStrategy != null) {
try {
final Timeout socketTimeout = connection.getSocketTimeout();
- final Timeout handshakeTimeout = tlsConfig.getHandshakeTimeout();
+ final Timeout handshakeTimeout = tlsConfig.getHandshakeTimeout() != null ? tlsConfig.getHandshakeTimeout() : connectTimeout;
final NamedEndpoint tlsName = endpointName != null ? endpointName : endpointHost;
onBeforeTlsHandshake(context, endpointHost);
if (LOG.isDebugEnabled()) {
@@ -143,7 +151,7 @@ public void completed(final IOSession session) {
connection,
tlsName,
attachment,
- handshakeTimeout != null ? handshakeTimeout : connectTimeout,
+ handshakeTimeout,
new FutureContribution(future) {
@Override
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/H2SharingConnPool.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/H2SharingConnPool.java
index 2efc867fe1..57c219d3c4 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/H2SharingConnPool.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/H2SharingConnPool.java
@@ -270,16 +270,17 @@ long track(final PoolEntry entry) {
PoolEntry lease() {
lock.lock();
try {
- final PoolEntry entry = entryMap.entrySet().stream()
+ return entryMap.entrySet().stream()
+ .filter(e -> {
+ final C conn = e.getKey().getConnection();
+ return conn != null && conn.isOpen();
+ })
.min(Comparator.comparingLong(e -> e.getValue().get()))
- .map(Map.Entry::getKey)
+ .map(e -> {
+ e.getValue().incrementAndGet();
+ return e.getKey();
+ })
.orElse(null);
- if (entry == null) {
- return null;
- }
- final AtomicLong counter = getCounter(entry);
- counter.incrementAndGet();
- return entry;
} finally {
lock.unlock();
}
@@ -288,20 +289,18 @@ PoolEntry lease() {
long release(final PoolEntry entry, final boolean reusable) {
lock.lock();
try {
- final C connection = entry.getConnection();
- if (!reusable || connection == null || !connection.isOpen()) {
- entryMap.remove(entry);
- return 0;
- } else {
- final AtomicLong counter = entryMap.compute(entry, (e, c) -> {
- if (c == null) {
- return null;
- }
- final long count = c.decrementAndGet();
- return count > 0 ? c : null;
- });
- return counter != null ? counter.get() : 0L;
+ if (!reusable) {
+ entry.discardConnection(CloseMode.GRACEFUL);
}
+
+ final AtomicLong counter = entryMap.compute(entry, (e, c) -> {
+ if (c == null) {
+ return null;
+ }
+ final long count = c.decrementAndGet();
+ return count > 0 ? c : null;
+ });
+ return counter != null ? counter.get() : 0L;
} finally {
lock.unlock();
}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java
index cd5473f2f1..90d3df916c 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java
@@ -42,6 +42,7 @@
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.TlsConfig;
import org.apache.hc.client5.http.impl.ConnPoolSupport;
+import org.apache.hc.client5.http.impl.ConnectionHolder;
import org.apache.hc.client5.http.impl.ConnectionShutdownException;
import org.apache.hc.client5.http.impl.PrefixedIncrementingId;
import org.apache.hc.client5.http.nio.AsyncClientConnectionManager;
@@ -57,6 +58,7 @@
import org.apache.hc.core5.concurrent.ComplexFuture;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.function.Resolver;
+import org.apache.hc.core5.http.HttpConnection;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpVersion;
import org.apache.hc.core5.http.ProtocolVersion;
@@ -75,6 +77,7 @@
import org.apache.hc.core5.http2.ssl.ApplicationProtocol;
import org.apache.hc.core5.io.CloseMode;
import org.apache.hc.core5.pool.ConnPoolControl;
+import org.apache.hc.core5.pool.DefaultDisposalCallback;
import org.apache.hc.core5.pool.LaxConnPool;
import org.apache.hc.core5.pool.ManagedConnPool;
import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
@@ -179,6 +182,7 @@ public PoolingAsyncClientConnectionManager(
DEFAULT_MAX_TOTAL_CONNECTIONS,
timeToLive,
poolReusePolicy,
+ new DefaultDisposalCallback<>(),
null) {
@Override
@@ -314,10 +318,6 @@ public void completed(final PoolEntry p
})), Command.Priority.IMMEDIATE);
return;
}
- if (LOG.isDebugEnabled()) {
- LOG.debug("{} connection {} is closed", id, ConnPoolSupport.getId(connection));
- }
- poolEntry.discardConnection(CloseMode.IMMEDIATE);
}
}
}
@@ -328,6 +328,9 @@ void leaseCompleted(final PoolEntry poo
final ManagedAsyncClientConnection connection = poolEntry.getConnection();
if (connection != null) {
connection.activate();
+ if (connectionConfig.getSocketTimeout() != null) {
+ connection.setSocketTimeout(connectionConfig.getSocketTimeout());
+ }
}
if (LOG.isDebugEnabled()) {
LOG.debug("{} endpoint leased {}", id, ConnPoolSupport.formatStats(route, state, pool));
@@ -680,7 +683,7 @@ public void setValidateAfterInactivity(final TimeValue validateAfterInactivity)
private static final PrefixedIncrementingId INCREMENTING_ID = new PrefixedIncrementingId("ep-");
- static class InternalConnectionEndpoint extends AsyncConnectionEndpoint implements Identifiable {
+ static class InternalConnectionEndpoint extends AsyncConnectionEndpoint implements ConnectionHolder, Identifiable {
private final AtomicReference> poolEntryRef;
private final String id;
@@ -777,6 +780,12 @@ public EndpointInfo getInfo() {
return null;
}
+ @Override
+ public HttpConnection get() {
+ final PoolEntry poolEntry = poolEntryRef.get();
+ return poolEntry != null ? poolEntry.getConnection() : null;
+ }
+
}
/**
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/AbstractClientTlsStrategy.java b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/AbstractClientTlsStrategy.java
index e4f6481f6e..35373c8650 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/AbstractClientTlsStrategy.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/AbstractClientTlsStrategy.java
@@ -220,8 +220,6 @@ private void executeHandshake(
final SSLSocket upgradedSocket,
final String target,
final Object attachment) throws IOException {
- final TlsConfig tlsConfig = attachment instanceof TlsConfig ? (TlsConfig) attachment : TlsConfig.DEFAULT;
-
final SSLParameters sslParameters = upgradedSocket.getSSLParameters();
if (supportedProtocols != null) {
sslParameters.setProtocols(supportedProtocols);
@@ -238,17 +236,11 @@ private void executeHandshake(
}
upgradedSocket.setSSLParameters(sslParameters);
- final Timeout handshakeTimeout = tlsConfig.getHandshakeTimeout();
- if (handshakeTimeout != null) {
- upgradedSocket.setSoTimeout(handshakeTimeout.toMillisecondsIntBound());
- }
-
initializeSocket(upgradedSocket);
if (LOG.isDebugEnabled()) {
LOG.debug("Enabled protocols: {}", (Object) upgradedSocket.getEnabledProtocols());
LOG.debug("Enabled cipher suites: {}", (Object) upgradedSocket.getEnabledCipherSuites());
- LOG.debug("Starting handshake ({})", handshakeTimeout);
}
upgradedSocket.startHandshake();
verifySession(target, upgradedSocket.getSession());
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestDefaultHttpRequestRetryStrategy.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestDefaultHttpRequestRetryStrategy.java
index 2440f5c989..4c2649b524 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestDefaultHttpRequestRetryStrategy.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestDefaultHttpRequestRetryStrategy.java
@@ -33,16 +33,17 @@
import java.net.UnknownHostException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
-
import javax.net.ssl.SSLException;
-
import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.core5.http.ConnectionClosedException;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.message.BasicHttpResponse;
import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -74,6 +75,29 @@ void testBasics() {
Assertions.assertEquals(TimeValue.ofMilliseconds(1234L), this.retryStrategy.getRetryInterval(response1, 1, null));
}
+ @Test
+ void testRetryRequestWithResponseTimeout() {
+ final HttpResponse response = new BasicHttpResponse(503, "Oopsie");
+
+ final HttpClientContext context = HttpClientContext.create();
+ context.setRequestConfig(RequestConfig.custom()
+ .build());
+
+ Assertions.assertTrue(retryStrategy.retryRequest(response, 1, context));
+
+ context.setRequestConfig(RequestConfig.custom()
+ .setResponseTimeout(Timeout.ofMilliseconds(1234L))
+ .build());
+
+ Assertions.assertTrue(retryStrategy.retryRequest(response, 1, context));
+
+ context.setRequestConfig(RequestConfig.custom()
+ .setResponseTimeout(Timeout.ofMilliseconds(1233L))
+ .build());
+
+ Assertions.assertFalse(retryStrategy.retryRequest(response, 1, context));
+ }
+
@Test
void testRetryAfterHeaderAsLong() {
final HttpResponse response = new BasicHttpResponse(503, "Oopsie");
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestDefaultRedirectStrategy.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestDefaultRedirectStrategy.java
index 406cb12772..ccbfc916f7 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestDefaultRedirectStrategy.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestDefaultRedirectStrategy.java
@@ -324,4 +324,48 @@ void testRedirectAllowed() throws Exception {
null));
}
+
+
+
+ @Test
+ void testRedirectAllowedDefaultPortNormalization() {
+ final DefaultRedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
+
+ // HTTPS with explicit 443 vs HTTPS with no port (defaults to 443)
+ final HttpHost explicitHttps = new HttpHost("https", "example.com", 443);
+ final HttpHost implicitHttps = new HttpHost("https", "example.com", -1);
+ Assertions.assertTrue(redirectStrategy.isRedirectAllowed(
+ explicitHttps,
+ implicitHttps,
+ BasicRequestBuilder.get("/")
+ .addHeader(HttpHeaders.AUTHORIZATION, "token")
+ .build(),
+ null));
+ Assertions.assertTrue(redirectStrategy.isRedirectAllowed(
+ implicitHttps,
+ explicitHttps,
+ BasicRequestBuilder.get("/")
+ .addHeader(HttpHeaders.COOKIE, "cookie=123")
+ .build(),
+ null));
+
+ final HttpHost explicitHttp = new HttpHost("http", "example.org", 80);
+ final HttpHost implicitHttp = new HttpHost("http", "example.org", -1);
+ Assertions.assertTrue(redirectStrategy.isRedirectAllowed(
+ explicitHttp,
+ implicitHttp,
+ BasicRequestBuilder.get("/")
+ .addHeader(HttpHeaders.AUTHORIZATION, "token123")
+ .build(),
+ null));
+ Assertions.assertTrue(redirectStrategy.isRedirectAllowed(
+ implicitHttp,
+ explicitHttp,
+ BasicRequestBuilder.get("/")
+ .addHeader(HttpHeaders.COOKIE, "cookie=abc")
+ .build(),
+ null));
+ }
+
+
}
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java
index 6b0fc5b59d..e44fe89558 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java
@@ -903,7 +903,7 @@ void testDigestAuthenticationWithNonAsciiUsername() throws Exception {
}
@Test
- void testRspAuthFieldAndQuoting() throws Exception {
+ void testRspAuthFieldNotPresentClient() throws Exception {
final ClassicHttpRequest request = new BasicClassicHttpRequest("POST", "/");
final HttpHost host = new HttpHost("somehost", 80);
final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
@@ -921,7 +921,8 @@ void testRspAuthFieldAndQuoting() throws Exception {
final Map table = parseAuthResponse(authResponse);
- Assertions.assertNotNull(table.get("rspauth"));
+ Assertions.assertNotNull(table);
+ Assertions.assertNull(table.get("rspauth"));
}
@Test
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestContentCompressionExec.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestContentCompressionExec.java
index 60eaa27f6f..13df4e32c8 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestContentCompressionExec.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestContentCompressionExec.java
@@ -40,6 +40,7 @@
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.Method;
+import org.apache.hc.core5.http.ProtocolException;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
@@ -236,4 +237,35 @@ void testContentEncodingRequestParameter() throws Exception {
Assertions.assertFalse(entity instanceof GzipDecompressingEntity);
}
+ @Test
+ void testContentEncodingExceedsCodecListLenMax() throws Exception {
+ impl = new ContentCompressionExec(5);
+
+ final ClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, host, "/");
+ final ClassicHttpResponse response1 = new BasicClassicHttpResponse(200, "OK");
+ final HttpEntity original1 = EntityBuilder.create()
+ .setText("encoded stuff")
+ .setContentEncoding("gzip,gzip,gzip,gzip,gzip")
+ .build();
+ response1.setEntity(original1);
+
+ Mockito.when(execChain.proceed(request, scope)).thenReturn(response1);
+
+ final HttpEntity entity = response1.getEntity();
+ Assertions.assertNotNull(entity);
+ Assertions.assertFalse(entity instanceof GzipDecompressingEntity);
+
+ final ClassicHttpResponse response2 = new BasicClassicHttpResponse(200, "OK");
+ final HttpEntity original2 = EntityBuilder.create()
+ .setText("encoded stuff")
+ .setContentEncoding("gzip,gzip,gzip,gzip,gzip,gzip")
+ .build();
+ response2.setEntity(original2);
+
+ Mockito.when(execChain.proceed(request, scope)).thenReturn(response2);
+
+ final ProtocolException exception = Assertions.assertThrows(ProtocolException.class, () -> impl.execute(request, scope, execChain));
+ Assertions.assertEquals("Codec list exceeds maximum of 5 elements", exception.getMessage());
+ }
+
}
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestHttpRequestRetryExec.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestHttpRequestRetryExec.java
index 5672587c50..006ef9a506 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestHttpRequestRetryExec.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestHttpRequestRetryExec.java
@@ -45,7 +45,6 @@
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.util.TimeValue;
-import org.apache.hc.core5.util.Timeout;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -138,38 +137,6 @@ void testRetrySleepOnIOException() throws Exception {
Mockito.verify(nextInterval, Mockito.times(1)).sleep();
}
- @Test
- void testRetryIntervalGreaterResponseTimeout() throws Exception {
- final HttpRoute route = new HttpRoute(target);
- final HttpGet request = new HttpGet("/test");
- final HttpClientContext context = HttpClientContext.create();
- context.setRequestConfig(RequestConfig.custom()
- .setResponseTimeout(Timeout.ofSeconds(3))
- .build());
-
- final ClassicHttpResponse response = Mockito.mock(ClassicHttpResponse.class);
-
- Mockito.when(chain.proceed(
- Mockito.same(request),
- Mockito.any())).thenReturn(response);
- Mockito.when(retryStrategy.retryRequest(
- Mockito.any(),
- Mockito.anyInt(),
- Mockito.any())).thenReturn(Boolean.TRUE, Boolean.FALSE);
- Mockito.when(retryStrategy.getRetryInterval(
- Mockito.any(),
- Mockito.anyInt(),
- Mockito.any())).thenReturn(TimeValue.ofSeconds(5));
-
- final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
- retryExec.execute(request, scope, chain);
-
- Mockito.verify(chain, Mockito.times(1)).proceed(
- Mockito.any(),
- Mockito.same(scope));
- Mockito.verify(response, Mockito.times(0)).close();
- }
-
@Test
void testRetryIntervalResponseTimeoutNull() throws Exception {
final HttpRoute route = new HttpRoute(target);
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/H2SharingConnPoolTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/H2SharingConnPoolTest.java
index 26fa2e1cb6..077db0ff81 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/H2SharingConnPoolTest.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/H2SharingConnPoolTest.java
@@ -86,10 +86,16 @@ void testLeaseFutureReturned() throws Exception {
@Test
void testLeaseExistingConnectionReturned() throws Exception {
final PoolEntry poolEntry = new PoolEntry<>(DEFAULT_ROUTE);
- final H2SharingConnPool.PerRoutePool routePool = h2SharingPool.getPerRoutePool(DEFAULT_ROUTE);
+ final HttpConnection conn = Mockito.mock(HttpConnection.class);
+ Mockito.when(conn.isOpen()).thenReturn(true);
+ poolEntry.assignConnection(conn);
+
+ final H2SharingConnPool.PerRoutePool routePool =
+ h2SharingPool.getPerRoutePool(DEFAULT_ROUTE);
routePool.track(poolEntry);
+ final Future> future =
+ h2SharingPool.lease(DEFAULT_ROUTE, null, Timeout.ONE_MILLISECOND, callback);
- final Future> future = h2SharingPool.lease(DEFAULT_ROUTE, null, Timeout.ONE_MILLISECOND, callback);
Assertions.assertNotNull(future);
Assertions.assertSame(poolEntry, future.get());
@@ -98,8 +104,7 @@ void testLeaseExistingConnectionReturned() throws Exception {
Mockito.any(),
Mockito.any(),
Mockito.any());
- Mockito.verify(callback).completed(
- Mockito.same(poolEntry));
+ Mockito.verify(callback).completed(Mockito.same(poolEntry));
}
@Test
@@ -314,24 +319,15 @@ void testReleaseReusableInCacheNotReturnedToPool() throws Exception {
Mockito.anyBoolean());
}
+ /**
+ * Same connection can only be released once.
+ * Attempting to release it again will throw: IllegalStateException("Pool entry is not present in the set of leased entries")
+ *
+ * @see org.apache.hc.core5.pool.LaxConnPool.PerRoutePool#removeLeased(PoolEntry)
+ * @see org.apache.hc.core5.pool.StrictConnPool#release(PoolEntry, boolean)
+ */
@Test
- void testReleaseNonReusableInCacheReturnedToPool() throws Exception {
- final PoolEntry poolEntry = new PoolEntry<>(DEFAULT_ROUTE);
- poolEntry.assignConnection(connection);
- Mockito.when(connection.isOpen()).thenReturn(true);
- final H2SharingConnPool.PerRoutePool routePool = h2SharingPool.getPerRoutePool(DEFAULT_ROUTE);
- routePool.track(poolEntry);
- routePool.track(poolEntry);
-
- h2SharingPool.release(poolEntry, false);
-
- Mockito.verify(connPool).release(
- Mockito.same(poolEntry),
- Mockito.eq(false));
- }
-
- @Test
- void testReleaseReusableAndClosedInCacheReturnedToPool() throws Exception {
+ void testReleaseNonReusableNotInCacheReturnedToPool() throws Exception {
final PoolEntry poolEntry = new PoolEntry<>(DEFAULT_ROUTE);
poolEntry.assignConnection(connection);
Mockito.when(connection.isOpen()).thenReturn(false);
@@ -339,11 +335,18 @@ void testReleaseReusableAndClosedInCacheReturnedToPool() throws Exception {
routePool.track(poolEntry);
routePool.track(poolEntry);
- h2SharingPool.release(poolEntry, true);
+ final AtomicReference connRef = new AtomicReference<>(connection);
+ Mockito.doAnswer(invocation -> {
+ final PoolEntry entry = invocation.getArgument(0);
+ if (!connRef.compareAndSet(entry.getConnection(), null)) {
+ throw new IllegalStateException("Pool entry is not present in the set of leased entries");
+ }
+ return null;
+ }).when(connPool).release(Mockito.eq(poolEntry), Mockito.anyBoolean());
- Mockito.verify(connPool).release(
- Mockito.same(poolEntry),
- Mockito.eq(true));
+ h2SharingPool.release(poolEntry, false);
+ // for reproduce https://issues.apache.org/jira/browse/HTTPCLIENT-2379
+ Assertions.assertThrows(IllegalStateException.class, () -> h2SharingPool.release(poolEntry, false));
}
@Test
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/H2SharingPerRoutePoolTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/H2SharingPerRoutePoolTest.java
index dc1a573fa5..b956fb08c5 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/H2SharingPerRoutePoolTest.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/nio/H2SharingPerRoutePoolTest.java
@@ -98,7 +98,7 @@ void testReleaseNonReusable() {
pool.track(poolEntry1);
pool.track(poolEntry1);
- Assertions.assertEquals(0, pool.release(poolEntry1, false));
+ Assertions.assertEquals(2, pool.release(poolEntry1, false)); // 3 → 2
}
@Test
@@ -114,7 +114,7 @@ void testReleaseConnectionClosed() {
pool.track(poolEntry1);
Mockito.when(poolEntry1.getConnection().isOpen()).thenReturn(false);
- Assertions.assertEquals(0, pool.release(poolEntry1, true));
+ Assertions.assertEquals(2, pool.release(poolEntry1, true)); // 3 → 2
}
@Test
@@ -124,7 +124,7 @@ void testReleaseConnectionMissing() {
pool.track(poolEntry1);
poolEntry1.discardConnection(CloseMode.IMMEDIATE);
- Assertions.assertEquals(0, pool.release(poolEntry1, true));
+ Assertions.assertEquals(2, pool.release(poolEntry1, true)); // 3 → 2
}
}
diff --git a/pom.xml b/pom.xml
index 03abba507e..eba25a72e8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -33,7 +33,7 @@
org.apache.httpcomponents.client5
httpclient5-parent
Apache HttpComponents Client Parent
- 5.5.1-SNAPSHOT
+ 5.5.3-SNAPSHOT
Apache HttpComponents Client is a library of components for building client side HTTP services
https://hc.apache.org/httpcomponents-client-5.5.x/${project.version}/
1999
@@ -48,7 +48,7 @@
scm:git:https://gitbox.apache.org/repos/asf/httpcomponents-client.git
scm:git:https://gitbox.apache.org/repos/asf/httpcomponents-client.git
https://github.com/apache/httpcomponents-client/tree/${project.scm.tag}
- 5.5.1-SNAPSHOT
+ 5.5.3-SNAPSHOT
@@ -62,21 +62,21 @@
1.8
1.8
- 5.3.4
+ 5.3.6
2.24.3
0.1.2
2.5.2
3.10.8
2.12.3
1.7.36
- 5.13.0
+ 5.13.3
3.0
4.11.0
1
2.2.21
1.21.1
5.3
- javax.net.ssl.SSLEngine,javax.net.ssl.SSLParameters,java.nio.ByteBuffer,java.nio.CharBuffer
+ javax.net.ssl.SSLEngine,javax.net.ssl.SSLParameters,java.nio.ByteBuffer,java.nio.CharBuffer,jdk.net.ExtendedSocketOptions,jdk.net.Sockets