diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestRequestTimeoutClassic.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestRequestTimeoutClassic.java
new file mode 100644
index 0000000000..0dacf65211
--- /dev/null
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestRequestTimeoutClassic.java
@@ -0,0 +1,194 @@
+/*
+ * ====================================================================
+ * 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.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.impl.bootstrap.HttpServer;
+import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap;
+import org.apache.hc.core5.http.io.HttpRequestHandler;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.net.URIBuilder;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class TestRequestTimeoutClassic {
+
+ private static HttpServer server;
+ private static HttpHost target;
+
+ private CloseableHttpClient client;
+
+ private static final HttpRequestHandler DELAY_HANDLER = (request, response, context) -> {
+ int seconds = 1;
+ final String path = request.getPath(); // e.g. /delay/5
+ final int idx = path.lastIndexOf('/');
+ if (idx >= 0 && idx + 1 < path.length()) {
+ try {
+ seconds = Integer.parseInt(path.substring(idx + 1));
+ } catch (final NumberFormatException ignore) { /* default 1s */ }
+ }
+ try {
+ TimeUnit.SECONDS.sleep(seconds);
+ } catch (final InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ }
+ response.setCode(200);
+ response.setEntity(new StringEntity("{\"ok\":true}", ContentType.APPLICATION_JSON));
+ };
+
+ @BeforeAll
+ static void startServer() throws Exception {
+ server = ServerBootstrap.bootstrap()
+ .setCanonicalHostName("localhost")
+ .register("/delay/*", DELAY_HANDLER)
+ .create();
+ server.start();
+ target = new HttpHost("http", "localhost", server.getLocalPort());
+ }
+
+ @AfterAll
+ static void stopServer() {
+ if (server != null) {
+ server.stop();
+ }
+ }
+
+ @BeforeEach
+ void createClient() {
+ final PoolingHttpClientConnectionManager cm =
+ PoolingHttpClientConnectionManagerBuilder.create()
+ .setDefaultConnectionConfig(ConnectionConfig.custom()
+ .setConnectTimeout(Timeout.ofSeconds(5))
+ .setSocketTimeout(Timeout.ofSeconds(5))
+ .build())
+ .build();
+
+ client = HttpClients.custom()
+ .setConnectionManager(cm)
+ .build();
+ }
+
+ @AfterEach
+ void closeClient() throws IOException {
+ if (client != null) {
+ client.close();
+ }
+ }
+
+ @Test
+ @org.junit.jupiter.api.Timeout(value = 10, unit = TimeUnit.SECONDS)
+ void timesOutHard() throws Exception {
+ final HttpGet req = new HttpGet(new URIBuilder()
+ .setScheme(target.getSchemeName())
+ .setHost(target.getHostName())
+ .setPort(target.getPort())
+ .setPath("/delay/5")
+ .build());
+ req.setConfig(RequestConfig.custom()
+ .setRequestTimeout(Timeout.ofSeconds(1)) // hard end-to-end deadline
+ .setConnectionRequestTimeout(Timeout.ofSeconds(2)) // pool lease cap
+ .build());
+
+ final IOException ex = assertThrows(IOException.class,
+ () -> client.execute(req, resp -> resp.getCode()));
+ assertTrue(ex instanceof java.io.InterruptedIOException,
+ "Expected InterruptedIOException, got: " + ex.getClass());
+ }
+
+ @Test
+ @org.junit.jupiter.api.Timeout(value = 10, unit = TimeUnit.SECONDS)
+ void succeedsWithinBudget() throws Exception {
+ final HttpGet req = new HttpGet(new URIBuilder()
+ .setScheme(target.getSchemeName())
+ .setHost(target.getHostName())
+ .setPort(target.getPort())
+ .setPath("/delay/1")
+ .build());
+ req.setConfig(RequestConfig.custom()
+ .setRequestTimeout(Timeout.ofSeconds(5)) // enough for lease+connect+1s delay
+ .setConnectionRequestTimeout(Timeout.ofSeconds(2))
+ .build());
+
+ final int code = client.execute(req, resp -> resp.getCode());
+ assertEquals(200, code);
+ }
+
+ @Test
+ @org.junit.jupiter.api.Timeout(value = 10, unit = TimeUnit.SECONDS)
+ void immediateExpirationFailsBeforeSend() throws Exception {
+ final HttpGet req = new HttpGet(new URIBuilder()
+ .setScheme(target.getSchemeName())
+ .setHost(target.getHostName())
+ .setPort(target.getPort())
+ .setPath("/delay/1")
+ .build());
+ req.setConfig(RequestConfig.custom()
+ .setRequestTimeout(Timeout.ofMilliseconds(1)) // near-immediate expiry
+ .setConnectionRequestTimeout(Timeout.ofSeconds(1))
+ .build());
+
+ assertThrows(java.io.InterruptedIOException.class,
+ () -> client.execute(req, resp -> resp.getCode()));
+ }
+
+ @Test
+ @org.junit.jupiter.api.Timeout(value = 10, unit = TimeUnit.SECONDS)
+ void largeBudgetStillHonorsPerOpTimeouts() throws Exception {
+ final HttpGet req = new HttpGet(new URIBuilder()
+ .setScheme(target.getSchemeName())
+ .setHost(target.getHostName())
+ .setPort(target.getPort())
+ .setPath("/delay/1")
+ .build());
+ req.setConfig(RequestConfig.custom()
+ .setRequestTimeout(Timeout.ofSeconds(30))
+ .setConnectionRequestTimeout(Timeout.ofSeconds(2))
+ .build());
+
+ final int code = client.execute(req, resp -> resp.getCode());
+ assertEquals(200, code);
+ }
+}
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 a589961307..40639692f6 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
@@ -72,13 +72,16 @@ public class RequestConfig implements Cloneable {
*/
private final PriorityValue h2Priority;
+
+ private final Timeout requestTimeout;
+
/**
* Intended for CDI compatibility
*/
protected RequestConfig() {
this(false, null, null, false, false, 0, false, null, null,
DEFAULT_CONNECTION_REQUEST_TIMEOUT, null, null, DEFAULT_CONN_KEEP_ALIVE, false, false, false, null,
- null);
+ null,null);
}
RequestConfig(
@@ -99,7 +102,8 @@ protected RequestConfig() {
final boolean hardCancellationEnabled,
final boolean protocolUpgradeEnabled,
final Path unixDomainSocket,
- final PriorityValue h2Priority) {
+ final PriorityValue h2Priority,
+ final Timeout requestTimeout) {
super();
this.expectContinueEnabled = expectContinueEnabled;
this.proxy = proxy;
@@ -119,6 +123,7 @@ protected RequestConfig() {
this.protocolUpgradeEnabled = protocolUpgradeEnabled;
this.unixDomainSocket = unixDomainSocket;
this.h2Priority = h2Priority;
+ this.requestTimeout = requestTimeout;
}
/**
@@ -252,6 +257,22 @@ public PriorityValue getH2Priority() {
return h2Priority;
}
+ /**
+ * Returns the hard end-to-end request timeout (call timeout / request deadline).
+ * The entire exchange must complete within this budget or the execution is aborted.
+ *
+ * This timeout is independent of {@linkplain #getConnectTimeout() connect} and
+ * {@linkplain #getResponseTimeout() response} timeouts. Pass
+ * {@link org.apache.hc.core5.util.Timeout#DISABLED} to disable.
+ *
+ *
+ * @return the configured request timeout, or {@code null} if not explicitly set
+ * @since 5.6
+ */
+ public Timeout getRequestTimeout() {
+ return requestTimeout;
+ }
+
@Override
protected RequestConfig clone() throws CloneNotSupportedException {
return (RequestConfig) super.clone();
@@ -279,6 +300,7 @@ public String toString() {
builder.append(", protocolUpgradeEnabled=").append(protocolUpgradeEnabled);
builder.append(", unixDomainSocket=").append(unixDomainSocket);
builder.append(", h2Priority=").append(h2Priority);
+ builder.append(", requestTimeout=").append(requestTimeout);
builder.append("]");
return builder.toString();
}
@@ -306,7 +328,9 @@ public static RequestConfig.Builder copy(final RequestConfig config) {
.setHardCancellationEnabled(config.isHardCancellationEnabled())
.setProtocolUpgradeEnabled(config.isProtocolUpgradeEnabled())
.setUnixDomainSocket(config.getUnixDomainSocket())
- .setH2Priority(config.getH2Priority());
+ .setH2Priority(config.getH2Priority())
+ .setRequestTimeout(config.getRequestTimeout());
+
}
public static class Builder {
@@ -329,6 +353,7 @@ public static class Builder {
private boolean protocolUpgradeEnabled;
private Path unixDomainSocket;
private PriorityValue h2Priority;
+ private Timeout requestTimeout;
Builder() {
super();
@@ -696,6 +721,28 @@ public Builder setH2Priority(final PriorityValue priority) {
return this;
}
+ /**
+ * Sets the hard end-to-end request timeout (also known as call timeout or request
+ * deadline). When set, the entire request execution — from connection leasing
+ * through connection establishment, request write, and response processing —
+ * must complete within this time budget or the execution will be aborted.
+ *
+ * Pass {@link Timeout#DISABLED} to turn this feature off. If this value is
+ * left unset ({@code null}), no hard request timeout is applied unless a higher
+ * layer provides one. A non-positive timeout value is treated as an immediate
+ * expiry.
+ *
+ *
+ * @param requestTimeout the request timeout to apply; use {@code Timeout.DISABLED} to disable
+ * @return this builder
+ * @since 5.6
+ */
+ public Builder setRequestTimeout(final Timeout requestTimeout) {
+ this.requestTimeout = requestTimeout;
+ return this;
+ }
+
+
public RequestConfig build() {
return new RequestConfig(
expectContinueEnabled,
@@ -715,7 +762,8 @@ public RequestConfig build() {
hardCancellationEnabled,
protocolUpgradeEnabled,
unixDomainSocket,
- h2Priority);
+ h2Priority,
+ requestTimeout);
}
}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalHttpAsyncExecRuntime.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalHttpAsyncExecRuntime.java
index 28d59abaf4..8bccadbdba 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalHttpAsyncExecRuntime.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalHttpAsyncExecRuntime.java
@@ -78,6 +78,8 @@ static class ReUseData {
private final AtomicReference endpointRef;
private final AtomicReference reuseDataRef;
+ private volatile long deadlineMillis;
+
InternalHttpAsyncExecRuntime(
final Logger log,
final AsyncClientConnectionManager manager,
@@ -92,6 +94,7 @@ static class ReUseData {
this.tlsConfig = tlsConfig;
this.endpointRef = new AtomicReference<>();
this.reuseDataRef = new AtomicReference<>();
+ this.deadlineMillis = 0L;
}
@Override
@@ -99,6 +102,29 @@ public boolean isEndpointAcquired() {
return endpointRef.get() != null;
}
+ private Timeout resolveTimeout(final Timeout stageTimeout, final HttpClientContext context) throws InterruptedIOException {
+ final RequestConfig requestConfig = context.getRequestConfigOrDefault();
+ final Timeout requestTimeout = requestConfig.getRequestTimeout();
+ if (requestTimeout == null || requestTimeout.isDisabled()) {
+ return stageTimeout;
+ }
+ final long now = System.currentTimeMillis();
+ long deadline = deadlineMillis;
+ if (deadline == 0L) {
+ deadline = now + requestTimeout.toMilliseconds();
+ deadlineMillis = deadline;
+ }
+ final long remainingMillis = deadline - now;
+ if (remainingMillis <= 0L) {
+ throw new InterruptedIOException("Request timeout");
+ }
+ if (stageTimeout == null || stageTimeout.isDisabled()) {
+ return Timeout.ofMilliseconds(remainingMillis);
+ }
+ final long stageMillis = stageTimeout.toMilliseconds();
+ return Timeout.ofMilliseconds(Math.min(stageMillis, remainingMillis));
+ }
+
@Override
public Cancellable acquireEndpoint(
final String id,
@@ -108,7 +134,14 @@ public Cancellable acquireEndpoint(
final FutureCallback callback) {
if (endpointRef.get() == null) {
final RequestConfig requestConfig = context.getRequestConfigOrDefault();
- final Timeout connectionRequestTimeout = requestConfig.getConnectionRequestTimeout();
+ final Timeout configuredTimeout = requestConfig.getConnectionRequestTimeout();
+ final Timeout connectionRequestTimeout;
+ try {
+ connectionRequestTimeout = resolveTimeout(configuredTimeout, context);
+ } catch (final InterruptedIOException ex) {
+ callback.failed(ex);
+ return Operations.nonCancellable();
+ }
if (log.isDebugEnabled()) {
log.debug("{} acquiring endpoint ({})", id, connectionRequestTimeout);
}
@@ -219,7 +252,14 @@ public Cancellable connectEndpoint(
}
final RequestConfig requestConfig = context.getRequestConfigOrDefault();
@SuppressWarnings("deprecation")
- final Timeout connectTimeout = requestConfig.getConnectTimeout();
+ final Timeout configuredConnectTimeout = requestConfig.getConnectTimeout();
+ final Timeout connectTimeout;
+ try {
+ connectTimeout = resolveTimeout(configuredConnectTimeout, context);
+ } catch (final InterruptedIOException ex) {
+ callback.failed(ex);
+ return Operations.nonCancellable();
+ }
if (log.isDebugEnabled()) {
log.debug("{} connecting endpoint ({})", ConnPoolSupport.getId(endpoint), connectTimeout);
}
@@ -291,8 +331,15 @@ public Cancellable execute(
log.debug("{} start execution {}", ConnPoolSupport.getId(endpoint), id);
}
final RequestConfig requestConfig = context.getRequestConfigOrDefault();
- final Timeout responseTimeout = requestConfig.getResponseTimeout();
- if (responseTimeout != null) {
+ final Timeout configuredResponseTimeout = requestConfig.getResponseTimeout();
+ final Timeout responseTimeout;
+ try {
+ responseTimeout = resolveTimeout(configuredResponseTimeout, context);
+ } catch (final InterruptedIOException ex) {
+ exchangeHandler.failed(ex);
+ return Operations.nonCancellable();
+ }
+ if (responseTimeout != null && !responseTimeout.isDisabled()) {
endpoint.setSocketTimeout(responseTimeout);
}
endpoint.execute(id, exchangeHandler, pushHandlerFactory, context);
@@ -311,7 +358,15 @@ public void completed(final AsyncExecRuntime runtime) {
log.debug("{} start execution {}", ConnPoolSupport.getId(endpoint), id);
}
try {
+ final RequestConfig requestConfig = context.getRequestConfigOrDefault();
+ final Timeout configuredResponseTimeout = requestConfig.getResponseTimeout();
+ final Timeout responseTimeout = resolveTimeout(configuredResponseTimeout, context);
+ if (responseTimeout != null && !responseTimeout.isDisabled()) {
+ endpoint.setSocketTimeout(responseTimeout);
+ }
endpoint.execute(id, exchangeHandler, pushHandlerFactory, context);
+ } catch (final InterruptedIOException ex) {
+ exchangeHandler.failed(ex);
} catch (final RuntimeException ex) {
failed(ex);
}
@@ -344,7 +399,10 @@ public void markConnectionNonReusable() {
@Override
public AsyncExecRuntime fork() {
- return new InternalHttpAsyncExecRuntime(log, manager, connectionInitiator, pushHandlerFactory, tlsConfig);
+ final InternalHttpAsyncExecRuntime clone =
+ new InternalHttpAsyncExecRuntime(log, manager, connectionInitiator, pushHandlerFactory, tlsConfig);
+ clone.deadlineMillis = this.deadlineMillis;
+ return clone;
}
}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/InternalExecRuntime.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/InternalExecRuntime.java
index 7813bc171d..c1b78f36fc 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/InternalExecRuntime.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/InternalExecRuntime.java
@@ -28,6 +28,7 @@
package org.apache.hc.client5.http.impl.classic;
import java.io.IOException;
+import java.io.InterruptedIOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
@@ -68,11 +69,22 @@ class InternalExecRuntime implements ExecRuntime, Cancellable {
private volatile Object state;
private volatile TimeValue validDuration;
+ private final long deadlineMillis;
+
InternalExecRuntime(
final Logger log,
final HttpClientConnectionManager manager,
final HttpRequestExecutor requestExecutor,
final CancellableDependency cancellableDependency) {
+ this(log, manager, requestExecutor, cancellableDependency, null);
+ }
+
+ InternalExecRuntime(
+ final Logger log,
+ final HttpClientConnectionManager manager,
+ final HttpRequestExecutor requestExecutor,
+ final CancellableDependency cancellableDependency,
+ final Timeout requestTimeout) {
super();
this.log = log;
this.manager = manager;
@@ -80,6 +92,12 @@ class InternalExecRuntime implements ExecRuntime, Cancellable {
this.cancellableDependency = cancellableDependency;
this.endpointRef = new AtomicReference<>();
this.validDuration = TimeValue.NEG_ONE_MILLISECOND;
+ if (requestTimeout != null && !requestTimeout.isDisabled()) {
+ final long now = System.currentTimeMillis();
+ this.deadlineMillis = now + requestTimeout.toMilliseconds();
+ } else {
+ this.deadlineMillis = 0L;
+ }
}
@Override
@@ -92,13 +110,30 @@ public boolean isEndpointAcquired() {
return endpointRef.get() != null;
}
+ private Timeout resolveTimeout(final Timeout stageTimeout) throws InterruptedIOException {
+ if (deadlineMillis == 0L) {
+ return stageTimeout;
+ }
+ final long now = System.currentTimeMillis();
+ final long remainingMillis = deadlineMillis - now;
+ if (remainingMillis <= 0L) {
+ throw new InterruptedIOException("Request timeout");
+ }
+ if (stageTimeout == null || stageTimeout.isDisabled()) {
+ return Timeout.ofMilliseconds(remainingMillis);
+ }
+ final long stageMillis = stageTimeout.toMilliseconds();
+ return Timeout.ofMilliseconds(Math.min(stageMillis, remainingMillis));
+ }
+
@Override
public void acquireEndpoint(
final String id, final HttpRoute route, final Object object, final HttpClientContext context) throws IOException {
Args.notNull(route, "Route");
if (endpointRef.get() == null) {
final RequestConfig requestConfig = context.getRequestConfigOrDefault();
- final Timeout connectionRequestTimeout = requestConfig.getConnectionRequestTimeout();
+ final Timeout configuredTimeout = requestConfig.getConnectionRequestTimeout();
+ final Timeout connectionRequestTimeout = resolveTimeout(configuredTimeout);
if (log.isDebugEnabled()) {
log.debug("{} acquiring endpoint ({})", id, connectionRequestTimeout);
}
@@ -157,7 +192,8 @@ private void connectEndpoint(final ConnectionEndpoint endpoint, final HttpClient
}
final RequestConfig requestConfig = context.getRequestConfigOrDefault();
@SuppressWarnings("deprecation")
- final Timeout connectTimeout = requestConfig.getConnectTimeout();
+ final Timeout configuredConnectTimeout = requestConfig.getConnectTimeout();
+ final Timeout connectTimeout = resolveTimeout(configuredConnectTimeout);
if (log.isDebugEnabled()) {
log.debug("{} connecting endpoint ({})", ConnPoolSupport.getId(endpoint), connectTimeout);
}
@@ -223,8 +259,9 @@ public ClassicHttpResponse execute(
throw new RequestFailedException("Request aborted");
}
final RequestConfig requestConfig = context.getRequestConfigOrDefault();
- final Timeout responseTimeout = requestConfig.getResponseTimeout();
- if (responseTimeout != null) {
+ final Timeout configuredResponseTimeout = requestConfig.getResponseTimeout();
+ final Timeout responseTimeout = resolveTimeout(configuredResponseTimeout);
+ if (responseTimeout != null && !responseTimeout.isDisabled()) {
endpoint.setSocketTimeout(responseTimeout);
}
if (log.isDebugEnabled()) {
@@ -305,8 +342,18 @@ public boolean cancel() {
}
@Override
- public ExecRuntime fork(final CancellableDependency cancellableDependency) {
- return new InternalExecRuntime(log, manager, requestExecutor, cancellableDependency);
+ public ExecRuntime fork(final CancellableDependency newDependency) {
+ final Timeout remainingTimeout;
+ if (deadlineMillis == 0L) {
+ remainingTimeout = null;
+ } else {
+ final long now = System.currentTimeMillis();
+ final long remainingMillis = deadlineMillis - now;
+ remainingTimeout = remainingMillis > 0L
+ ? Timeout.ofMilliseconds(remainingMillis)
+ : Timeout.ZERO_MILLISECONDS;
+ }
+ return new InternalExecRuntime(log, manager, requestExecutor, newDependency, remainingTimeout);
}
}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/InternalHttpClient.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/InternalHttpClient.java
index 07265fcb4b..82918c9c42 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/InternalHttpClient.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/InternalHttpClient.java
@@ -29,6 +29,7 @@
import java.io.Closeable;
import java.io.IOException;
+import java.io.InterruptedIOException;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Function;
@@ -56,7 +57,6 @@
import org.apache.hc.core5.http.ClassicHttpResponse;
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.config.Lookup;
import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
@@ -65,18 +65,10 @@
import org.apache.hc.core5.io.ModalCloseable;
import org.apache.hc.core5.net.URIAuthority;
import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-/**
- * Internal implementation of {@link CloseableHttpClient}.
- *
- * Concurrent message exchanges executed by this client will get assigned to
- * separate connections leased from the connection pool.
- *
- *
- * @since 4.3
- */
@Contract(threading = ThreadingBehavior.SAFE_CONDITIONAL)
@Internal
class InternalHttpClient extends CloseableHttpClient implements Configurable {
@@ -95,7 +87,7 @@ class InternalHttpClient extends CloseableHttpClient implements Configurable {
private final RequestConfig defaultConfig;
private final ConcurrentLinkedQueue closeables;
- public InternalHttpClient(
+ InternalHttpClient(
final HttpClientConnectionManager connManager,
final HttpRequestExecutor requestExecutor,
final ExecChainElement execChain,
@@ -121,7 +113,10 @@ public InternalHttpClient(
this.closeables = closeables != null ? new ConcurrentLinkedQueue<>(closeables) : null;
}
- private HttpRoute determineRoute(final HttpHost target, final HttpRequest request, final HttpContext context) throws HttpException {
+ private HttpRoute determineRoute(
+ final HttpHost target,
+ final org.apache.hc.core5.http.HttpRequest request,
+ final HttpContext context) throws HttpException {
return this.routePlanner.determineRoute(target, request, context);
}
@@ -151,6 +146,7 @@ protected CloseableHttpResponse doExecute(
Args.notNull(request, "HTTP request");
try {
final HttpClientContext localcontext = contextAdaptor.apply(context);
+
RequestConfig config = null;
if (request instanceof Configurable) {
config = ((Configurable) request).getConfig();
@@ -169,21 +165,32 @@ protected CloseableHttpResponse doExecute(
request.setAuthority(new URIAuthority(resolvedTarget));
}
}
- final HttpRoute route = determineRoute(
- resolvedTarget,
- request,
- localcontext);
+ final HttpRoute route = determineRoute(resolvedTarget, request, localcontext);
final String exchangeId = ExecSupport.getNextExchangeId();
localcontext.setExchangeId(exchangeId);
if (LOG.isDebugEnabled()) {
LOG.debug("{} preparing request execution", exchangeId);
}
- final ExecRuntime execRuntime = new InternalExecRuntime(LOG, connManager, requestExecutor,
- request instanceof CancellableDependency ? (CancellableDependency) request : null);
+ final RequestConfig effectiveCfg = localcontext.getRequestConfig();
+ final Timeout requestTimeout = effectiveCfg != null ? effectiveCfg.getRequestTimeout() : null;
+
+ final ExecRuntime execRuntime = new InternalExecRuntime(
+ LOG,
+ connManager,
+ requestExecutor,
+ request instanceof CancellableDependency ? (CancellableDependency) request : null,
+ requestTimeout);
+
final ExecChain.Scope scope = new ExecChain.Scope(exchangeId, route, request, execRuntime, localcontext);
- final ClassicHttpResponse response = this.execChain.execute(ClassicRequestBuilder.copy(request).build(), scope);
+
+ final ClassicHttpResponse response =
+ this.execChain.execute(ClassicRequestBuilder.copy(request).build(), scope);
return CloseableHttpResponse.adapt(response);
+
+ } catch (final InterruptedIOException ioEx) {
+ // treat our deadline violations as I/O timeouts
+ throw ioEx;
} catch (final HttpException httpException) {
throw new ClientProtocolException(httpException.getMessage(), httpException);
}
@@ -216,5 +223,4 @@ public void close(final CloseMode closeMode) {
}
}
}
-
}
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientRequestTimeoutExample.java b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientRequestTimeoutExample.java
new file mode 100644
index 0000000000..4477503c08
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientRequestTimeoutExample.java
@@ -0,0 +1,145 @@
+/*
+ * ====================================================================
+ * 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.examples;
+
+import java.io.InterruptedIOException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+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.RequestConfig;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.message.StatusLine;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.util.Timeout;
+
+/**
+ * Demonstrates per-request hard end-to-end timeout (query timeout / request deadline).
+ */
+public class AsyncClientRequestTimeoutExample {
+
+ public static void main(final String[] args) throws Exception {
+
+ // No default requestTimeout at the client level (leave it opt-in per request).
+ try (final CloseableHttpAsyncClient client = HttpAsyncClients.custom().build()) {
+
+ client.start();
+
+ final HttpHost host = new HttpHost("https", "httpbin.org");
+
+ // 1) This one should TIME OUT (server delays ~5s, our requestTimeout is 2s)
+ final SimpleHttpRequest willTimeout = SimpleRequestBuilder.get()
+ .setHttpHost(host)
+ .setPath("/delay/5")
+ .build();
+ willTimeout.setConfig(RequestConfig.custom()
+ .setRequestTimeout(Timeout.ofSeconds(2))
+ .build());
+
+ System.out.println("Executing (expected timeout): " + willTimeout);
+
+ final Future f1 = client.execute(
+ SimpleRequestProducer.create(willTimeout),
+ SimpleResponseConsumer.create(),
+ new FutureCallback() {
+ @Override
+ public void completed(final SimpleHttpResponse response) {
+ System.out.println(willTimeout + " -> " + new StatusLine(response));
+ System.out.println(response.getBodyText());
+ }
+
+ @Override
+ public void failed(final Exception ex) {
+ System.out.println(willTimeout + " -> FAILED: " + ex);
+ if (ex instanceof InterruptedIOException) {
+ System.out.println("As expected: hard request timeout triggered.");
+ }
+ }
+
+ @Override
+ public void cancelled() {
+ System.out.println(willTimeout + " -> CANCELLED");
+ }
+ });
+
+ try {
+ f1.get(); // Will throw ExecutionException wrapping InterruptedIOException
+ } catch (final ExecutionException ee) {
+ final Throwable cause = ee.getCause();
+ if (cause instanceof InterruptedIOException) {
+ System.out.println("Future failed with InterruptedIOException (OK): " + cause.getMessage());
+ } else {
+ System.out.println("Unexpected failure type: " + cause);
+ }
+ }
+
+ // 2) This one should SUCCEED (server delays ~1s, our requestTimeout is 3s)
+ final SimpleHttpRequest willSucceed = SimpleRequestBuilder.get()
+ .setHttpHost(host)
+ .setPath("/delay/1")
+ .build();
+ willSucceed.setConfig(RequestConfig.custom()
+ .setRequestTimeout(Timeout.ofSeconds(3)) // <--- longer budget
+ .build());
+
+ System.out.println("Executing (expected success): " + willSucceed);
+
+ final Future f2 = client.execute(
+ SimpleRequestProducer.create(willSucceed),
+ SimpleResponseConsumer.create(),
+ new FutureCallback() {
+ @Override
+ public void completed(final SimpleHttpResponse response) {
+ System.out.println(willSucceed + " -> " + new StatusLine(response));
+ System.out.println(response.getBodyText());
+ }
+
+ @Override
+ public void failed(final Exception ex) {
+ System.out.println(willSucceed + " -> FAILED: " + ex);
+ }
+
+ @Override
+ public void cancelled() {
+ System.out.println(willSucceed + " -> CANCELLED");
+ }
+ });
+
+ f2.get(); // Should complete normally
+
+ System.out.println("Shutting down");
+ client.close(CloseMode.GRACEFUL);
+ }
+ }
+}
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/examples/ClassicClientCallTimeoutExample.java b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/ClassicClientCallTimeoutExample.java
new file mode 100644
index 0000000000..75a773316c
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/ClassicClientCallTimeoutExample.java
@@ -0,0 +1,91 @@
+/*
+ * ====================================================================
+ * 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.examples;
+
+import java.io.IOException;
+
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.io.HttpClientResponseHandler;
+import org.apache.hc.core5.util.Timeout;
+
+public class ClassicClientCallTimeoutExample {
+
+ public static void main(final String[] args) throws Exception {
+
+ // Non-deprecated: set connect/socket timeouts via ConnectionConfig
+ final PoolingHttpClientConnectionManager cm =
+ PoolingHttpClientConnectionManagerBuilder.create()
+ .setDefaultConnectionConfig(
+ ConnectionConfig.custom()
+ .setConnectTimeout(Timeout.ofSeconds(10))
+ .setSocketTimeout(Timeout.ofSeconds(10))
+ .build())
+ .build();
+
+ try (final CloseableHttpClient client = HttpClients.custom()
+ .setConnectionManager(cm)
+ .build()) {
+
+ // ---- Expected TIMEOUT (hard call deadline) ----
+ final HttpGet slow = new HttpGet("https://httpbin.org/delay/5");
+ slow.setConfig(RequestConfig.custom()
+ .setRequestTimeout(Timeout.ofSeconds(2)) // hard end-to-end cap
+ .setConnectionRequestTimeout(Timeout.ofSeconds(3)) // don't hang on pool lease
+ .build());
+
+ final HttpClientResponseHandler handler = (ClassicHttpResponse response) -> {
+ return response.getCode() + " " + response.getReasonPhrase();
+ };
+
+ System.out.println("Executing (expected timeout): " + slow.getPath());
+ try {
+ client.execute(slow, handler); // will throw by design
+ System.out.println("UNEXPECTED: completed");
+ } catch (final IOException ex) {
+ System.out.println("As expected: " + ex.getClass().getSimpleName() + " - " + ex.getMessage());
+ }
+
+ // ---- Expected SUCCESS within budget (use HTTP to avoid TLS variance) ----
+ final HttpGet fast = new HttpGet("http://httpbin.org/delay/1"); // HTTP on purpose
+ fast.setConfig(RequestConfig.custom()
+ .setRequestTimeout(Timeout.ofSeconds(8)) // generous end-to-end budget
+ .setConnectionRequestTimeout(Timeout.ofSeconds(2)) // quick fail if pool stuck
+ .build());
+
+ System.out.println("Executing (expected success): " + fast.getPath());
+ final String ok = client.execute(fast, handler);
+ System.out.println("OK: " + ok);
+ }
+ }
+}