HTTPCLIENT-1425: Fixed socket closed exception thrown by caching HttpClient when the origin server sends a long chunked response

Contributed by James Leigh <james at 3roundstones dot com>

git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@1534585 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Oleg Kalnichevski 2013-10-22 10:10:16 +00:00
parent 9bdc844a40
commit 6bb9398b39
8 changed files with 103 additions and 18 deletions

View File

@ -1,6 +1,10 @@
Changes since 4.3.1
-------------------
* [HTTPCLIENT-1425] Fixed socket closed exception thrown by caching HttpClient when the origin
server sends a long chunked response.
Contributed by James Leigh <james at 3roundstones dot com>
* [HTTPCLIENT-1417] Fixed NPE in BrowserCompatSpec#formatCookies caused by version 1
cookies with null cookie value.
Contributed by Oleg Kalnichevski <olegk at apache.org>

View File

@ -50,10 +50,10 @@ import org.apache.http.client.cache.HttpCacheUpdateCallback;
import org.apache.http.client.cache.HttpCacheUpdateException;
import org.apache.http.client.cache.Resource;
import org.apache.http.client.cache.ResourceFactory;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
class BasicHttpCache implements HttpCache {
private static final Set<String> safeRequestMethods = new HashSet<String>(
@ -276,12 +276,25 @@ class BasicHttpCache implements HttpCache {
public HttpResponse cacheAndReturnResponse(final HttpHost host, final HttpRequest request,
final HttpResponse originResponse, final Date requestSent, final Date responseReceived)
throws IOException {
return cacheAndReturnResponse(host, request,
Proxies.enhanceResponse(originResponse), requestSent,
responseReceived);
}
public HttpResponse cacheAndReturnResponse(
final HttpHost host,
final HttpRequest request,
final CloseableHttpResponse originResponse,
final Date requestSent,
final Date responseReceived) throws IOException {
boolean closeOriginResponse = true;
final SizeLimitedResponseReader responseReader = getResponseReader(request, originResponse);
try {
responseReader.readResponse();
if (responseReader.isLimitReached()) {
closeOriginResponse = false;
return responseReader.getReconstructedResponse();
}
@ -298,12 +311,10 @@ class BasicHttpCache implements HttpCache {
resource);
storeInCache(host, request, entry);
return responseGenerator.generateResponse(entry);
} catch (final IOException ex) {
EntityUtils.consume(originResponse.getEntity());
throw ex;
} catch (final RuntimeException ex) {
EntityUtils.consumeQuietly(originResponse.getEntity());
throw ex;
} finally {
if (closeOriginResponse) {
originResponse.close();
}
}
}

View File

@ -808,13 +808,9 @@ public class CachingExec implements ClientExecChain {
final boolean cacheable = responseCachingPolicy.isResponseCacheable(request, backendResponse);
responseCache.flushInvalidatedCacheEntriesFor(target, request, backendResponse);
if (cacheable && !alreadyHaveNewerCacheEntry(target, request, backendResponse)) {
try {
storeRequestIfModifiedSinceFor304Response(request, backendResponse);
return Proxies.enhanceResponse(responseCache.cacheAndReturnResponse(
target, request, backendResponse, requestDate, responseDate));
} finally {
backendResponse.close();
}
storeRequestIfModifiedSinceFor304Response(request, backendResponse);
return Proxies.enhanceResponse(responseCache.cacheAndReturnResponse(
target, request, backendResponse, requestDate, responseDate));
}
if (!cacheable) {
try {

View File

@ -34,6 +34,7 @@ import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.cache.HttpCacheEntry;
import org.apache.http.client.methods.CloseableHttpResponse;
/**
* @since 4.1
@ -103,6 +104,21 @@ interface HttpCache {
Date requestSent, Date responseReceived)
throws IOException;
/**
* Store a {@link HttpResponse} in the cache if possible, and return
* @param host
* @param request
* @param originResponse
* @param requestSent
* @param responseReceived
* @return the {@link HttpResponse}
* @throws IOException
*/
HttpResponse cacheAndReturnResponse(HttpHost host, HttpRequest request,
CloseableHttpResponse originResponse, Date requestSent,
Date responseReceived)
throws IOException;
/**
* Update a {@link HttpCacheEntry} using a 304 {@link HttpResponse}.
* @param target

View File

@ -72,6 +72,11 @@ public abstract class AbstractProtocolTest {
return null;
}
public static CloseableHttpResponse eqCloseableResponse(final CloseableHttpResponse in) {
EasyMock.reportMatcher(new ResponseEquivalent(in));
return null;
}
@Before
public void setUp() {
host = new HttpHost("foo.example.com");

View File

@ -36,11 +36,14 @@ import static org.easymock.classextension.EasyMock.createMockBuilder;
import static org.easymock.classextension.EasyMock.createNiceMock;
import static org.easymock.classextension.EasyMock.replay;
import static org.easymock.classextension.EasyMock.verify;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
@ -52,11 +55,14 @@ import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpExecutionAware;
import org.apache.http.client.methods.HttpRequestWrapper;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.DateUtils;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.execchain.ClientExecChain;
import org.apache.http.message.BasicHttpRequest;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.message.BasicStatusLine;
import org.apache.http.protocol.HTTP;
import org.easymock.IExpectationSetters;
import org.easymock.classextension.EasyMock;
import org.junit.Assert;
@ -92,7 +98,7 @@ public class TestCachingExec extends TestCachingExecChain {
}
@Override
public ClientExecChain createCachingExecChain(final ClientExecChain mockBackend,
public CachingExec createCachingExecChain(final ClientExecChain mockBackend,
final HttpCache mockCache, final CacheValidityPolicy mockValidityPolicy,
final ResponseCachingPolicy mockResponsePolicy,
final CachedHttpResponseGenerator mockResponseGenerator,
@ -118,7 +124,7 @@ public class TestCachingExec extends TestCachingExecChain {
}
@Override
public ClientExecChain createCachingExecChain(final ClientExecChain backend,
public CachingExec createCachingExecChain(final ClientExecChain backend,
final HttpCache cache, final CacheConfig config) {
return impl = new CachingExec(backend, cache, config);
}
@ -300,6 +306,53 @@ public class TestCachingExec extends TestCachingExecChain {
verifyMocks();
}
@Test
public void testEndlessResponsesArePassedThrough() throws Exception {
impl = createCachingExecChain(mockBackend, new BasicHttpCache(), CacheConfig.DEFAULT);
final HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
resp1.setHeader("Date", DateUtils.formatDate(new Date()));
resp1.setHeader("Server", "MockOrigin/1.0");
resp1.setHeader(HTTP.TRANSFER_ENCODING, HTTP.CHUNK_CODING);
final AtomicInteger size = new AtomicInteger();
final AtomicInteger maxlength = new AtomicInteger(Integer.MAX_VALUE);
resp1.setEntity(new InputStreamEntity(new InputStream() {
private Throwable closed;
public void close() throws IOException {
closed = new Throwable();
super.close();
}
public int read() throws IOException {
Thread.yield();
if (closed != null) {
throw new IOException("Response has been closed");
}
if (size.incrementAndGet() > maxlength.get())
return -1;
return 'y';
}
}));
final CloseableHttpResponse resp = mockBackend.execute(
EasyMock.isA(HttpRoute.class),
EasyMock.isA(HttpRequestWrapper.class),
EasyMock.isA(HttpClientContext.class),
EasyMock.<HttpExecutionAware>isNull());
EasyMock.expect(resp).andReturn(Proxies.enhanceResponse(resp1));
final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(HttpTestUtils.makeDefaultRequest());
replayMocks();
final CloseableHttpResponse resp2 = impl.execute(route, req1, context, null);
maxlength.set(size.get() * 2);
verifyMocks();
assertTrue(HttpTestUtils.semanticallyTransparent(resp1, resp2));
}
@Test
public void testCallBackendMakesBackEndRequestAndHandlesResponse() throws Exception {
mockImplMethods(GET_CURRENT_DATE, HANDLE_BACKEND_RESPONSE);

View File

@ -1319,7 +1319,7 @@ public abstract class TestCachingExecChain {
isA(HttpRequest.class)))
.andThrow(new IOException()).anyTimes();
expect(mockCache.cacheAndReturnResponse(eq(host),
isA(HttpRequest.class), isA(HttpResponse.class),
isA(HttpRequest.class), isA(CloseableHttpResponse.class),
isA(Date.class), isA(Date.class)))
.andReturn(resp).anyTimes();
expect(mockBackend.execute(

View File

@ -2899,7 +2899,7 @@ public class TestProtocolRequirements extends AbstractProtocolTest {
EasyMock.expect(mockCache.cacheAndReturnResponse(
EasyMock.isA(HttpHost.class),
EasyMock.isA(HttpRequestWrapper.class),
eqResponse(validated),
eqCloseableResponse(validated),
EasyMock.isA(Date.class),
EasyMock.isA(Date.class))).andReturn(reconstructed).times(0, 1);