RFC 7231: automatic retrial of idempotent methods

git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@1686589 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Oleg Kalnichevski 2015-06-20 12:12:03 +00:00
parent d3a1171524
commit 6d72c0e326
7 changed files with 41 additions and 147 deletions

View File

@ -29,6 +29,7 @@ package org.apache.http.client;
import java.io.IOException; import java.io.IOException;
import org.apache.http.HttpRequest;
import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpContext;
/** /**
@ -47,6 +48,7 @@ public interface HttpRequestRetryHandler {
* Determines if a method should be retried after an IOException * Determines if a method should be retried after an IOException
* occurs during execution. * occurs during execution.
* *
* @param request request failed die to an I/O exception.
* @param exception the exception that occurred * @param exception the exception that occurred
* @param executionCount the number of times this method has been * @param executionCount the number of times this method has been
* unsuccessfully executed * unsuccessfully executed
@ -55,6 +57,6 @@ public interface HttpRequestRetryHandler {
* @return {@code true} if the method should be retried, {@code false} * @return {@code true} if the method should be retried, {@code false}
* otherwise * otherwise
*/ */
boolean retryRequest(IOException exception, int executionCount, HttpContext context); boolean retryRequest(HttpRequest request, IOException exception, int executionCount, HttpContext context);
} }

View File

@ -34,16 +34,17 @@ import java.net.UnknownHostException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest; import org.apache.http.HttpRequest;
import org.apache.http.annotation.Immutable; import org.apache.http.annotation.Immutable;
import org.apache.http.client.HttpRequestRetryHandler; import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpContext;
import org.apache.http.util.Args; import org.apache.http.util.Args;
@ -60,9 +61,7 @@ public class DefaultHttpRequestRetryHandler implements HttpRequestRetryHandler {
/** the number of times a method will be retried */ /** the number of times a method will be retried */
private final int retryCount; private final int retryCount;
/** Whether or not methods that have successfully sent their request will be retried */ private final Map<String, Boolean> idempotentMethods;
private final boolean requestSentRetryEnabled;
private final Set<Class<? extends IOException>> nonRetriableClasses; private final Set<Class<? extends IOException>> nonRetriableClasses;
/** /**
@ -79,7 +78,13 @@ public class DefaultHttpRequestRetryHandler implements HttpRequestRetryHandler {
final Collection<Class<? extends IOException>> clazzes) { final Collection<Class<? extends IOException>> clazzes) {
super(); super();
this.retryCount = retryCount; this.retryCount = retryCount;
this.requestSentRetryEnabled = requestSentRetryEnabled; this.idempotentMethods = new ConcurrentHashMap<>();
this.idempotentMethods.put("GET", Boolean.TRUE);
this.idempotentMethods.put("HEAD", Boolean.TRUE);
this.idempotentMethods.put("PUT", Boolean.TRUE);
this.idempotentMethods.put("DELETE", Boolean.TRUE);
this.idempotentMethods.put("OPTIONS", Boolean.TRUE);
this.idempotentMethods.put("TRACE", Boolean.TRUE);
this.nonRetriableClasses = new HashSet<>(); this.nonRetriableClasses = new HashSet<>();
for (final Class<? extends IOException> clazz: clazzes) { for (final Class<? extends IOException> clazz: clazzes) {
this.nonRetriableClasses.add(clazz); this.nonRetriableClasses.add(clazz);
@ -120,17 +125,20 @@ public class DefaultHttpRequestRetryHandler implements HttpRequestRetryHandler {
public DefaultHttpRequestRetryHandler() { public DefaultHttpRequestRetryHandler() {
this(3, false); this(3, false);
} }
/** /**
* Used {@code retryCount} and {@code requestSentRetryEnabled} to determine * Used {@code retryCount} and {@code requestSentRetryEnabled} to determine
* if the given method should be retried. * if the given method should be retried.
*/ */
@Override @Override
public boolean retryRequest( public boolean retryRequest(
final HttpRequest request,
final IOException exception, final IOException exception,
final int executionCount, final int executionCount,
final HttpContext context) { final HttpContext context) {
Args.notNull(exception, "Exception parameter"); Args.notNull(request, "HTTP request");
Args.notNull(context, "HTTP context"); Args.notNull(exception, "I/O exception");
if (executionCount > this.retryCount) { if (executionCount > this.retryCount) {
// Do not retry if over max retry count // Do not retry if over max retry count
return false; return false;
@ -144,35 +152,17 @@ public class DefaultHttpRequestRetryHandler implements HttpRequestRetryHandler {
} }
} }
} }
final HttpClientContext clientContext = HttpClientContext.adapt(context);
final HttpRequest request = clientContext.getRequest();
if (request instanceof HttpUriRequest && ((HttpUriRequest)request).isAborted()) { if (request instanceof HttpUriRequest && ((HttpUriRequest)request).isAborted()) {
return false; return false;
} }
if (handleAsIdempotent(request)) { if (handleAsIdempotent(request)) {
// Retry if the request is considered idempotent // Retry if the request is considered idempotent
return true; return true;
} }
if (!clientContext.isRequestSent() || this.requestSentRetryEnabled) {
// Retry if the request has not been sent fully or
// if it's OK to retry methods that have been sent
return true;
}
// otherwise do not retry // otherwise do not retry
return false; return false;
} }
/**
* @return {@code true} if this handler will retry methods that have
* successfully sent their request, {@code false} otherwise
*/
public boolean isRequestSentRetryEnabled() {
return requestSentRetryEnabled;
}
/** /**
* @return the maximum number of times a method will be retried * @return the maximum number of times a method will be retried
*/ */
@ -184,7 +174,9 @@ public class DefaultHttpRequestRetryHandler implements HttpRequestRetryHandler {
* @since 4.2 * @since 4.2
*/ */
protected boolean handleAsIdempotent(final HttpRequest request) { protected boolean handleAsIdempotent(final HttpRequest request) {
return !(request instanceof HttpEntityEnclosingRequest); final String method = request.getRequestLine().getMethod().toUpperCase(Locale.ROOT);
final Boolean b = this.idempotentMethods.get(method);
return b != null && b.booleanValue();
} }
} }

View File

@ -1,81 +0,0 @@
/*
* ====================================================================
* 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.http.impl.client;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.http.HttpRequest;
import org.apache.http.annotation.Immutable;
/**
* {@link org.apache.http.client.HttpRequestRetryHandler} which assumes
* that all requested HTTP methods which should be idempotent according
* to RFC-2616 are in fact idempotent and can be retried.
* <p>
* According to RFC-2616 section 9.1.2 the idempotent HTTP methods are:
* GET, HEAD, PUT, DELETE, OPTIONS, and TRACE
* </p>
*
* @since 4.2
*/
@Immutable
public class StandardHttpRequestRetryHandler extends DefaultHttpRequestRetryHandler {
private final Map<String, Boolean> idempotentMethods;
/**
* Default constructor
*/
public StandardHttpRequestRetryHandler(final int retryCount, final boolean requestSentRetryEnabled) {
super(retryCount, requestSentRetryEnabled);
this.idempotentMethods = new ConcurrentHashMap<>();
this.idempotentMethods.put("GET", Boolean.TRUE);
this.idempotentMethods.put("HEAD", Boolean.TRUE);
this.idempotentMethods.put("PUT", Boolean.TRUE);
this.idempotentMethods.put("DELETE", Boolean.TRUE);
this.idempotentMethods.put("OPTIONS", Boolean.TRUE);
this.idempotentMethods.put("TRACE", Boolean.TRUE);
}
/**
* Default constructor
*/
public StandardHttpRequestRetryHandler() {
this(3, false);
}
@Override
protected boolean handleAsIdempotent(final HttpRequest request) {
final String method = request.getRequestLine().getMethod().toUpperCase(Locale.ROOT);
final Boolean b = this.idempotentMethods.get(method);
return b != null && b.booleanValue();
}
}

View File

@ -91,7 +91,7 @@ public class RetryExec implements ClientExecChain {
this.log.debug("Request has been aborted"); this.log.debug("Request has been aborted");
throw ex; throw ex;
} }
if (retryHandler.retryRequest(ex, execCount, context)) { if (retryHandler.retryRequest(request, ex, execCount, context)) {
if (this.log.isInfoEnabled()) { if (this.log.isInfoEnabled()) {
this.log.info("I/O exception ("+ ex.getClass().getName() + this.log.info("I/O exception ("+ ex.getClass().getName() +
") caught when processing request to " ") caught when processing request to "

View File

@ -26,16 +26,12 @@
*/ */
package org.apache.http.impl.client; package org.apache.http.impl.client;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.IOException; import java.io.IOException;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.ConnectTimeoutException; import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpCoreContext;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
@ -45,70 +41,49 @@ public class TestDefaultHttpRequestRetryHandler {
@Test @Test
public void noRetryOnConnectTimeout() throws Exception { public void noRetryOnConnectTimeout() throws Exception {
final HttpContext context = mock(HttpContext.class); final HttpUriRequest request = new HttpGet("/");
final HttpUriRequest request = mock(HttpUriRequest.class);
final DefaultHttpRequestRetryHandler retryHandler = new DefaultHttpRequestRetryHandler(); final DefaultHttpRequestRetryHandler retryHandler = new DefaultHttpRequestRetryHandler();
Assert.assertEquals(3, retryHandler.getRetryCount()); Assert.assertEquals(3, retryHandler.getRetryCount());
when(request.isAborted()).thenReturn(Boolean.FALSE); Assert.assertFalse(retryHandler.retryRequest(request, new ConnectTimeoutException(), 1, null));
when(context.getAttribute(HttpCoreContext.HTTP_REQUEST)).thenReturn(request);
Assert.assertFalse(retryHandler.retryRequest(new ConnectTimeoutException(), 1, context));
} }
@Test @Test
public void noRetryOnUnknownHost() throws Exception { public void noRetryOnUnknownHost() throws Exception {
final HttpContext context = mock(HttpContext.class); final HttpUriRequest request = new HttpGet("/");
final HttpUriRequest request = mock(HttpUriRequest.class);
final DefaultHttpRequestRetryHandler retryHandler = new DefaultHttpRequestRetryHandler(); final DefaultHttpRequestRetryHandler retryHandler = new DefaultHttpRequestRetryHandler();
when(request.isAborted()).thenReturn(Boolean.FALSE); Assert.assertFalse(retryHandler.retryRequest(request, new UnknownHostException(), 1, null));
when(context.getAttribute(HttpCoreContext.HTTP_REQUEST)).thenReturn(request);
Assert.assertFalse(retryHandler.retryRequest(new UnknownHostException(), 1, context));
} }
@Test @Test
public void noRetryOnAbortedRequests() throws Exception{ public void noRetryOnAbortedRequests() throws Exception{
final HttpContext context = mock(HttpContext.class); final HttpUriRequest request = new HttpGet("/");
final HttpUriRequest request = mock(HttpUriRequest.class); request.abort();
final DefaultHttpRequestRetryHandler retryHandler = new DefaultHttpRequestRetryHandler(); final DefaultHttpRequestRetryHandler retryHandler = new DefaultHttpRequestRetryHandler();
when(request.isAborted()).thenReturn(Boolean.TRUE); Assert.assertFalse(retryHandler.retryRequest(request, new IOException(), 3, null));
when(context.getAttribute(HttpCoreContext.HTTP_REQUEST)).thenReturn(request);
Assert.assertFalse(retryHandler.retryRequest(new IOException(),3,context));
} }
@Test @Test
public void retryOnNonAbortedRequests() throws Exception{ public void retryOnNonAbortedRequests() throws Exception{
final HttpUriRequest request = new HttpGet("/");
final HttpContext context = mock(HttpContext.class);
final HttpUriRequest request = mock(HttpUriRequest.class);
final DefaultHttpRequestRetryHandler retryHandler = new DefaultHttpRequestRetryHandler(); final DefaultHttpRequestRetryHandler retryHandler = new DefaultHttpRequestRetryHandler();
when(request.isAborted()).thenReturn(Boolean.FALSE); Assert.assertTrue(retryHandler.retryRequest(request, new IOException(), 3, null));
when(context.getAttribute(HttpCoreContext.HTTP_REQUEST)).thenReturn(request);
Assert.assertTrue(retryHandler.retryRequest(new IOException(),3,context));
} }
@Test @Test
public void noRetryOnConnectionTimeout() throws Exception{ public void noRetryOnConnectionTimeout() throws Exception{
final HttpUriRequest request = new HttpGet("/");
final HttpContext context = mock(HttpContext.class);
final HttpUriRequest request = mock(HttpUriRequest.class);
final DefaultHttpRequestRetryHandler retryHandler = new DefaultHttpRequestRetryHandler(); final DefaultHttpRequestRetryHandler retryHandler = new DefaultHttpRequestRetryHandler();
when(request.isAborted()).thenReturn(false); Assert.assertFalse(retryHandler.retryRequest(request, new ConnectTimeoutException(), 3, null));
when(context.getAttribute(HttpCoreContext.HTTP_REQUEST)).thenReturn(request);
Assert.assertFalse(retryHandler.retryRequest(new ConnectTimeoutException(),3,context));
} }
} }

View File

@ -126,6 +126,7 @@ public class TestClientRequestExecution extends LocalServerTestBase {
@Override @Override
public boolean retryRequest( public boolean retryRequest(
final HttpRequest request,
final IOException exception, final IOException exception,
final int executionCount, final int executionCount,
final HttpContext context) { final HttpContext context) {
@ -166,6 +167,7 @@ public class TestClientRequestExecution extends LocalServerTestBase {
@Override @Override
public boolean retryRequest( public boolean retryRequest(
final HttpRequest request,
final IOException exception, final IOException exception,
final int executionCount, final int executionCount,
final HttpContext context) { final HttpContext context) {

View File

@ -29,6 +29,7 @@ package org.apache.http.impl.execchain;
import org.apache.http.Header; import org.apache.http.Header;
import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHost; import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.client.HttpRequestRetryHandler; import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.NonRepeatableRequestException; import org.apache.http.client.NonRepeatableRequestException;
import org.apache.http.client.entity.EntityBuilder; import org.apache.http.client.entity.EntityBuilder;
@ -101,6 +102,7 @@ public class TestRetryExec {
}); });
Mockito.when(retryHandler.retryRequest( Mockito.when(retryHandler.retryRequest(
Mockito.<HttpRequest>any(),
Mockito.<IOException>any(), Mockito.<IOException>any(),
Mockito.eq(1), Mockito.eq(1),
Mockito.<HttpContext>any())).thenReturn(Boolean.TRUE); Mockito.<HttpContext>any())).thenReturn(Boolean.TRUE);
@ -139,6 +141,7 @@ public class TestRetryExec {
Mockito.same(context), Mockito.same(context),
Mockito.same(execAware)); Mockito.same(execAware));
Mockito.verify(retryHandler, Mockito.never()).retryRequest( Mockito.verify(retryHandler, Mockito.never()).retryRequest(
Mockito.<HttpRequest>any(),
Mockito.<IOException>any(), Mockito.<IOException>any(),
Mockito.anyInt(), Mockito.anyInt(),
Mockito.<HttpContext>any()); Mockito.<HttpContext>any());
@ -173,6 +176,7 @@ public class TestRetryExec {
}); });
Mockito.when(retryHandler.retryRequest( Mockito.when(retryHandler.retryRequest(
Mockito.<HttpRequest>any(),
Mockito.<IOException>any(), Mockito.<IOException>any(),
Mockito.eq(1), Mockito.eq(1),
Mockito.<HttpContext>any())).thenReturn(Boolean.TRUE); Mockito.<HttpContext>any())).thenReturn(Boolean.TRUE);