Revised handling of non-repeatable requests

This commit is contained in:
Oleg Kalnichevski 2017-11-06 22:44:14 +01:00
parent 45f1a2a740
commit 2ad0370517
13 changed files with 80 additions and 159 deletions

View File

@ -35,8 +35,6 @@
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import org.apache.hc.client5.http.ClientProtocolException;
import org.apache.hc.client5.http.NonRepeatableRequestException;
import org.apache.hc.client5.http.auth.AuthCache; import org.apache.hc.client5.http.auth.AuthCache;
import org.apache.hc.client5.http.auth.AuthChallenge; import org.apache.hc.client5.http.auth.AuthChallenge;
import org.apache.hc.client5.http.auth.AuthScheme; import org.apache.hc.client5.http.auth.AuthScheme;
@ -215,7 +213,7 @@ public void testBasicAuthenticationSuccessOnNonRepeatablePutExpectContinue() thr
Assert.assertNotNull(entity); Assert.assertNotNull(entity);
} }
@Test(expected=ClientProtocolException.class) @Test
public void testBasicAuthenticationFailureOnNonRepeatablePutDontExpectContinue() throws Exception { public void testBasicAuthenticationFailureOnNonRepeatablePutDontExpectContinue() throws Exception {
this.server.registerHandler("*", new EchoHandler()); this.server.registerHandler("*", new EchoHandler());
final HttpHost target = start(); final HttpHost target = start();
@ -233,15 +231,11 @@ public void testBasicAuthenticationFailureOnNonRepeatablePutDontExpectContinue()
new UsernamePasswordCredentials("test", "boom".toCharArray())); new UsernamePasswordCredentials("test", "boom".toCharArray()));
context.setCredentialsProvider(credsProvider); context.setCredentialsProvider(credsProvider);
try { final CloseableHttpResponse response = this.httpclient.execute(target, httpput, context);
this.httpclient.execute(target, httpput, context); final HttpEntity entity = response.getEntity();
Assert.fail("ClientProtocolException should have been thrown"); Assert.assertEquals(401, response.getCode());
} catch (final ClientProtocolException ex) { Assert.assertNotNull(entity);
final Throwable cause = ex.getCause(); EntityUtils.consume(entity);
Assert.assertNotNull(cause);
Assert.assertTrue(cause instanceof NonRepeatableRequestException);
throw ex;
}
} }
@Test @Test
@ -267,7 +261,7 @@ public void testBasicAuthenticationSuccessOnRepeatablePost() throws Exception {
Assert.assertEquals("test realm", authscope.getRealm()); Assert.assertEquals("test realm", authscope.getRealm());
} }
@Test(expected=ClientProtocolException.class) @Test
public void testBasicAuthenticationFailureOnNonRepeatablePost() throws Exception { public void testBasicAuthenticationFailureOnNonRepeatablePost() throws Exception {
this.server.registerHandler("*", new EchoHandler()); this.server.registerHandler("*", new EchoHandler());
final HttpHost target = start(); final HttpHost target = start();
@ -278,19 +272,18 @@ public void testBasicAuthenticationFailureOnNonRepeatablePost() throws Exception
new byte[] { 0,1,2,3,4,5,6,7,8,9 }), -1)); new byte[] { 0,1,2,3,4,5,6,7,8,9 }), -1));
final HttpClientContext context = HttpClientContext.create(); final HttpClientContext context = HttpClientContext.create();
context.setRequestConfig(RequestConfig.custom()
.setExpectContinueEnabled(false)
.build());
final TestCredentialsProvider credsProvider = new TestCredentialsProvider( final TestCredentialsProvider credsProvider = new TestCredentialsProvider(
new UsernamePasswordCredentials("test", "test".toCharArray())); new UsernamePasswordCredentials("test", "test".toCharArray()));
context.setCredentialsProvider(credsProvider); context.setCredentialsProvider(credsProvider);
try { final CloseableHttpResponse response = this.httpclient.execute(target, httppost, context);
this.httpclient.execute(target, httppost, context); final HttpEntity entity = response.getEntity();
Assert.fail("ClientProtocolException should have been thrown"); Assert.assertEquals(401, response.getCode());
} catch (final ClientProtocolException ex) { Assert.assertNotNull(entity);
final Throwable cause = ex.getCause(); EntityUtils.consume(entity);
Assert.assertNotNull(cause);
Assert.assertTrue(cause instanceof NonRepeatableRequestException);
throw ex;
}
} }
static class TestTargetAuthenticationStrategy extends DefaultAuthenticationStrategy { static class TestTargetAuthenticationStrategy extends DefaultAuthenticationStrategy {

View File

@ -31,9 +31,7 @@
import java.net.URI; import java.net.URI;
import java.util.List; import java.util.List;
import org.apache.hc.client5.http.ClientProtocolException;
import org.apache.hc.client5.http.HttpRequestRetryHandler; import org.apache.hc.client5.http.HttpRequestRetryHandler;
import org.apache.hc.client5.http.NonRepeatableRequestException;
import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.client5.http.protocol.HttpClientContext;
@ -161,7 +159,7 @@ public boolean retryRequest(
Assert.assertEquals(1, myheaders.length); Assert.assertEquals(1, myheaders.length);
} }
@Test(expected=ClientProtocolException.class) @Test(expected=IOException.class)
public void testNonRepeatableEntity() throws Exception { public void testNonRepeatableEntity() throws Exception {
this.server.registerHandler("*", new SimpleService()); this.server.registerHandler("*", new SimpleService());
@ -192,16 +190,7 @@ public boolean retryRequest(
new ByteArrayInputStream( new ByteArrayInputStream(
new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 } ), new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 } ),
-1)); -1));
this.httpclient.execute(target, httppost, context);
try {
this.httpclient.execute(target, httppost, context);
} catch (final ClientProtocolException ex) {
Assert.assertTrue(ex.getCause() instanceof NonRepeatableRequestException);
final NonRepeatableRequestException nonRepeat = (NonRepeatableRequestException)ex.getCause();
Assert.assertTrue(nonRepeat.getCause() instanceof IOException);
Assert.assertEquals("a message showing that this failed", nonRepeat.getCause().getMessage());
throw ex;
}
} }
@Test @Test

View File

@ -1,70 +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.hc.client5.http;
import org.apache.hc.core5.http.ProtocolException;
/**
* Signals failure to retry the request due to non-repeatable request
* entity.
*
*
* @since 4.0
*/
public class NonRepeatableRequestException extends ProtocolException {
private static final long serialVersionUID = 82685265288806048L;
/**
* Creates a new NonRepeatableEntityException with a {@code null} detail message.
*/
public NonRepeatableRequestException() {
super();
}
/**
* Creates a new NonRepeatableEntityException with the specified detail message.
*
* @param message The exception detail message
*/
public NonRepeatableRequestException(final String message) {
super(message);
}
/**
* Creates a new NonRepeatableEntityException with the specified detail message.
*
* @param message The exception detail message
* @param cause the cause
*/
public NonRepeatableRequestException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -47,16 +47,7 @@ public static SimpleRequestProducer create(final SimpleHttpRequest request, fina
if (body.isText()) { if (body.isText()) {
entityProducer = new StringAsyncEntityProducer(body.getBodyText(), body.getContentType()); entityProducer = new StringAsyncEntityProducer(body.getBodyText(), body.getContentType());
} else { } else {
entityProducer = new BasicAsyncEntityProducer(body.getBodyBytes(), body.getContentType()) { entityProducer = new BasicAsyncEntityProducer(body.getBodyBytes(), body.getContentType());
//TODO: return the actual content length once
// the entity producers made repeatable in HttpCore
@Override
public long getContentLength() {
return -1;
}
};
} }
} else { } else {
entityProducer = null; entityProducer = null;

View File

@ -207,16 +207,24 @@ public void completed() {
} }
if (challenged.get()) { if (challenged.get()) {
// Reset request headers if (entityProducer != null && !entityProducer.isRepeatable()) {
final HttpRequest original = scope.originalRequest; log.debug("Cannot retry non-repeatable request");
request.setHeaders(); asyncExecCallback.completed();
for (final Iterator<Header> it = original.headerIterator(); it.hasNext(); ) { } else {
request.addHeader(it.next()); // Reset request headers
} final HttpRequest original = scope.originalRequest;
try { request.setHeaders();
internalExecute(challenged, request, entityProducer, scope, chain, asyncExecCallback); for (final Iterator<Header> it = original.headerIterator(); it.hasNext(); ) {
} catch (final HttpException | IOException ex) { request.addHeader(it.next());
asyncExecCallback.failed(ex); }
try {
if (entityProducer != null) {
entityProducer.releaseResources();
}
internalExecute(challenged, request, entityProducer, scope, chain, asyncExecCallback);
} catch (final HttpException | IOException ex) {
asyncExecCallback.failed(ex);
}
} }
} else { } else {
asyncExecCallback.completed(); asyncExecCallback.completed();

View File

@ -166,13 +166,19 @@ public void completed() {
if (state.redirectURI == null) { if (state.redirectURI == null) {
asyncExecCallback.completed(); asyncExecCallback.completed();
} else { } else {
try { final AsyncEntityProducer entityProducer = state.currentEntityProducer;
if (state.reroute) { if (entityProducer != null && !entityProducer.isRepeatable()) {
scope.execRuntime.releaseConnection(); log.debug("Cannot redirect non-repeatable request");
asyncExecCallback.completed();
} else {
try {
if (state.reroute) {
scope.execRuntime.releaseConnection();
}
internalExecute(state, chain, asyncExecCallback);
} catch (final IOException | HttpException ex) {
asyncExecCallback.failed(ex);
} }
internalExecute(state, chain, asyncExecCallback);
} catch (final IOException | HttpException ex) {
asyncExecCallback.failed(ex);
} }
} }
} }

View File

@ -83,7 +83,9 @@ public void failed(final Exception cause) {
if (cause instanceof IOException) { if (cause instanceof IOException) {
final HttpRoute route = scope.route; final HttpRoute route = scope.route;
final HttpClientContext clientContext = scope.clientContext; final HttpClientContext clientContext = scope.clientContext;
if (retryHandler.retryRequest(request, (IOException) cause, execCount, clientContext)) { if (entityProducer != null && !entityProducer.isRepeatable()) {
log.debug("Cannot retry non-repeatable request");
} else if (retryHandler.retryRequest(request, (IOException) cause, execCount, clientContext)) {
if (log.isInfoEnabled()) { if (log.isInfoEnabled()) {
log.info("I/O exception ("+ cause.getClass().getName() + log.info("I/O exception ("+ cause.getClass().getName() +
") caught when processing request to " ") caught when processing request to "
@ -99,6 +101,9 @@ public void failed(final Exception cause) {
} }
try { try {
scope.execRuntime.discardConnection(); scope.execRuntime.discardConnection();
if (entityProducer != null) {
entityProducer.releaseResources();
}
internalExecute(execCount + 1, request, entityProducer, scope, chain, asyncExecCallback); internalExecute(execCount + 1, request, entityProducer, scope, chain, asyncExecCallback);
return; return;
} catch (final IOException | HttpException ex) { } catch (final IOException | HttpException ex) {

View File

@ -38,6 +38,7 @@
import org.apache.hc.client5.http.async.AsyncExecCallback; import org.apache.hc.client5.http.async.AsyncExecCallback;
import org.apache.hc.client5.http.async.AsyncExecChain; import org.apache.hc.client5.http.async.AsyncExecChain;
import org.apache.hc.client5.http.async.AsyncExecRuntime; import org.apache.hc.client5.http.async.AsyncExecRuntime;
import org.apache.hc.client5.http.async.methods.SimpleRequestProducer;
import org.apache.hc.client5.http.auth.AuthSchemeProvider; import org.apache.hc.client5.http.auth.AuthSchemeProvider;
import org.apache.hc.client5.http.auth.CredentialsProvider; import org.apache.hc.client5.http.auth.CredentialsProvider;
import org.apache.hc.client5.http.config.Configurable; import org.apache.hc.client5.http.config.Configurable;
@ -201,7 +202,8 @@ public void failed(final Exception cause) {
@Override @Override
public boolean isRepeatable() { public boolean isRepeatable() {
return false; //TODO: use AsyncRequestProducer#isRepeatable once available
return requestProducer instanceof SimpleRequestProducer;
} }
@Override @Override

View File

@ -34,7 +34,6 @@
import org.apache.hc.client5.http.AuthenticationStrategy; import org.apache.hc.client5.http.AuthenticationStrategy;
import org.apache.hc.client5.http.HttpRoute; import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.NonRepeatableRequestException;
import org.apache.hc.client5.http.StandardMethods; import org.apache.hc.client5.http.StandardMethods;
import org.apache.hc.client5.http.auth.AuthExchange; import org.apache.hc.client5.http.auth.AuthExchange;
import org.apache.hc.client5.http.auth.ChallengeType; import org.apache.hc.client5.http.auth.ChallengeType;
@ -138,15 +137,7 @@ public ClassicHttpResponse execute(
final AuthExchange targetAuthExchange = context.getAuthExchange(target); final AuthExchange targetAuthExchange = context.getAuthExchange(target);
final AuthExchange proxyAuthExchange = proxy != null ? context.getAuthExchange(proxy) : new AuthExchange(); final AuthExchange proxyAuthExchange = proxy != null ? context.getAuthExchange(proxy) : new AuthExchange();
for (int execCount = 1;; execCount++) { for (;;) {
if (execCount > 1) {
final HttpEntity entity = request.getEntity();
if (entity != null && !entity.isRepeatable()) {
throw new NonRepeatableRequestException("Cannot retry request " +
"with a non-repeatable request entity.");
}
}
// Run request protocol interceptors // Run request protocol interceptors
context.setAttribute(HttpClientContext.HTTP_ROUTE, route); context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
@ -176,12 +167,16 @@ public ClassicHttpResponse execute(
// Do not perform authentication for TRACE request // Do not perform authentication for TRACE request
return response; return response;
} }
final HttpEntity requestEntity = request.getEntity();
if (requestEntity != null && !requestEntity.isRepeatable()) {
log.debug("Cannot retry non-repeatable request");
return response;
}
if (needAuthentication(targetAuthExchange, proxyAuthExchange, route, request, response, context)) { if (needAuthentication(targetAuthExchange, proxyAuthExchange, route, request, response, context)) {
// Make sure the response body is fully consumed, if present // Make sure the response body is fully consumed, if present
final HttpEntity entity = response.getEntity(); final HttpEntity responseEntity = response.getEntity();
if (execRuntime.isConnectionReusable()) { if (execRuntime.isConnectionReusable()) {
EntityUtils.consume(entity); EntityUtils.consume(responseEntity);
} else { } else {
execRuntime.disconnect(); execRuntime.disconnect();
if (proxyAuthExchange.getState() == AuthExchange.State.SUCCESS if (proxyAuthExchange.getState() == AuthExchange.State.SUCCESS

View File

@ -49,6 +49,7 @@
import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse; 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.HttpException;
import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.HttpStatus;
@ -110,7 +111,11 @@ public ClassicHttpResponse execute(
final ClassicHttpResponse response = chain.proceed(currentRequest, currentScope); final ClassicHttpResponse response = chain.proceed(currentRequest, currentScope);
try { try {
if (config.isRedirectsEnabled() && this.redirectStrategy.isRedirected(request, response, context)) { if (config.isRedirectsEnabled() && this.redirectStrategy.isRedirected(request, response, context)) {
final HttpEntity requestEntity = request.getEntity();
if (requestEntity != null && !requestEntity.isRepeatable()) {
this.log.debug("Cannot redirect non-repeatable request");
return response;
}
if (redirectCount >= maxRedirects) { if (redirectCount >= maxRedirects) {
throw new RedirectException("Maximum redirects ("+ maxRedirects + ") exceeded"); throw new RedirectException("Maximum redirects ("+ maxRedirects + ") exceeded");
} }

View File

@ -31,7 +31,6 @@
import org.apache.hc.client5.http.HttpRequestRetryHandler; import org.apache.hc.client5.http.HttpRequestRetryHandler;
import org.apache.hc.client5.http.HttpRoute; import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.NonRepeatableRequestException;
import org.apache.hc.client5.http.classic.ExecChain; import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecChainHandler; import org.apache.hc.client5.http.classic.ExecChainHandler;
import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.client5.http.protocol.HttpClientContext;
@ -88,6 +87,11 @@ public ClassicHttpResponse execute(
if (scope.execRuntime.isExecutionAborted()) { if (scope.execRuntime.isExecutionAborted()) {
throw new RequestFailedException("Request aborted"); throw new RequestFailedException("Request aborted");
} }
final HttpEntity requestEntity = request.getEntity();
if (requestEntity != null && !requestEntity.isRepeatable()) {
this.log.debug("Cannot retry non-repeatable request");
throw ex;
}
if (retryHandler.retryRequest(request, 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() +
@ -99,12 +103,6 @@ public ClassicHttpResponse execute(
if (this.log.isDebugEnabled()) { if (this.log.isDebugEnabled()) {
this.log.debug(ex.getMessage(), ex); this.log.debug(ex.getMessage(), ex);
} }
final HttpEntity entity = request.getEntity();
if (entity != null && !entity.isRepeatable()) {
this.log.debug("Cannot retry non-repeatable request");
throw new NonRepeatableRequestException("Cannot retry request " +
"with a non-repeatable request entity", ex);
}
currentRequest = ClassicRequestCopier.INSTANCE.copy(scope.originalRequest); currentRequest = ClassicRequestCopier.INSTANCE.copy(scope.originalRequest);
if (this.log.isInfoEnabled()) { if (this.log.isInfoEnabled()) {
this.log.info("Retrying request to " + route); this.log.info("Retrying request to " + route);

View File

@ -36,7 +36,6 @@
import org.apache.hc.client5.http.AuthenticationStrategy; import org.apache.hc.client5.http.AuthenticationStrategy;
import org.apache.hc.client5.http.HttpRoute; import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.NonRepeatableRequestException;
import org.apache.hc.client5.http.auth.AuthChallenge; import org.apache.hc.client5.http.auth.AuthChallenge;
import org.apache.hc.client5.http.auth.AuthExchange; import org.apache.hc.client5.http.auth.AuthExchange;
import org.apache.hc.client5.http.auth.AuthScheme; import org.apache.hc.client5.http.auth.AuthScheme;
@ -295,7 +294,7 @@ public void testExecEntityEnclosingRequestRetryOnAuthChallenge() throws Exceptio
Assert.assertNull(proxyAuthExchange.getAuthScheme()); Assert.assertNull(proxyAuthExchange.getAuthScheme());
} }
@Test(expected = NonRepeatableRequestException.class) @Test
public void testExecEntityEnclosingRequest() throws Exception { public void testExecEntityEnclosingRequest() throws Exception {
final HttpRoute route = new HttpRoute(target); final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = new HttpClientContext(); final HttpClientContext context = new HttpClientContext();
@ -335,7 +334,8 @@ public HttpResponse answer(final InvocationOnMock invocationOnMock) throws Throw
Mockito.<HttpClientContext>any())).thenReturn(Collections.<AuthScheme>singletonList(new BasicScheme())); Mockito.<HttpClientContext>any())).thenReturn(Collections.<AuthScheme>singletonList(new BasicScheme()));
final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context); final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context);
protocolExec.execute(request, scope, chain); final ClassicHttpResponse response = protocolExec.execute(request, scope, chain);
Assert.assertEquals(401, response.getCode());
} }
} }

View File

@ -32,7 +32,6 @@
import org.apache.hc.client5.http.HttpRequestRetryHandler; import org.apache.hc.client5.http.HttpRequestRetryHandler;
import org.apache.hc.client5.http.HttpRoute; import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.NonRepeatableRequestException;
import org.apache.hc.client5.http.classic.ExecChain; import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecRuntime; 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.HttpGet;
@ -144,7 +143,7 @@ public void testAbortedRequest() throws Exception {
} }
} }
@Test(expected = NonRepeatableRequestException.class) @Test(expected = IOException.class)
public void testNonRepeatableRequest() throws Exception { public void testNonRepeatableRequest() throws Exception {
final HttpRoute route = new HttpRoute(target); final HttpRoute route = new HttpRoute(target);
final HttpPost originalRequest = new HttpPost("/test"); final HttpPost originalRequest = new HttpPost("/test");