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

View File

@ -31,9 +31,7 @@ import java.io.IOException;
import java.net.URI;
import java.util.List;
import org.apache.hc.client5.http.ClientProtocolException;
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.HttpPost;
import org.apache.hc.client5.http.protocol.HttpClientContext;
@ -161,7 +159,7 @@ public class TestClientRequestExecution extends LocalServerTestBase {
Assert.assertEquals(1, myheaders.length);
}
@Test(expected=ClientProtocolException.class)
@Test(expected=IOException.class)
public void testNonRepeatableEntity() throws Exception {
this.server.registerHandler("*", new SimpleService());
@ -192,16 +190,7 @@ public class TestClientRequestExecution extends LocalServerTestBase {
new ByteArrayInputStream(
new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 } ),
-1));
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;
}
this.httpclient.execute(target, httppost, context);
}
@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 final class SimpleRequestProducer extends DefaultAsyncRequestProducer {
if (body.isText()) {
entityProducer = new StringAsyncEntityProducer(body.getBodyText(), body.getContentType());
} else {
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;
}
};
entityProducer = new BasicAsyncEntityProducer(body.getBodyBytes(), body.getContentType());
}
} else {
entityProducer = null;

View File

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

View File

@ -166,13 +166,19 @@ class AsyncRedirectExec implements AsyncExecChainHandler {
if (state.redirectURI == null) {
asyncExecCallback.completed();
} else {
try {
if (state.reroute) {
scope.execRuntime.releaseConnection();
final AsyncEntityProducer entityProducer = state.currentEntityProducer;
if (entityProducer != null && !entityProducer.isRepeatable()) {
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 @@ class AsyncRetryExec implements AsyncExecChainHandler {
if (cause instanceof IOException) {
final HttpRoute route = scope.route;
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()) {
log.info("I/O exception ("+ cause.getClass().getName() +
") caught when processing request to "
@ -99,6 +101,9 @@ class AsyncRetryExec implements AsyncExecChainHandler {
}
try {
scope.execRuntime.discardConnection();
if (entityProducer != null) {
entityProducer.releaseResources();
}
internalExecute(execCount + 1, request, entityProducer, scope, chain, asyncExecCallback);
return;
} catch (final IOException | HttpException ex) {

View File

@ -38,6 +38,7 @@ 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.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.CredentialsProvider;
import org.apache.hc.client5.http.config.Configurable;
@ -201,7 +202,8 @@ class InternalHttpAsyncClient extends AbstractHttpAsyncClientBase {
@Override
public boolean isRepeatable() {
return false;
//TODO: use AsyncRequestProducer#isRepeatable once available
return requestProducer instanceof SimpleRequestProducer;
}
@Override

View File

@ -34,7 +34,6 @@ import java.util.Iterator;
import org.apache.hc.client5.http.AuthenticationStrategy;
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.auth.AuthExchange;
import org.apache.hc.client5.http.auth.ChallengeType;
@ -138,15 +137,7 @@ final class ProtocolExec implements ExecChainHandler {
final AuthExchange targetAuthExchange = context.getAuthExchange(target);
final AuthExchange proxyAuthExchange = proxy != null ? context.getAuthExchange(proxy) : new AuthExchange();
for (int execCount = 1;; execCount++) {
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.");
}
}
for (;;) {
// Run request protocol interceptors
context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
@ -176,12 +167,16 @@ final class ProtocolExec implements ExecChainHandler {
// Do not perform authentication for TRACE request
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)) {
// Make sure the response body is fully consumed, if present
final HttpEntity entity = response.getEntity();
final HttpEntity responseEntity = response.getEntity();
if (execRuntime.isConnectionReusable()) {
EntityUtils.consume(entity);
EntityUtils.consume(responseEntity);
} else {
execRuntime.disconnect();
if (proxyAuthExchange.getState() == AuthExchange.State.SUCCESS

View File

@ -49,6 +49,7 @@ import org.apache.hc.core5.annotation.Contract;
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.HttpHost;
import org.apache.hc.core5.http.HttpStatus;
@ -110,7 +111,11 @@ final class RedirectExec implements ExecChainHandler {
final ClassicHttpResponse response = chain.proceed(currentRequest, currentScope);
try {
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) {
throw new RedirectException("Maximum redirects ("+ maxRedirects + ") exceeded");
}

View File

@ -31,7 +31,6 @@ import java.io.IOException;
import org.apache.hc.client5.http.HttpRequestRetryHandler;
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.ExecChainHandler;
import org.apache.hc.client5.http.protocol.HttpClientContext;
@ -88,6 +87,11 @@ final class RetryExec implements ExecChainHandler {
if (scope.execRuntime.isExecutionAborted()) {
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 (this.log.isInfoEnabled()) {
this.log.info("I/O exception ("+ ex.getClass().getName() +
@ -99,12 +103,6 @@ final class RetryExec implements ExecChainHandler {
if (this.log.isDebugEnabled()) {
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);
if (this.log.isInfoEnabled()) {
this.log.info("Retrying request to " + route);

View File

@ -36,7 +36,6 @@ import java.util.Map;
import org.apache.hc.client5.http.AuthenticationStrategy;
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.AuthExchange;
import org.apache.hc.client5.http.auth.AuthScheme;
@ -295,7 +294,7 @@ public class TestProtocolExec {
Assert.assertNull(proxyAuthExchange.getAuthScheme());
}
@Test(expected = NonRepeatableRequestException.class)
@Test
public void testExecEntityEnclosingRequest() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = new HttpClientContext();
@ -335,7 +334,8 @@ public class TestProtocolExec {
Mockito.<HttpClientContext>any())).thenReturn(Collections.<AuthScheme>singletonList(new BasicScheme()));
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 java.io.IOException;
import org.apache.hc.client5.http.HttpRequestRetryHandler;
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.ExecRuntime;
import org.apache.hc.client5.http.classic.methods.HttpGet;
@ -144,7 +143,7 @@ public class TestRetryExec {
}
}
@Test(expected = NonRepeatableRequestException.class)
@Test(expected = IOException.class)
public void testNonRepeatableRequest() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpPost originalRequest = new HttpPost("/test");