You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2025/07/08 15:09:38 UTC

(httpcomponents-client) branch master updated: HTTPCLIENT-2141: revision of the original implementation / better debug logging / additional integration tests

This is an automated email from the ASF dual-hosted git repository.

olegk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git


The following commit(s) were added to refs/heads/master by this push:
     new e02c106d5 HTTPCLIENT-2141: revision of the original implementation / better debug logging / additional integration tests
e02c106d5 is described below

commit e02c106d54e5ca27d146579af96dc458de7cc01d
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Sat Jul 5 17:40:03 2025 +0200

    HTTPCLIENT-2141: revision of the original implementation / better debug logging / additional integration tests
---
 .../classic/ServiceUnavailableDecorator.java       |  80 ++++++++++++++
 .../client5/testing/sync/HttpIntegrationTests.java |  20 ++++
 .../testing/sync/TestClientRequestReExecution.java | 118 +++++++++++++++++++++
 .../hc/client5/http/config/RequestConfig.java      |   6 ++
 .../http/impl/DefaultHttpRequestRetryStrategy.java |  14 ++-
 .../http/impl/async/AsyncHttpRequestRetryExec.java |  15 ++-
 .../http/impl/classic/HttpRequestRetryExec.java    |  86 ++++++---------
 .../impl/TestDefaultHttpRequestRetryStrategy.java  |  28 ++++-
 .../impl/classic/TestHttpRequestRetryExec.java     |  33 ------
 9 files changed, 307 insertions(+), 93 deletions(-)

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 000000000..3a00cbfd5
--- /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
+ * <http://www.apache.org/>.
+ *
+ */
+
+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<HttpRequest, TimeValue> serviceAvailabilityResolver;
+    private final AtomicBoolean serviceUnavailable;
+
+    public ServiceUnavailableDecorator(final HttpServerRequestHandler requestHandler,
+                                       final Resolver<HttpRequest, TimeValue> 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/sync/HttpIntegrationTests.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/HttpIntegrationTests.java
index f162795f6..9b0b191f7 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 000000000..30b36b82d
--- /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
+ * <http://www.apache.org/>.
+ *
+ */
+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<HttpRequest, TimeValue> serviceAvailabilityResolver = new Resolver<HttpRequest, TimeValue>() {
+
+            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/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 54ce77634..b18a7b122 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
@@ -531,6 +531,12 @@ public Builder setConnectTimeout(final long connectTimeout, final TimeUnit timeU
          * HTTP transports with message multiplexing.
          * </p>
          * <p>
+         * 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.
+         * </p>
+         * <p>
          * Default: {@code null}
          * </p>
          *
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 b8dfcb535..47c9bab56 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<Class<? extends IOException>> clazzes,
             final Collection<Integer> 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/async/AsyncHttpRequestRetryExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncHttpRequestRetryExec.java
index 292b6ed07..90beadff7 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
@@ -126,9 +126,6 @@ public AsyncDataConsumer handleResponse(
                 state.retrying = retryStrategy.retryRequest(response, scope.execCount.get(), clientContext);
                 if (state.retrying) {
                     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);
@@ -146,13 +143,17 @@ public void completed() {
                     if (entityProducer != null) {
                        entityProducer.releaseResources();
                     }
+                    final TimeValue delay = TimeValue.isPositive(state.delay) ? state.delay : TimeValue.ZERO_MILLISECONDS;
+                    if (LOG.isDebugEnabled()) {
+                        LOG.debug("{} wait for {}", exchangeId, delay);
+                    }
                     scope.scheduler.scheduleExecution(
                             request,
                             entityProducer,
                             scope,
                             (r, e, s, c) -> execute(r, e, s, chain, c),
                             asyncExecCallback,
-                            state.delay);
+                            delay);
                 } else {
                     asyncExecCallback.completed();
                 }
@@ -182,13 +183,17 @@ 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.isDebugEnabled()) {
+                            LOG.debug("{} wait for {}", exchangeId, delay);
+                        }
                         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/classic/HttpRequestRetryExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpRequestRetryExec.java
index 2ab32b664..2e0021085 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;
@@ -48,7 +47,6 @@
 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;
 
@@ -108,9 +106,28 @@ public ClassicHttpResponse execute(
         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);
+                        pause(exchangeId, 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");
@@ -130,18 +147,8 @@ public ClassicHttpResponse execute(
                         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();
-                        }
-                    }
+                    final TimeValue delay = retryStrategy.getRetryInterval(request, ex, execCount, context);
+                    pause(exchangeId, delay);
                     currentRequest = ClassicRequestBuilder.copy(scope.originalRequest).build();
                     continue;
                 }
@@ -153,44 +160,19 @@ public ClassicHttpResponse execute(
                 }
                 throw ex;
             }
+        }
+    }
 
+    private static void pause(final String exchangeId, final TimeValue delay) throws InterruptedIOException {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("{} wait for {}", exchangeId, delay);
+        }
+        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/test/java/org/apache/hc/client5/http/impl/TestDefaultHttpRequestRetryStrategy.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestDefaultHttpRequestRetryStrategy.java
index 2440f5c98..4c2649b52 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/classic/TestHttpRequestRetryExec.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestHttpRequestRetryExec.java
index 5672587c5..006ef9a50 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);