From 2ad037051706a3c712c3686546f68b2a64002e52 Mon Sep 17 00:00:00 2001 From: Oleg Kalnichevski Date: Mon, 6 Nov 2017 22:44:14 +0100 Subject: [PATCH] Revised handling of non-repeatable requests --- .../sync/TestClientAuthentication.java | 37 ++++------ .../sync/TestClientRequestExecution.java | 15 +--- .../http/NonRepeatableRequestException.java | 70 ------------------- .../async/methods/SimpleRequestProducer.java | 11 +-- .../http/impl/async/AsyncProtocolExec.java | 28 +++++--- .../http/impl/async/AsyncRedirectExec.java | 18 +++-- .../http/impl/async/AsyncRetryExec.java | 7 +- .../impl/async/InternalHttpAsyncClient.java | 4 +- .../http/impl/classic/ProtocolExec.java | 21 +++--- .../http/impl/classic/RedirectExec.java | 7 +- .../client5/http/impl/classic/RetryExec.java | 12 ++-- .../http/impl/classic/TestProtocolExec.java | 6 +- .../http/impl/classic/TestRetryExec.java | 3 +- 13 files changed, 80 insertions(+), 159 deletions(-) delete mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/NonRepeatableRequestException.java diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java index 71948c390..e03877086 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java @@ -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 { 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 c12d6cf33..f89d4a18f 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 @@ -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 diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/NonRepeatableRequestException.java b/httpclient5/src/main/java/org/apache/hc/client5/http/NonRepeatableRequestException.java deleted file mode 100644 index 90287df67..000000000 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/NonRepeatableRequestException.java +++ /dev/null @@ -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 - * . - * - */ -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); - } - - - -} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/SimpleRequestProducer.java b/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/SimpleRequestProducer.java index 69bb6fad2..ffa2f50cd 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/SimpleRequestProducer.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/SimpleRequestProducer.java @@ -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; diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java index decf267b5..622c2694e 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java @@ -207,16 +207,24 @@ class AsyncProtocolExec implements AsyncExecChainHandler { } if (challenged.get()) { - // Reset request headers - final HttpRequest original = scope.originalRequest; - request.setHeaders(); - for (final Iterator
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
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(); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncRedirectExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncRedirectExec.java index 3b2982061..f59277e74 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncRedirectExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncRedirectExec.java @@ -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); } } } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncRetryExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncRetryExec.java index d08cc1f03..309d44b8c 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncRetryExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncRetryExec.java @@ -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) { diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalHttpAsyncClient.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalHttpAsyncClient.java index 02b4d9e26..0a9f3c9b2 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalHttpAsyncClient.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalHttpAsyncClient.java @@ -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 diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java index 39ffcefad..96794b370 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java @@ -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 diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/RedirectExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/RedirectExec.java index a4fae9058..99b02cb29 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/RedirectExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/RedirectExec.java @@ -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"); } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/RetryExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/RetryExec.java index 42167b171..8e7c8a50e 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/RetryExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/RetryExec.java @@ -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); diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestProtocolExec.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestProtocolExec.java index ca498be93..db1a06443 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestProtocolExec.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestProtocolExec.java @@ -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.any())).thenReturn(Collections.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()); } } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestRetryExec.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestRetryExec.java index 030d1757d..61b180271 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestRetryExec.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestRetryExec.java @@ -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");