You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by mi...@apache.org on 2019/12/09 11:58:53 UTC

[httpcomponents-client] branch HTTPCLIENT-2034 updated (ce74867 -> 7df52dc)

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

michaelo pushed a change to branch HTTPCLIENT-2034
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git.


 discard ce74867  Add note about retry interval
 discard 795ba2a  Use log#is..Enabled()
 discard 815c3c6  Corrected async retry exec handling logic
    omit 50a1276  Fix issues
    omit 0d6419f  Return null on async retry
    omit 6c4a721  Async Builders
    omit 0477834  Use HttpRequestRetryHandler if set
    omit 8ea9c87  Modify testing
    omit 1f61310  Make it final
    omit e1e4e7c  Add second test
    omit 11c2d0b  Add first test
    omit 1880653  First draft
     add d054112  Fixed typo in method name (no functional changes)
     add f3c418c  CloseableHttpAsyncClient to support explicit HttpHost execution parameter
     add 8602215  Upgraded Conscrypt dependency to version 2.2.1
     add 070f30f  Fixed session i/o and wire logging in async clients
     new 7df52dc  HTTPCLIENT-2034: Introduce HttpRequestRetryStrategy

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (ce74867)
            \
             N -- N -- N   refs/heads/HTTPCLIENT-2034 (7df52dc)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../hc/client5/http/HttpRequestRetryStrategy.java  |  10 +-
 .../http/impl/DefaultHttpRequestRetryStrategy.java |   4 +-
 .../async/AbstractMinimalHttpAsyncClientBase.java  |   4 +-
 .../http/impl/async/AsyncHttpRequestRetryExec.java |   7 +-
 .../http/impl/async/CloseableHttpAsyncClient.java  |  38 ++++++
 .../http/impl/async/H2AsyncClientBuilder.java      |   2 +-
 .../async/H2AsyncClientEventHandlerFactory.java    |  12 +-
 .../http/impl/async/HttpAsyncClientBuilder.java    |   2 +-
 .../async/HttpAsyncClientEventHandlerFactory.java  |  12 +-
 .../async/InternalAbstractHttpAsyncClient.java     |  17 ++-
 .../http/impl/async/InternalH2AsyncClient.java     |   9 +-
 .../http/impl/async/InternalHttpAsyncClient.java   |   9 +-
 .../client5/http/impl/async/LoggingIOSession.java  | 129 ++++++---------------
 ...allback.java => LoggingIOSessionDecorator.java} |  21 ++--
 .../http/impl/async/MinimalH2AsyncClient.java      |   2 +-
 .../http/impl/async/MinimalHttpAsyncClient.java    |   2 +-
 .../http/impl/classic/CloseableHttpClient.java     |  21 +---
 .../http/impl/classic/HttpRequestRetryExec.java    |   4 +-
 .../nio/DefaultManagedAsyncClientConnection.java   |   3 +-
 .../hc/client5/http/routing/RoutingSupport.java    |  16 +++
 pom.xml                                            |   2 +-
 21 files changed, 153 insertions(+), 173 deletions(-)
 copy httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/{LoggingExceptionCallback.java => LoggingIOSessionDecorator.java} (64%)


[httpcomponents-client] 01/01: HTTPCLIENT-2034: Introduce HttpRequestRetryStrategy

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 7df52dc611c28fdd5520d256b13b8300e648c56d
Author: Michael Osipov <mi...@apache.org>
AuthorDate: Thu Dec 5 17:57:52 2019 +0100

    HTTPCLIENT-2034: Introduce HttpRequestRetryStrategy
---
 .../testing/sync/TestClientRequestExecution.java   |  44 +++-
 .../hc/client5/http/HttpRequestRetryStrategy.java  |  90 +++++++
 .../apache/hc/client5/http/impl/ChainElements.java |   5 +-
 .../http/impl/DefaultHttpRequestRetryStrategy.java | 234 ++++++++++++++++++
 .../http/impl/async/AsyncHttpRequestRetryExec.java | 187 ++++++++++++++
 .../http/impl/async/H2AsyncClientBuilder.java      |  34 ++-
 .../http/impl/async/HttpAsyncClientBuilder.java    |  34 ++-
 .../http/impl/classic/HttpClientBuilder.java       |  35 ++-
 .../http/impl/classic/HttpRequestRetryExec.java    | 160 ++++++++++++
 .../impl/TestDefaultHttpRequestRetryStrategy.java  | 158 ++++++++++++
 .../impl/classic/TestHttpRequestRetryExec.java     | 269 +++++++++++++++++++++
 11 files changed, 1222 insertions(+), 28 deletions(-)

diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientRequestExecution.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientRequestExecution.java
index d56a6be..d9be97e 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientRequestExecution.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientRequestExecution.java
@@ -30,7 +30,7 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.net.URI;
 
-import org.apache.hc.client5.http.HttpRequestRetryHandler;
+import org.apache.hc.client5.http.HttpRequestRetryStrategy;
 import org.apache.hc.client5.http.classic.methods.HttpGet;
 import org.apache.hc.client5.http.classic.methods.HttpPost;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
@@ -44,6 +44,7 @@ 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.HttpRequestInterceptor;
+import org.apache.hc.core5.http.HttpResponse;
 import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
 import org.apache.hc.core5.http.io.HttpClientConnection;
@@ -54,6 +55,7 @@ import org.apache.hc.core5.http.io.entity.StringEntity;
 import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
 import org.apache.hc.core5.http.protocol.HttpContext;
 import org.apache.hc.core5.net.URIBuilder;
+import org.apache.hc.core5.util.TimeValue;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -122,7 +124,7 @@ public class TestClientRequestExecution extends LocalServerTestBase {
 
         };
 
-        final HttpRequestRetryHandler requestRetryHandler = new HttpRequestRetryHandler() {
+        final HttpRequestRetryStrategy requestRetryStrategy = new HttpRequestRetryStrategy() {
 
             @Override
             public boolean retryRequest(
@@ -133,12 +135,28 @@ public class TestClientRequestExecution extends LocalServerTestBase {
                 return true;
             }
 
+            @Override
+            public boolean retryRequest(
+                    final HttpResponse response,
+                    final int executionCount,
+                    final HttpContext context) {
+                return false;
+            }
+
+            @Override
+            public TimeValue getRetryInterval(
+                    final HttpResponse response,
+                    final int executionCount,
+                    final HttpContext context) {
+                return TimeValue.ofSeconds(1L);
+            }
+
         };
 
         this.httpclient = this.clientBuilder
             .addRequestInterceptorFirst(interceptor)
             .setRequestExecutor(new FaultyHttpRequestExecutor("Oppsie"))
-            .setRetryHandler(requestRetryHandler)
+            .setRetryStrategy(requestRetryStrategy)
             .build();
 
         final HttpHost target = start();
@@ -163,7 +181,7 @@ public class TestClientRequestExecution extends LocalServerTestBase {
     public void testNonRepeatableEntity() throws Exception {
         this.server.registerHandler("*", new SimpleService());
 
-        final HttpRequestRetryHandler requestRetryHandler = new HttpRequestRetryHandler() {
+        final HttpRequestRetryStrategy requestRetryStrategy = new HttpRequestRetryStrategy() {
 
             @Override
             public boolean retryRequest(
@@ -174,11 +192,27 @@ public class TestClientRequestExecution extends LocalServerTestBase {
                 return true;
             }
 
+            @Override
+            public boolean retryRequest(
+                    final HttpResponse response,
+                    final int executionCount,
+                    final HttpContext context) {
+                return false;
+            }
+
+            @Override
+            public TimeValue getRetryInterval(
+                    final HttpResponse response,
+                    final int executionCount,
+                    final HttpContext context) {
+                return TimeValue.ofSeconds(1L);
+            }
+
         };
 
         this.httpclient = this.clientBuilder
             .setRequestExecutor(new FaultyHttpRequestExecutor("a message showing that this failed"))
-            .setRetryHandler(requestRetryHandler)
+            .setRetryStrategy(requestRetryStrategy)
             .build();
 
         final HttpHost target = start();
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/HttpRequestRetryStrategy.java b/httpclient5/src/main/java/org/apache/hc/client5/http/HttpRequestRetryStrategy.java
new file mode 100644
index 0000000..8614090
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/HttpRequestRetryStrategy.java
@@ -0,0 +1,90 @@
+/*
+ * ====================================================================
+ * 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.http;
+
+import java.io.IOException;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.util.TimeValue;
+
+/**
+ * Strategy interface that allows API users to plug in their own logic to
+ * control whether or not a retry should automatically be done, how many times
+ * it should be done and so on.
+ *
+ * @since 5.0
+ */
+@Contract(threading = ThreadingBehavior.STATELESS)
+public interface HttpRequestRetryStrategy {
+
+    /**
+     * Determines if a method should be retried after an I/O exception
+     * occured during execution.
+     *
+     * @param request the request failed due to an I/O exception
+     * @param exception the exception that occurred
+     * @param execCount the number of times this method has been
+     *                  unsuccessfully executed
+     * @param context the context for the request execution
+     *
+     * @return {@code true} if the request should be retried, {@code false}
+     *         otherwise
+     */
+    boolean retryRequest(HttpRequest request, IOException exception, int execCount, HttpContext context);
+
+    /**
+     * Determines if a method should be retried given the response from
+     * the target server.
+     *
+     * @param response the response from the target server
+     * @param execCount the number of times this method has been
+     *                  unsuccessfully executed
+     * @param context the context for the request execution
+     *
+     * @return {@code true} if the request should be retried, {@code false}
+     *         otherwise
+     */
+    boolean retryRequest(HttpResponse response, int execCount, HttpContext context);
+
+    /**
+     * Determines the retry interval between subsequent retries.
+     *
+     * @param response the response from the target server
+     * @param execCount the number of times this method has been
+     *                  unsuccessfully executed
+     * @param context the context for the request execution
+     *
+     * @return the retry interval between subsequent retries
+     */
+    TimeValue getRetryInterval(HttpResponse response, int execCount, HttpContext context);
+
+}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ChainElements.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ChainElements.java
index 9dd95cd..9f034f5 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ChainElements.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ChainElements.java
@@ -34,6 +34,7 @@ package org.apache.hc.client5.http.impl;
  */
 public enum ChainElements {
 
-    REDIRECT, BACK_OFF, RETRY_SERVICE_UNAVAILABLE, RETRY_IO_ERROR, CACHING, PROTOCOL, CONNECT, MAIN_TRANSPORT
+    REDIRECT, BACK_OFF, RETRY, RETRY_SERVICE_UNAVAILABLE, RETRY_IO_ERROR, CACHING, PROTOCOL,
+    CONNECT, MAIN_TRANSPORT
 
-}
\ No newline at end of file
+}
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
new file mode 100644
index 0000000..5ee192c
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultHttpRequestRetryStrategy.java
@@ -0,0 +1,234 @@
+/*
+ * ====================================================================
+ * 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.http.impl;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.ConnectException;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.net.ssl.SSLException;
+
+import org.apache.hc.client5.http.HttpRequestRetryStrategy;
+import org.apache.hc.client5.http.utils.DateUtils;
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.concurrent.CancellableDependency;
+import org.apache.hc.core5.http.ConnectionClosedException;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpHeaders;
+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.Methods;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.TimeValue;
+
+/**
+ * Default implementation of the {@link HttpRequestRetryStrategy} interface.
+ *
+ * @since 5.0
+ */
+@Contract(threading = ThreadingBehavior.STATELESS)
+public class DefaultHttpRequestRetryStrategy implements HttpRequestRetryStrategy {
+
+    public static final DefaultHttpRequestRetryStrategy INSTANCE = new DefaultHttpRequestRetryStrategy();
+
+    /**
+     * Maximum number of allowed retries
+     */
+    private final int maxRetries;
+
+    /**
+     * Retry interval between subsequent retries
+     */
+    private final TimeValue defaultRetryInterval;
+
+    /**
+     * Derived {@code IOExceptions} which shall not be retried
+     */
+    private final Set<Class<? extends IOException>> nonRetriableClasses;
+
+    /**
+     * HTTP status codes which shall be retried
+     */
+    private final Set<Integer> retriableCodes;
+
+    protected DefaultHttpRequestRetryStrategy(
+            final int maxRetries,
+            final TimeValue defaultRetryInterval,
+            final Collection<Class<? extends IOException>> clazzes,
+            final Collection<Integer> codes) {
+        Args.notNegative(maxRetries, "maxRetries");
+        Args.positive(defaultRetryInterval.getDuration(), "defaultRetryInterval");
+        this.maxRetries = maxRetries;
+        this.defaultRetryInterval = defaultRetryInterval;
+        this.nonRetriableClasses = new HashSet<>();
+        this.nonRetriableClasses.addAll(clazzes);
+        this.retriableCodes = new HashSet<>();
+        this.retriableCodes.addAll(codes);
+    }
+
+    /**
+     * Create the HTTP request retry strategy using the following list of
+     * non-retriable I/O exception classes:<br>
+     * <ul>
+     * <li>InterruptedIOException</li>
+     * <li>UnknownHostException</li>
+     * <li>ConnectException</li>
+     * <li>ConnectionClosedException</li>
+     * <li>SSLException</li>
+     * </ul>
+     *
+     * and retriable HTTP status codes:<br>
+     * <ul>
+     * <li>SC_TOO_MANY_REQUESTS (429)</li>
+     * <li>SC_SERVICE_UNAVAILABLE (503)</li>
+     * </ul>
+     *
+     * @param maxRetries how many times to retry; 0 means no retries
+     * @param defaultRetryInterval the default retry interval between
+     * subsequent retries if the {@code Retry-After} header is not set
+     * or invalid.
+     */
+    public DefaultHttpRequestRetryStrategy(
+            final int maxRetries,
+            final TimeValue defaultRetryInterval) {
+        this(maxRetries, defaultRetryInterval,
+                Arrays.asList(
+                        InterruptedIOException.class,
+                        UnknownHostException.class,
+                        ConnectException.class,
+                        ConnectionClosedException.class,
+                        SSLException.class),
+                Arrays.asList(
+                        HttpStatus.SC_TOO_MANY_REQUESTS,
+                        HttpStatus.SC_SERVICE_UNAVAILABLE));
+    }
+
+    /**
+     * Create the HTTP request retry strategy with a max retry count of 1,
+     * default retry interval of 1 second, and using the following list of
+     * non-retriable I/O exception classes:<br>
+     * <ul>
+     * <li>InterruptedIOException</li>
+     * <li>UnknownHostException</li>
+     * <li>ConnectException</li>
+     * <li>ConnectionClosedException</li>
+     * <li>SSLException</li>
+     * </ul>
+     *
+     * and retriable HTTP status codes:<br>
+     * <ul>
+     * <li>SC_TOO_MANY_REQUESTS (429)</li>
+     * <li>SC_SERVICE_UNAVAILABLE (503)</li>
+     * </ul>
+     */
+    public DefaultHttpRequestRetryStrategy() {
+        this(1, TimeValue.ofSeconds(1L));
+    }
+
+    @Override
+    public boolean retryRequest(
+            final HttpRequest request,
+            final IOException exception,
+            final int executionCount,
+            final HttpContext context) {
+        Args.notNull(request, "request");
+        Args.notNull(exception, "exception");
+
+        if (executionCount > this.maxRetries) {
+            // Do not retry if over max retries
+            return false;
+        }
+        if (this.nonRetriableClasses.contains(exception.getClass())) {
+            return false;
+        } else {
+            for (final Class<? extends IOException> rejectException : this.nonRetriableClasses) {
+                if (rejectException.isInstance(exception)) {
+                    return false;
+                }
+            }
+        }
+        if (request instanceof CancellableDependency && ((CancellableDependency) request).isCancelled()) {
+            return false;
+        }
+
+        // Retry if the request is considered idempotent
+        return handleAsIdempotent(request);
+    }
+
+    @Override
+    public boolean retryRequest(
+            final HttpResponse response,
+            final int executionCount,
+            final HttpContext context) {
+        Args.notNull(response, "response");
+
+        return executionCount <= this.maxRetries && retriableCodes.contains(response.getCode());
+    }
+
+    @Override
+    public TimeValue getRetryInterval(
+            final HttpResponse response,
+            final int executionCount,
+            final HttpContext context) {
+        Args.notNull(response, "response");
+
+        final Header header = response.getFirstHeader(HttpHeaders.RETRY_AFTER);
+        TimeValue retryAfter = null;
+        if (header != null) {
+            final String value = header.getValue();
+            try {
+                retryAfter = TimeValue.ofSeconds(Long.parseLong(value));
+            } catch (final NumberFormatException ignore) {
+                final Date retryAfterDate = DateUtils.parseDate(value);
+                if (retryAfterDate != null) {
+                    retryAfter =
+                            TimeValue.ofMilliseconds(retryAfterDate.getTime() - System.currentTimeMillis());
+                }
+            }
+
+            if (TimeValue.isPositive(retryAfter)) {
+                return retryAfter;
+            }
+        }
+        return this.defaultRetryInterval;
+    }
+
+    protected boolean handleAsIdempotent(final HttpRequest request) {
+        return Methods.isIdempotent(request.getMethod());
+    }
+
+}
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
new file mode 100644
index 0000000..f3c18c6
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncHttpRequestRetryExec.java
@@ -0,0 +1,187 @@
+/*
+ * ====================================================================
+ * 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.http.impl.async;
+
+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;
+import org.apache.hc.client5.http.impl.RequestCopier;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+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.EntityDetails;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.nio.AsyncDataConsumer;
+import org.apache.hc.core5.http.nio.AsyncEntityProducer;
+import org.apache.hc.core5.http.nio.entity.NoopEntityConsumer;
+import org.apache.hc.core5.util.Args;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Request executor in the asynchronous request execution chain that is
+ * responsible for making a decision whether a request that failed due to
+ * an I/O exception or received a specific response from the target server should
+ * be re-executed. Note that this exec chain handler <em>will not</em> respect
+ * {@link HttpRequestRetryStrategy#getRetryInterval(HttpResponse, int, org.apache.hc.core5.http.protocol.HttpContext)}.
+ * <p>
+ * Further responsibilities such as communication with the opposite
+ * endpoint is delegated to the next executor in the request execution
+ * chain.
+ * </p>
+ *
+ * @since 5.0
+ */
+@Contract(threading = ThreadingBehavior.STATELESS)
+@Internal
+public final class AsyncHttpRequestRetryExec implements AsyncExecChainHandler {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private final HttpRequestRetryStrategy retryStrategy;
+
+    public AsyncHttpRequestRetryExec(final HttpRequestRetryStrategy retryStrategy) {
+        Args.notNull(retryStrategy, "retryStrategy");
+        this.retryStrategy = retryStrategy;
+    }
+
+    private static class State {
+
+        volatile int execCount;
+        volatile boolean retrying;
+
+    }
+
+    private void internalExecute(
+            final State state,
+            final HttpRequest request,
+            final AsyncEntityProducer entityProducer,
+            final AsyncExecChain.Scope scope,
+            final AsyncExecChain chain,
+            final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
+
+        final String exchangeId = scope.exchangeId;
+
+        chain.proceed(RequestCopier.INSTANCE.copy(request), entityProducer, scope, new AsyncExecCallback() {
+
+            @Override
+            public AsyncDataConsumer handleResponse(
+                    final HttpResponse response,
+                    final EntityDetails entityDetails) throws HttpException, IOException {
+                final HttpClientContext clientContext = scope.clientContext;
+                if (entityProducer != null && !entityProducer.isRepeatable()) {
+                    if (log.isDebugEnabled()) {
+                        log.debug("{}: cannot retry non-repeatable request", exchangeId);
+                    }
+                    return asyncExecCallback.handleResponse(response, entityDetails);
+                }
+                state.retrying = retryStrategy.retryRequest(response, state.execCount, clientContext);
+                if (state.retrying) {
+                    return new NoopEntityConsumer();
+                } else {
+                    return asyncExecCallback.handleResponse(response, entityDetails);
+                }
+            }
+
+            @Override
+            public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
+                asyncExecCallback.handleInformationResponse(response);
+            }
+
+            @Override
+            public void completed() {
+                if (state.retrying) {
+                    state.execCount++;
+                    try {
+                        internalExecute(state, request, entityProducer, scope, chain, asyncExecCallback);
+                    } catch (final IOException | HttpException ex) {
+                        asyncExecCallback.failed(ex);
+                    }
+                } else {
+                    asyncExecCallback.completed();
+                }
+            }
+
+            @Override
+            public void failed(final Exception cause) {
+                if (cause instanceof IOException) {
+                    final HttpRoute route = scope.route;
+                    final HttpClientContext clientContext = scope.clientContext;
+                    if (entityProducer != null && !entityProducer.isRepeatable()) {
+                        if (log.isDebugEnabled()) {
+                            log.debug("{}: cannot retry non-repeatable request", exchangeId);
+                        }
+                    } else if (retryStrategy.retryRequest(request, (IOException) cause, state.execCount, clientContext)) {
+                        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();
+                        }
+                        state.retrying = true;
+                        state.execCount++;
+                        try {
+                            internalExecute(state, request, entityProducer, scope, chain, asyncExecCallback);
+                        } catch (final IOException | HttpException ex) {
+                            asyncExecCallback.failed(ex);
+                        }
+                        return;
+                    }
+                }
+                asyncExecCallback.failed(cause);
+            }
+
+        });
+
+    }
+
+    @Override
+    public void execute(
+            final HttpRequest request,
+            final AsyncEntityProducer entityProducer,
+            final AsyncExecChain.Scope scope,
+            final AsyncExecChain chain,
+            final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
+        final State state = new State();
+        state.execCount = 1;
+        state.retrying = false;
+        internalExecute(state, request, entityProducer, scope, chain, asyncExecCallback);
+    }
+
+}
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 02e6f10..dab9a3b 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
@@ -41,6 +41,7 @@ import java.util.concurrent.ThreadFactory;
 import org.apache.hc.client5.http.AuthenticationStrategy;
 import org.apache.hc.client5.http.DnsResolver;
 import org.apache.hc.client5.http.HttpRequestRetryHandler;
+import org.apache.hc.client5.http.HttpRequestRetryStrategy;
 import org.apache.hc.client5.http.SchemePortResolver;
 import org.apache.hc.client5.http.SystemDefaultDnsResolver;
 import org.apache.hc.client5.http.async.AsyncExecChainHandler;
@@ -55,7 +56,7 @@ import org.apache.hc.client5.http.cookie.CookieStore;
 import org.apache.hc.client5.http.impl.ChainElements;
 import org.apache.hc.client5.http.impl.CookieSpecSupport;
 import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
-import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryHandler;
+import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy;
 import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
 import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
 import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
@@ -192,6 +193,7 @@ public class H2AsyncClientBuilder {
     private HttpRoutePlanner routePlanner;
     private RedirectStrategy redirectStrategy;
     private HttpRequestRetryHandler retryHandler;
+    private HttpRequestRetryStrategy retryStrategy;
 
     private Lookup<AuthSchemeProvider> authSchemeRegistry;
     private Lookup<CookieSpecProvider> cookieSpecRegistry;
@@ -390,6 +392,17 @@ public class H2AsyncClientBuilder {
     }
 
     /**
+     * Assigns {@link HttpRequestRetryStrategy} instance.
+     * <p>
+     * Please note this value can be overridden by the {@link #disableAutomaticRetries()}
+     * method.
+     */
+    public final H2AsyncClientBuilder setRetryStrategy(final HttpRequestRetryStrategy retryStrategy) {
+        this.retryStrategy = retryStrategy;
+        return this;
+    }
+
+    /**
      * Assigns {@link RedirectStrategy} instance.
      * <p>
      * Please note this value can be overridden by the {@link #disableRedirectHandling()}
@@ -674,13 +687,20 @@ public class H2AsyncClientBuilder {
 
         // Add request retry executor, if not disabled
         if (!automaticRetriesDisabled) {
-            HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
-            if (retryHandlerCopy == null) {
-                retryHandlerCopy = DefaultHttpRequestRetryHandler.INSTANCE;
+            final HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
+            if (retryHandlerCopy != null) {
+                execChainDefinition.addFirst(
+                        new AsyncRetryExec(retryHandlerCopy),
+                        ChainElements.RETRY_IO_ERROR.name());
+            } else {
+                HttpRequestRetryStrategy retryStrategyCopy = this.retryStrategy;
+                if (retryStrategyCopy == null) {
+                    retryStrategyCopy = DefaultHttpRequestRetryStrategy.INSTANCE;
+                }
+                execChainDefinition.addFirst(
+                        new AsyncHttpRequestRetryExec(retryStrategyCopy),
+                        ChainElements.RETRY.name());
             }
-            execChainDefinition.addFirst(
-                    new AsyncRetryExec(retryHandlerCopy),
-                    ChainElements.RETRY_IO_ERROR.name());
         }
 
         HttpRoutePlanner routePlannerCopy = this.routePlanner;
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 75eb512..9178476 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
@@ -41,6 +41,7 @@ import java.util.concurrent.ThreadFactory;
 import org.apache.hc.client5.http.AuthenticationStrategy;
 import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
 import org.apache.hc.client5.http.HttpRequestRetryHandler;
+import org.apache.hc.client5.http.HttpRequestRetryStrategy;
 import org.apache.hc.client5.http.SchemePortResolver;
 import org.apache.hc.client5.http.SystemDefaultDnsResolver;
 import org.apache.hc.client5.http.UserTokenHandler;
@@ -57,7 +58,7 @@ import org.apache.hc.client5.http.impl.ChainElements;
 import org.apache.hc.client5.http.impl.CookieSpecSupport;
 import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
 import org.apache.hc.client5.http.impl.DefaultConnectionKeepAliveStrategy;
-import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryHandler;
+import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy;
 import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
 import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
 import org.apache.hc.client5.http.impl.DefaultUserTokenHandler;
@@ -227,6 +228,7 @@ public class HttpAsyncClientBuilder {
     private HttpRoutePlanner routePlanner;
     private RedirectStrategy redirectStrategy;
     private HttpRequestRetryHandler retryHandler;
+    private HttpRequestRetryStrategy retryStrategy;
 
     private ConnectionReuseStrategy reuseStrategy;
 
@@ -497,6 +499,17 @@ public class HttpAsyncClientBuilder {
     }
 
     /**
+     * Assigns {@link HttpRequestRetryStrategy} instance.
+     * <p>
+     * Please note this value can be overridden by the {@link #disableAutomaticRetries()}
+     * method.
+     */
+    public final HttpAsyncClientBuilder setRetryStrategy(final HttpRequestRetryStrategy retryStrategy) {
+        this.retryStrategy = retryStrategy;
+        return this;
+    }
+
+    /**
      * Assigns {@link RedirectStrategy} instance.
      * <p>
      * Please note this value can be overridden by the {@link #disableRedirectHandling()}
@@ -824,13 +837,20 @@ public class HttpAsyncClientBuilder {
 
         // Add request retry executor, if not disabled
         if (!automaticRetriesDisabled) {
-            HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
-            if (retryHandlerCopy == null) {
-                retryHandlerCopy = DefaultHttpRequestRetryHandler.INSTANCE;
+            final HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
+            if (retryHandlerCopy != null) {
+                execChainDefinition.addFirst(
+                        new AsyncRetryExec(retryHandlerCopy),
+                        ChainElements.RETRY_IO_ERROR.name());
+            } else {
+                HttpRequestRetryStrategy retryStrategyCopy = this.retryStrategy;
+                if (retryStrategyCopy == null) {
+                    retryStrategyCopy = DefaultHttpRequestRetryStrategy.INSTANCE;
+                }
+                execChainDefinition.addFirst(
+                        new AsyncHttpRequestRetryExec(retryStrategyCopy),
+                        ChainElements.RETRY.name());
             }
-            execChainDefinition.addFirst(
-                    new AsyncRetryExec(retryHandlerCopy),
-                    ChainElements.RETRY_IO_ERROR.name());
         }
 
         HttpRoutePlanner routePlannerCopy = this.routePlanner;
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 cae1e0f..0dfef46 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
@@ -40,6 +40,7 @@ import java.util.Map;
 import org.apache.hc.client5.http.AuthenticationStrategy;
 import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
 import org.apache.hc.client5.http.HttpRequestRetryHandler;
+import org.apache.hc.client5.http.HttpRequestRetryStrategy;
 import org.apache.hc.client5.http.SchemePortResolver;
 import org.apache.hc.client5.http.ServiceUnavailableRetryStrategy;
 import org.apache.hc.client5.http.SystemDefaultDnsResolver;
@@ -60,7 +61,7 @@ import org.apache.hc.client5.http.impl.ChainElements;
 import org.apache.hc.client5.http.impl.CookieSpecSupport;
 import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
 import org.apache.hc.client5.http.impl.DefaultConnectionKeepAliveStrategy;
-import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryHandler;
+import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy;
 import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
 import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
 import org.apache.hc.client5.http.impl.DefaultUserTokenHandler;
@@ -204,6 +205,7 @@ public class HttpClientBuilder {
     private LinkedList<ExecInterceptorEntry> execInterceptors;
 
     private HttpRequestRetryHandler retryHandler;
+    private HttpRequestRetryStrategy retryStrategy;
     private HttpRoutePlanner routePlanner;
     private RedirectStrategy redirectStrategy;
     private ConnectionBackoffStrategy connectionBackoffStrategy;
@@ -506,6 +508,17 @@ public class HttpClientBuilder {
     }
 
     /**
+     * Assigns {@link HttpRequestRetryStrategy} instance.
+     * <p>
+     * Please note this value can be overridden by the {@link #disableAutomaticRetries()}
+     * method.
+     */
+    public final HttpClientBuilder setRetryStrategy(final HttpRequestRetryStrategy retryStrategy) {
+        this.retryStrategy = retryStrategy;
+        return this;
+    }
+
+    /**
      * Disables automatic request recovery and re-execution.
      */
     public final HttpClientBuilder disableAutomaticRetries() {
@@ -859,13 +872,21 @@ public class HttpClientBuilder {
 
         // Add request retry executor, if not disabled
         if (!automaticRetriesDisabled) {
-            HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
-            if (retryHandlerCopy == null) {
-                retryHandlerCopy = DefaultHttpRequestRetryHandler.INSTANCE;
+            // This needs to be cleaned up as soon as HttpRequestRetryHandler will be removed
+            final HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
+            if (retryHandlerCopy != null) {
+                execChainDefinition.addFirst(
+                        new RetryExec(retryHandlerCopy),
+                        ChainElements.RETRY_IO_ERROR.name());
+            } else {
+                HttpRequestRetryStrategy retryStrategyCopy = this.retryStrategy;
+                if (retryStrategyCopy == null) {
+                    retryStrategyCopy = DefaultHttpRequestRetryStrategy.INSTANCE;
+                }
+                execChainDefinition.addFirst(
+                        new HttpRequestRetryExec(retryStrategyCopy),
+                        ChainElements.RETRY.name());
             }
-            execChainDefinition.addFirst(
-                    new RetryExec(retryHandlerCopy),
-                    ChainElements.RETRY_IO_ERROR.name());
         }
 
         HttpRoutePlanner routePlannerCopy = this.routePlanner;
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
new file mode 100644
index 0000000..c674854
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpRequestRetryExec.java
@@ -0,0 +1,160 @@
+/*
+ * ====================================================================
+ * 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.http.impl.classic;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+
+import org.apache.hc.client5.http.HttpRequestRetryStrategy;
+import org.apache.hc.client5.http.HttpRoute;
+import org.apache.hc.client5.http.classic.ExecChain;
+import org.apache.hc.client5.http.classic.ExecChain.Scope;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.http.classic.ExecChainHandler;
+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.ClassicHttpRequest;
+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.NoHttpResponseException;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.TimeValue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Request executor in the request execution chain that is responsible for
+ * making a decision whether a request that failed due to an I/O exception
+ * or received a specific response from the target server should
+ * be re-executed.
+ * <p>
+ * Further responsibilities such as communication with the opposite
+ * endpoint is delegated to the next executor in the request execution
+ * chain.
+ * </p>
+ *
+ * @since 5.0
+ */
+@Contract(threading = ThreadingBehavior.STATELESS)
+@Internal
+public class HttpRequestRetryExec implements ExecChainHandler {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private final HttpRequestRetryStrategy retryStrategy;
+
+    public HttpRequestRetryExec(
+            final HttpRequestRetryStrategy retryStrategy) {
+         Args.notNull(retryStrategy, "retryStrategy");
+         this.retryStrategy = retryStrategy;
+    }
+
+    @Override
+    public ClassicHttpResponse execute(
+            final ClassicHttpRequest request,
+            final Scope scope,
+            final ExecChain chain) throws IOException, HttpException {
+        Args.notNull(request, "request");
+        Args.notNull(scope, "scope");
+        final String exchangeId = scope.exchangeId;
+        final HttpRoute route = scope.route;
+        final HttpClientContext context = scope.clientContext;
+        ClassicHttpRequest currentRequest = request;
+
+        for (int execCount = 1;; execCount++) {
+            final ClassicHttpResponse response;
+            try {
+                 response = chain.proceed(currentRequest, scope);
+            } catch (final IOException ex) {
+                if (scope.execRuntime.isExecutionAborted()) {
+                    throw new RequestFailedException("Request aborted");
+                }
+                final HttpEntity requestEntity = request.getEntity();
+                if (requestEntity != null && !requestEntity.isRepeatable()) {
+                    if (log.isDebugEnabled()) {
+                        log.debug("{}: cannot retry non-repeatable request", exchangeId);
+                    }
+                    throw ex;
+                }
+                if (retryStrategy.retryRequest(request, ex, execCount, context)) {
+                    if (log.isDebugEnabled()) {
+                        log.debug("{}: {}", exchangeId, ex.getMessage(), ex);
+                    }
+                    if (log.isInfoEnabled()) {
+                        log.info("Recoverable I/O exception ({}) caught when processing request to {}",
+                                ex.getClass().getName(), route);
+                    }
+                    currentRequest = ClassicRequestCopier.INSTANCE.copy(scope.originalRequest);
+                    continue;
+                } else {
+                    if (ex instanceof NoHttpResponseException) {
+                        final NoHttpResponseException updatedex = new NoHttpResponseException(
+                                route.getTargetHost().toHostString() + " failed to respond");
+                        updatedex.setStackTrace(ex.getStackTrace());
+                        throw updatedex;
+                    }
+                    throw ex;
+                }
+            }
+
+            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 nextInterval =
+                            retryStrategy.getRetryInterval(response, 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();
+                        }
+                    }
+                    currentRequest = ClassicRequestCopier.INSTANCE.copy(scope.originalRequest);
+                } else {
+                    return response;
+                }
+            } catch (final RuntimeException ex) {
+                response.close();
+                throw ex;
+            }
+        }
+    }
+
+}
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
new file mode 100644
index 0000000..5bf1663
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestDefaultHttpRequestRetryStrategy.java
@@ -0,0 +1,158 @@
+/*
+ * ====================================================================
+ * 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.http.impl;
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.util.Date;
+
+import javax.net.ssl.SSLException;
+
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+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.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestDefaultHttpRequestRetryStrategy {
+
+    private DefaultHttpRequestRetryStrategy impl;
+
+    @Before
+    public void setup() {
+        this.impl = new DefaultHttpRequestRetryStrategy(3, TimeValue.ofMilliseconds(1234L));
+    }
+
+    @Test
+    public void testBasics() throws Exception {
+        final HttpResponse response1 = new BasicHttpResponse(503, "Oppsie");
+        Assert.assertTrue(this.impl.retryRequest(response1, 1, null));
+        Assert.assertTrue(this.impl.retryRequest(response1, 2, null));
+        Assert.assertTrue(this.impl.retryRequest(response1, 3, null));
+        Assert.assertFalse(this.impl.retryRequest(response1, 4, null));
+        final HttpResponse response2 = new BasicHttpResponse(500, "Big Time Oppsie");
+        Assert.assertFalse(this.impl.retryRequest(response2, 1, null));
+        final HttpResponse response3 = new BasicHttpResponse(429, "Oppsie");
+        Assert.assertTrue(this.impl.retryRequest(response3, 1, null));
+        Assert.assertTrue(this.impl.retryRequest(response3, 2, null));
+        Assert.assertTrue(this.impl.retryRequest(response3, 3, null));
+        Assert.assertFalse(this.impl.retryRequest(response3, 4, null));
+
+        Assert.assertEquals(TimeValue.ofMilliseconds(1234L), this.impl.getRetryInterval(response1, 1, null));
+    }
+
+    @Test
+    public void testRetryAfterHeaderAsLong() throws Exception {
+        final HttpResponse response = new BasicHttpResponse(503, "Oppsie");
+        response.setHeader(HttpHeaders.RETRY_AFTER, "321");
+
+        Assert.assertEquals(TimeValue.ofSeconds(321L), this.impl.getRetryInterval(response, 3, null));
+    }
+
+    @Test
+    public void testRetryAfterHeaderAsDate() throws Exception {
+        this.impl = new DefaultHttpRequestRetryStrategy(3, TimeValue.ofMilliseconds(1L));
+        final HttpResponse response = new BasicHttpResponse(503, "Oppsie");
+        response.setHeader(HttpHeaders.RETRY_AFTER, DateUtils.formatDate(new Date(System.currentTimeMillis() + 100000L)));
+
+        Assert.assertTrue(this.impl.getRetryInterval(response, 3, null).compareTo(TimeValue.ofMilliseconds(1L)) > 0);
+    }
+
+    @Test
+    public void testRetryAfterHeaderAsPastDate() throws Exception {
+        final HttpResponse response = new BasicHttpResponse(503, "Oppsie");
+        response.setHeader(HttpHeaders.RETRY_AFTER, DateUtils.formatDate(new Date(System.currentTimeMillis() - 100000L)));
+
+        Assert.assertEquals(TimeValue.ofMilliseconds(1234L), this.impl.getRetryInterval(response, 3, null));
+    }
+
+    @Test
+    public void testInvalidRetryAfterHeader() throws Exception {
+        final HttpResponse response = new BasicHttpResponse(503, "Oppsie");
+        response.setHeader(HttpHeaders.RETRY_AFTER, "Stuff");
+
+        Assert.assertEquals(TimeValue.ofMilliseconds(1234L), impl.getRetryInterval(response, 3, null));
+    }
+
+    @Test
+    public void noRetryOnConnectTimeout() throws Exception {
+        final HttpGet request = new HttpGet("/");
+
+        Assert.assertFalse(impl.retryRequest(request, new SocketTimeoutException(), 1, null));
+    }
+
+    @Test
+    public void noRetryOnConnect() throws Exception {
+        final HttpGet request = new HttpGet("/");
+
+        Assert.assertFalse(impl.retryRequest(request, new ConnectException(), 1, null));
+    }
+
+    @Test
+    public void noRetryOnConnectionClosed() throws Exception {
+        final HttpGet request = new HttpGet("/");
+
+        Assert.assertFalse(impl.retryRequest(request, new ConnectionClosedException(), 1, null));
+    }
+
+    @Test
+    public void noRetryOnSSLFailure() throws Exception {
+        final HttpGet request = new HttpGet("/");
+
+        Assert.assertFalse(impl.retryRequest(request, new SSLException("encryption failed"), 1, null));
+    }
+
+    @Test
+    public void noRetryOnUnknownHost() throws Exception {
+        final HttpGet request = new HttpGet("/");
+
+        Assert.assertFalse(impl.retryRequest(request, new UnknownHostException(), 1, null));
+    }
+
+    @Test
+    public void noRetryOnAbortedRequests() throws Exception {
+        final HttpGet request = new HttpGet("/");
+        request.cancel();
+
+        Assert.assertFalse(impl.retryRequest(request, new IOException(), 1, null));
+    }
+
+    @Test
+    public void retryOnNonAbortedRequests() throws Exception {
+        final HttpGet request = new HttpGet("/");
+
+        Assert.assertTrue(impl.retryRequest(request, new IOException(), 1, null));
+    }
+
+}
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
new file mode 100644
index 0000000..c9ce01c
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestHttpRequestRetryExec.java
@@ -0,0 +1,269 @@
+/*
+ * ====================================================================
+ * 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.http.impl.classic;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.apache.hc.client5.http.HttpRequestRetryStrategy;
+import org.apache.hc.client5.http.HttpRoute;
+import org.apache.hc.client5.http.classic.ExecChain;
+import org.apache.hc.client5.http.classic.ExecRuntime;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.entity.EntityBuilder;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.Header;
+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.protocol.HttpContext;
+import org.apache.hc.core5.util.TimeValue;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+@SuppressWarnings({"boxing","static-access"}) // test code
+public class TestHttpRequestRetryExec {
+
+    @Mock
+    private HttpRequestRetryStrategy retryStrategy;
+    @Mock
+    private ExecChain chain;
+    @Mock
+    private ExecRuntime endpoint;
+
+    private HttpRequestRetryExec retryExec;
+    private HttpHost target;
+
+    @Before
+    public void setup() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        retryExec = new HttpRequestRetryExec(retryStrategy);
+        target = new HttpHost("localhost", 80);
+    }
+
+
+    @Test
+    public void testFundamentals1() throws Exception {
+        final HttpRoute route = new HttpRoute(target);
+        final HttpGet request = new HttpGet("/test");
+        final HttpClientContext context = HttpClientContext.create();
+
+        final ClassicHttpResponse response = Mockito.mock(ClassicHttpResponse.class);
+
+        Mockito.when(chain.proceed(
+                Mockito.same(request),
+                Mockito.<ExecChain.Scope>any())).thenReturn(response);
+        Mockito.when(retryStrategy.retryRequest(
+                Mockito.<HttpResponse>any(),
+                Mockito.anyInt(),
+                Mockito.<HttpContext>any())).thenReturn(Boolean.TRUE, Boolean.FALSE);
+        Mockito.when(retryStrategy.getRetryInterval(
+                Mockito.<HttpResponse>any(),
+                Mockito.anyInt(),
+                Mockito.<HttpContext>any())).thenReturn(TimeValue.ZERO_MILLISECONDS);
+
+        final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
+        retryExec.execute(request, scope, chain);
+
+        Mockito.verify(chain, Mockito.times(2)).proceed(
+                Mockito.<ClassicHttpRequest>any(),
+                Mockito.same(scope));
+        Mockito.verify(response, Mockito.times(1)).close();
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testStrategyRuntimeException() throws Exception {
+        final HttpRoute route = new HttpRoute(target);
+        final ClassicHttpRequest request = new HttpGet("/test");
+        final HttpClientContext context = HttpClientContext.create();
+
+        final ClassicHttpResponse response = Mockito.mock(ClassicHttpResponse.class);
+        Mockito.when(chain.proceed(
+                Mockito.<ClassicHttpRequest>any(),
+                Mockito.<ExecChain.Scope>any())).thenReturn(response);
+        Mockito.doThrow(new RuntimeException("Ooopsie")).when(retryStrategy).retryRequest(
+                Mockito.<HttpResponse>any(),
+                Mockito.anyInt(),
+                Mockito.<HttpContext>any());
+        final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
+        try {
+            retryExec.execute(request, scope, chain);
+        } catch (final Exception ex) {
+            Mockito.verify(response).close();
+            throw ex;
+        }
+    }
+
+    @Test
+    public void testNonRepeatableEntityResponseReturnedImmediately() throws Exception {
+        final HttpRoute route = new HttpRoute(target);
+
+        final HttpPost request = new HttpPost("/test");
+        request.setEntity(EntityBuilder.create()
+                .setStream(new ByteArrayInputStream(new byte[]{}))
+                .build());
+        final HttpClientContext context = HttpClientContext.create();
+
+        final ClassicHttpResponse response = Mockito.mock(ClassicHttpResponse.class);
+        Mockito.when(chain.proceed(
+                Mockito.<ClassicHttpRequest>any(),
+                Mockito.<ExecChain.Scope>any())).thenReturn(response);
+        Mockito.when(retryStrategy.retryRequest(
+                Mockito.<HttpResponse>any(),
+                Mockito.anyInt(),
+                Mockito.<HttpContext>any())).thenReturn(Boolean.TRUE, Boolean.FALSE);
+
+        final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
+        final ClassicHttpResponse finalResponse = retryExec.execute(request, scope, chain);
+
+        Assert.assertSame(response, finalResponse);
+        Mockito.verify(response, Mockito.times(0)).close();
+    }
+
+    @Test(expected = IOException.class)
+    public void testFundamentals2() throws Exception {
+        final HttpRoute route = new HttpRoute(target);
+        final HttpGet originalRequest = new HttpGet("/test");
+        originalRequest.addHeader("header", "this");
+        originalRequest.addHeader("header", "that");
+        final HttpClientContext context = HttpClientContext.create();
+
+        Mockito.when(chain.proceed(
+                Mockito.<ClassicHttpRequest>any(),
+                Mockito.<ExecChain.Scope>any())).thenAnswer(new Answer<Object>() {
+
+            @Override
+            public Object answer(final InvocationOnMock invocationOnMock) throws Throwable {
+                final Object[] args = invocationOnMock.getArguments();
+                final ClassicHttpRequest wrapper = (ClassicHttpRequest) args[0];
+                final Header[] headers = wrapper.getHeaders();
+                Assert.assertEquals(2, headers.length);
+                Assert.assertEquals("this", headers[0].getValue());
+                Assert.assertEquals("that", headers[1].getValue());
+                wrapper.addHeader("Cookie", "monster");
+                throw new IOException("Ka-boom");
+            }
+
+        });
+        Mockito.when(retryStrategy.retryRequest(
+                Mockito.<HttpRequest>any(),
+                Mockito.<IOException>any(),
+                Mockito.eq(1),
+                Mockito.<HttpContext>any())).thenReturn(Boolean.TRUE);
+        final ExecChain.Scope scope = new ExecChain.Scope("test", route, originalRequest, endpoint, context);
+        final ClassicHttpRequest request = ClassicRequestCopier.INSTANCE.copy(originalRequest);
+        try {
+            retryExec.execute(request, scope, chain);
+        } catch (final IOException ex) {
+            Mockito.verify(chain, Mockito.times(2)).proceed(
+                    Mockito.<ClassicHttpRequest>any(),
+                    Mockito.same(scope));
+            throw ex;
+        }
+    }
+
+
+    @Test(expected = IOException.class)
+    public void testAbortedRequest() throws Exception {
+        final HttpRoute route = new HttpRoute(target);
+        final HttpGet originalRequest = new HttpGet("/test");
+        final HttpClientContext context = HttpClientContext.create();
+
+        Mockito.when(chain.proceed(
+                Mockito.<ClassicHttpRequest>any(),
+                Mockito.<ExecChain.Scope>any())).thenThrow(new IOException("Ka-boom"));
+        Mockito.when(endpoint.isExecutionAborted()).thenReturn(true);
+
+        final ExecChain.Scope scope = new ExecChain.Scope("test", route, originalRequest, endpoint, context);
+        final ClassicHttpRequest request = ClassicRequestCopier.INSTANCE.copy(originalRequest);
+        try {
+            retryExec.execute(request, scope, chain);
+        } catch (final IOException ex) {
+            Mockito.verify(chain, Mockito.times(1)).proceed(
+                    Mockito.same(request),
+                    Mockito.same(scope));
+            Mockito.verify(retryStrategy, Mockito.never()).retryRequest(
+                    Mockito.<HttpRequest>any(),
+                    Mockito.<IOException>any(),
+                    Mockito.anyInt(),
+                    Mockito.<HttpContext>any());
+
+            throw ex;
+        }
+    }
+
+    @Test(expected = IOException.class)
+    public void testNonRepeatableRequest() throws Exception {
+        final HttpRoute route = new HttpRoute(target);
+        final HttpPost originalRequest = new HttpPost("/test");
+        originalRequest.setEntity(EntityBuilder.create()
+                .setStream(new ByteArrayInputStream(new byte[]{}))
+                .build());
+        final HttpClientContext context = HttpClientContext.create();
+
+        Mockito.when(chain.proceed(
+                Mockito.<ClassicHttpRequest>any(),
+                Mockito.<ExecChain.Scope>any())).thenAnswer(new Answer<Object>() {
+
+            @Override
+            public Object answer(final InvocationOnMock invocationOnMock) throws Throwable {
+                final Object[] args = invocationOnMock.getArguments();
+                final ClassicHttpRequest req = (ClassicHttpRequest) args[0];
+                req.getEntity().writeTo(new ByteArrayOutputStream());
+                throw new IOException("Ka-boom");
+            }
+
+        });
+        Mockito.when(retryStrategy.retryRequest(
+                Mockito.<HttpRequest>any(),
+                Mockito.<IOException>any(),
+                Mockito.eq(1),
+                Mockito.<HttpContext>any())).thenReturn(Boolean.TRUE);
+        final ExecChain.Scope scope = new ExecChain.Scope("test", route, originalRequest, endpoint, context);
+        final ClassicHttpRequest request = ClassicRequestCopier.INSTANCE.copy(originalRequest);
+        try {
+            retryExec.execute(request, scope, chain);
+        } catch (final IOException ex) {
+            Mockito.verify(chain, Mockito.times(1)).proceed(
+                    Mockito.same(request),
+                    Mockito.same(scope));
+
+            throw ex;
+        }
+    }
+
+}