HTTPCLIENT-958: Client cache no longer allows incomplete responses to be passed on to the client

Contributed by Jonathan Moore <jonathan_moore at comcast.com>


git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@959941 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Oleg Kalnichevski 2010-07-02 10:31:34 +00:00
parent c80f04cb45
commit 9b8fb9f02a
5 changed files with 820 additions and 23 deletions

View File

@ -1,6 +1,10 @@
Changes since 4.1 ALPHA2
-------------------
* [HTTPCLIENT-958] Client cache no longer allows incomplete responses to be
passed on to the client.
Contributed by Jonathan Moore <jonathan_moore at comcast.com>
* [HTTPCLIENT-951] Non-repeatable entity enclosing requests are not correctly
retried when 'expect-continue' handshake is active.
Contributed by Oleg Kalnichevski <olegk at apache.org>

View File

@ -34,6 +34,7 @@ import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
@ -51,6 +52,7 @@ import org.apache.http.client.cache.HttpCacheOperationException;
import org.apache.http.client.cache.HttpCacheUpdateCallback;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.message.BasicStatusLine;
@ -533,6 +535,32 @@ public class CachingHttpClient implements HttpClient {
}
}
protected HttpResponse correctIncompleteResponse(HttpResponse resp,
byte[] bodyBytes) {
int status = resp.getStatusLine().getStatusCode();
if (status != HttpStatus.SC_OK
&& status != HttpStatus.SC_PARTIAL_CONTENT) {
return resp;
}
Header hdr = resp.getFirstHeader("Content-Length");
if (hdr == null) return resp;
int contentLength;
try {
contentLength = Integer.parseInt(hdr.getValue());
} catch (NumberFormatException nfe) {
return resp;
}
if (bodyBytes.length >= contentLength) return resp;
HttpResponse error =
new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_BAD_GATEWAY, "Bad Gateway");
error.setHeader("Content-Type","text/plain;charset=UTF-8");
String msg = String.format("Received incomplete response with Content-Length %d but actual body length %d", contentLength, bodyBytes.length);
byte[] msgBytes = msg.getBytes();
error.setHeader("Content-Length", String.format("%d",msgBytes.length));
error.setEntity(new ByteArrayEntity(msgBytes));
return error;
}
protected HttpResponse handleBackendResponse(
HttpHost target,
HttpRequest request,
@ -545,6 +573,7 @@ public class CachingHttpClient implements HttpClient {
boolean cacheable = responseCachingPolicy.isResponseCacheable(request, backendResponse);
HttpResponse corrected = backendResponse;
if (cacheable) {
SizeLimitedResponseReader responseReader = getResponseReader(backendResponse);
@ -553,13 +582,17 @@ public class CachingHttpClient implements HttpClient {
return responseReader.getReconstructedResponse();
}
CacheEntry entry = cacheEntryGenerator.generateEntry(
requestDate,
responseDate,
backendResponse,
responseReader.getResponseBytes());
storeInCache(target, request, entry);
return responseGenerator.generateResponse(entry);
byte[] responseBytes = responseReader.getResponseBytes();
corrected = correctIncompleteResponse(backendResponse,
responseBytes);
int correctedStatus = corrected.getStatusLine().getStatusCode();
if (HttpStatus.SC_BAD_GATEWAY != correctedStatus) {
CacheEntry entry = cacheEntryGenerator
.generateEntry(requestDate, responseDate, corrected,
responseBytes);
storeInCache(target, request, entry);
return responseGenerator.generateResponse(entry);
}
}
String uri = uriExtractor.getURI(target, request);
@ -568,7 +601,7 @@ public class CachingHttpClient implements HttpClient {
} catch (HttpCacheOperationException ex) {
log.debug("Was unable to remove an entry from the cache based on the uri provided", ex);
}
return backendResponse;
return corrected;
}
protected SizeLimitedResponseReader getResponseReader(HttpResponse backEndResponse) {

View File

@ -27,6 +27,7 @@
package org.apache.http.impl.client.cache;
import java.io.InputStream;
import java.util.Random;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
@ -35,6 +36,7 @@ import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.RequestLine;
import org.apache.http.StatusLine;
import org.apache.http.entity.ByteArrayEntity;
public class HttpTestUtils {
@ -203,4 +205,19 @@ public class HttpTestUtils {
return (equivalent(r1.getRequestLine(), r2.getRequestLine()) && isEndToEndHeaderSubset(r1,
r2));
}
public static byte[] getRandomBytes(int nbytes) {
byte[] bytes = new byte[nbytes];
(new Random()).nextBytes(bytes);
return bytes;
}
/** Generates a response body with random content.
* @param nbytes length of the desired response body
* @return an {@link HttpEntity}
*/
public static HttpEntity makeBody(int nbytes) {
return new ByteArrayEntity(getRandomBytes(nbytes));
}
}

View File

@ -26,7 +26,26 @@
*/
package org.apache.http.impl.client.cache;
import org.apache.http.*;
import static junit.framework.Assert.assertTrue;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.ProtocolException;
import org.apache.http.ProtocolVersion;
import org.apache.http.RequestLine;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
@ -41,6 +60,8 @@ import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.message.BasicStatusLine;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HttpContext;
import org.easymock.classextension.EasyMock;
@ -49,15 +70,6 @@ import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import static junit.framework.Assert.assertTrue;
public class TestCachingHttpClient {
private static final ProtocolVersion HTTP_1_1 = new ProtocolVersion("HTTP",1,1);
@ -246,6 +258,8 @@ public class TestCachingHttpClient {
generateCacheEntry(requestDate, responseDate, buf);
storeInCacheWasCalled();
responseIsGeneratedFromCache();
responseStatusLineIsInspectable();
responseDoesNotHaveExplicitContentLength();
replayMocks();
HttpResponse result = impl.handleBackendResponse(host, mockRequest, requestDate,
@ -597,6 +611,8 @@ public class TestCachingHttpClient {
generateCacheEntry(requestDate, responseDate, buf);
storeInCacheWasCalled();
responseIsGeneratedFromCache();
responseStatusLineIsInspectable();
responseDoesNotHaveExplicitContentLength();
replayMocks();
@ -927,6 +943,117 @@ public class TestCachingHttpClient {
Assert.assertTrue(gotException);
}
@Test
public void testCorrectIncompleteResponseDoesNotCorrectComplete200Response()
throws Exception {
HttpResponse resp = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_OK, "OK");
byte[] bytes = HttpTestUtils.getRandomBytes(128);
resp.setEntity(new ByteArrayEntity(bytes));
resp.setHeader("Content-Length","128");
HttpResponse result = impl.correctIncompleteResponse(resp, bytes);
Assert.assertTrue(HttpTestUtils.semanticallyTransparent(resp, result));
}
@Test
public void testCorrectIncompleteResponseDoesNotCorrectComplete206Response()
throws Exception {
HttpResponse resp = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
byte[] bytes = HttpTestUtils.getRandomBytes(128);
resp.setEntity(new ByteArrayEntity(bytes));
resp.setHeader("Content-Length","128");
resp.setHeader("Content-Range","bytes 0-127/255");
HttpResponse result = impl.correctIncompleteResponse(resp, bytes);
Assert.assertTrue(HttpTestUtils.semanticallyTransparent(resp, result));
}
@Test
public void testCorrectIncompleteResponseGenerates502ForIncomplete200Response()
throws Exception {
HttpResponse resp = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_OK, "OK");
byte[] bytes = HttpTestUtils.getRandomBytes(128);
resp.setEntity(new ByteArrayEntity(bytes));
resp.setHeader("Content-Length","256");
HttpResponse result = impl.correctIncompleteResponse(resp, bytes);
Assert.assertTrue(HttpStatus.SC_BAD_GATEWAY == result.getStatusLine().getStatusCode());
}
@Test
public void testCorrectIncompleteResponseDoesNotCorrectIncompleteNon200Or206Responses()
throws Exception {
HttpResponse resp = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_FORBIDDEN, "Forbidden");
byte[] bytes = HttpTestUtils.getRandomBytes(128);
resp.setEntity(new ByteArrayEntity(bytes));
resp.setHeader("Content-Length","256");
HttpResponse result = impl.correctIncompleteResponse(resp, bytes);
Assert.assertTrue(HttpTestUtils.semanticallyTransparent(resp, result));
}
@Test
public void testCorrectIncompleteResponseDoesNotCorrectResponsesWithoutExplicitContentLength()
throws Exception {
HttpResponse resp = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_OK, "OK");
byte[] bytes = HttpTestUtils.getRandomBytes(128);
resp.setEntity(new ByteArrayEntity(bytes));
HttpResponse result = impl.correctIncompleteResponse(resp, bytes);
Assert.assertTrue(HttpTestUtils.semanticallyTransparent(resp, result));
}
@Test
public void testCorrectIncompleteResponseDoesNotCorrectResponsesWithUnparseableContentLengthHeader()
throws Exception {
HttpResponse resp = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_OK, "OK");
byte[] bytes = HttpTestUtils.getRandomBytes(128);
resp.setHeader("Content-Length","foo");
resp.setEntity(new ByteArrayEntity(bytes));
HttpResponse result = impl.correctIncompleteResponse(resp, bytes);
Assert.assertTrue(HttpTestUtils.semanticallyTransparent(resp, result));
}
@Test
public void testCorrectIncompleteResponseProvidesPlainTextErrorMessage()
throws Exception {
HttpResponse resp = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_OK, "OK");
byte[] bytes = HttpTestUtils.getRandomBytes(128);
resp.setEntity(new ByteArrayEntity(bytes));
resp.setHeader("Content-Length","256");
HttpResponse result = impl.correctIncompleteResponse(resp, bytes);
Header ctype = result.getFirstHeader("Content-Type");
Assert.assertEquals("text/plain;charset=UTF-8", ctype.getValue());
}
@Test
public void testCorrectIncompleteResponseProvidesNonEmptyErrorMessage()
throws Exception {
HttpResponse resp = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_OK, "OK");
byte[] bytes = HttpTestUtils.getRandomBytes(128);
resp.setEntity(new ByteArrayEntity(bytes));
resp.setHeader("Content-Length","256");
HttpResponse result = impl.correctIncompleteResponse(resp, bytes);
int clen = Integer.parseInt(result.getFirstHeader("Content-Length").getValue());
Assert.assertTrue(clen > 0);
HttpEntity body = result.getEntity();
if (body.getContentLength() < 0) {
InputStream is = body.getContent();
int bytes_read = 0;
while((is.read()) != -1) {
bytes_read++;
}
is.close();
Assert.assertEquals(clen, bytes_read);
} else {
Assert.assertTrue(body.getContentLength() == clen);
}
}
private byte[] readResponse(HttpResponse response) {
try {
ByteArrayOutputStream s1 = new ByteArrayOutputStream();
@ -998,6 +1125,11 @@ public class TestCachingHttpClient {
allow);
}
private void responseDoesNotHaveExplicitContentLength() {
EasyMock.expect(mockBackendResponse.getFirstHeader("Content-Length"))
.andReturn(null).anyTimes();
}
private byte[] responseReaderReturnsBufferOfSize(int bufferSize) {
byte[] buffer = new byte[bufferSize];
org.easymock.EasyMock.expect(mockResponseReader.getResponseBytes()).andReturn(buffer);
@ -1051,8 +1183,14 @@ public class TestCachingHttpClient {
}
private void responseIsGeneratedFromCache() {
org.easymock.EasyMock.expect(mockResponseGenerator.generateResponse(mockCacheEntry))
.andReturn(mockCachedResponse);
EasyMock.expect(mockResponseGenerator.generateResponse(mockCacheEntry))
.andReturn(mockCachedResponse);
}
private void responseStatusLineIsInspectable() {
StatusLine statusLine = new BasicStatusLine(HTTP_1_1, HttpStatus.SC_OK, "OK");
EasyMock.expect(mockBackendResponse.getStatusLine())
.andReturn(statusLine).anyTimes();
}
private void responseIsGeneratedFromCache(CacheEntry entry) {

View File

@ -126,9 +126,7 @@ public class TestProtocolRequirements {
}
private HttpEntity makeBody(int nbytes) {
byte[] bytes = new byte[nbytes];
(new Random()).nextBytes(bytes);
return new ByteArrayEntity(bytes);
return HttpTestUtils.makeBody(nbytes);
}
private IExpectationSetters<HttpResponse> backendExpectsAnyRequest() throws Exception {
@ -3663,6 +3661,613 @@ public class TestProtocolRequirements {
HttpTestUtils.getCanonicalHeaderValue(result2, h));
}
/* "If a cache has a stored non-empty set of subranges for an
* entity, and an incoming response transfers another subrange,
* the cache MAY combine the new subrange with the existing set if
* both the following conditions are met:
*
* - Both the incoming response and the cache entry have a cache
* validator.
*
* - The two cache validators match using the strong comparison
* function (see section 13.3.3).
*
* If either requirement is not met, the cache MUST use only the
* most recent partial response (based on the Date values
* transmitted with every response, and using the incoming
* response if these values are equal or missing), and MUST
* discard the other partial information."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.4
*/
@Test
public void testCannotCombinePartialResponseIfIncomingResponseDoesNotHaveACacheValidator()
throws Exception {
HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req1.setHeader("Range","bytes=0-49");
Date now = new Date();
Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
HttpResponse resp1 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
resp1.setEntity(makeBody(50));
resp1.setHeader("Server","MockServer/1.0");
resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("Content-Range","bytes 0-49/128");
resp1.setHeader("ETag","\"etag1\"");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req2.setHeader("Range","bytes=50-127");
HttpResponse resp2 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
resp2.setEntity(makeBody(78));
resp2.setHeader("Cache-Control","max-age=3600");
resp2.setHeader("Content-Range","bytes 50-127/128");
resp2.setHeader("Server","MockServer/1.0");
resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
backendExpectsAnyRequest().andReturn(resp2);
HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1);
HttpResponse resp3 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_OK, "OK");
resp3.setEntity(makeBody(128));
resp3.setHeader("Server","MockServer/1.0");
resp3.setHeader("Date", DateUtils.formatDate(now));
backendExpectsAnyRequest().andReturn(resp3);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
impl.execute(host, req3);
verifyMocks();
}
@Test
public void testCannotCombinePartialResponseIfCacheEntryDoesNotHaveACacheValidator()
throws Exception {
Date now = new Date();
Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req1.setHeader("Range","bytes=0-49");
HttpResponse resp1 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
resp1.setEntity(makeBody(50));
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("Content-Range","bytes 0-49/128");
resp1.setHeader("Server","MockServer/1.0");
resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req2.setHeader("Range","bytes=50-127");
HttpResponse resp2 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
resp2.setEntity(makeBody(78));
resp2.setHeader("Cache-Control","max-age=3600");
resp2.setHeader("Content-Range","bytes 50-127/128");
resp2.setHeader("ETag","\"etag1\"");
resp2.setHeader("Server","MockServer/1.0");
resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
backendExpectsAnyRequest().andReturn(resp2);
HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1);
HttpResponse resp3 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_OK, "OK");
resp3.setEntity(makeBody(128));
resp3.setHeader("Server","MockServer/1.0");
resp3.setHeader("Date", DateUtils.formatDate(now));
backendExpectsAnyRequest().andReturn(resp3);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
impl.execute(host, req3);
verifyMocks();
}
@Test
public void testCannotCombinePartialResponseIfCacheValidatorsDoNotStronglyMatch()
throws Exception {
Date now = new Date();
Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req1.setHeader("Range","bytes=0-49");
HttpResponse resp1 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
resp1.setEntity(makeBody(50));
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("Content-Range","bytes 0-49/128");
resp1.setHeader("ETag","\"etag1\"");
resp1.setHeader("Server","MockServer/1.0");
resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req2.setHeader("Range","bytes=50-127");
HttpResponse resp2 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
resp2.setEntity(makeBody(78));
resp2.setHeader("Cache-Control","max-age=3600");
resp2.setHeader("Content-Range","bytes 50-127/128");
resp2.setHeader("ETag","\"etag2\"");
resp2.setHeader("Server","MockServer/1.0");
resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
backendExpectsAnyRequest().andReturn(resp2);
HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1);
HttpResponse resp3 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_OK, "OK");
resp3.setEntity(makeBody(128));
resp3.setHeader("Server","MockServer/1.0");
resp3.setHeader("Date", DateUtils.formatDate(now));
backendExpectsAnyRequest().andReturn(resp3);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
impl.execute(host, req3);
verifyMocks();
}
@Test
public void testMustDiscardLeastRecentPartialResponseIfIncomingRequestDoesNotHaveCacheValidator()
throws Exception {
Date now = new Date();
Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req1.setHeader("Range","bytes=0-49");
HttpResponse resp1 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
resp1.setEntity(makeBody(50));
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("Content-Range","bytes 0-49/128");
resp1.setHeader("ETag","\"etag1\"");
resp1.setHeader("Server","MockServer/1.0");
resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req2.setHeader("Range","bytes=50-127");
HttpResponse resp2 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
resp2.setEntity(makeBody(78));
resp2.setHeader("Cache-Control","max-age=3600");
resp2.setHeader("Content-Range","bytes 50-127/128");
resp2.setHeader("Server","MockServer/1.0");
resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
backendExpectsAnyRequest().andReturn(resp2);
HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req3.setHeader("Range","bytes=0-49");
HttpResponse resp3 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_OK, "OK");
resp3.setEntity(makeBody(128));
resp3.setHeader("Server","MockServer/1.0");
resp3.setHeader("Date", DateUtils.formatDate(now));
// must make this request; cannot serve from cache
backendExpectsAnyRequest().andReturn(resp3);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
impl.execute(host, req3);
verifyMocks();
}
@Test
public void testMustDiscardLeastRecentPartialResponseIfCachedResponseDoesNotHaveCacheValidator()
throws Exception {
Date now = new Date();
Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req1.setHeader("Range","bytes=0-49");
HttpResponse resp1 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
resp1.setEntity(makeBody(50));
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("Content-Range","bytes 0-49/128");
resp1.setHeader("Server","MockServer/1.0");
resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req2.setHeader("Range","bytes=50-127");
HttpResponse resp2 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
resp2.setEntity(makeBody(78));
resp2.setHeader("Cache-Control","max-age=3600");
resp2.setHeader("Content-Range","bytes 50-127/128");
resp2.setHeader("ETag","\"etag1\"");
resp2.setHeader("Server","MockServer/1.0");
resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
backendExpectsAnyRequest().andReturn(resp2);
HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req3.setHeader("Range","bytes=0-49");
HttpResponse resp3 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_OK, "OK");
resp3.setEntity(makeBody(128));
resp3.setHeader("Server","MockServer/1.0");
resp3.setHeader("Date", DateUtils.formatDate(now));
// must make this request; cannot serve from cache
backendExpectsAnyRequest().andReturn(resp3);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
impl.execute(host, req3);
verifyMocks();
}
@Test
public void testMustDiscardLeastRecentPartialResponseIfCacheValidatorsDoNotStronglyMatch()
throws Exception {
Date now = new Date();
Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req1.setHeader("Range","bytes=0-49");
HttpResponse resp1 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
resp1.setEntity(makeBody(50));
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("Content-Range","bytes 0-49/128");
resp1.setHeader("Etag","\"etag1\"");
resp1.setHeader("Server","MockServer/1.0");
resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req2.setHeader("Range","bytes=50-127");
HttpResponse resp2 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
resp2.setEntity(makeBody(78));
resp2.setHeader("Cache-Control","max-age=3600");
resp2.setHeader("Content-Range","bytes 50-127/128");
resp2.setHeader("ETag","\"etag2\"");
resp2.setHeader("Server","MockServer/1.0");
resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
backendExpectsAnyRequest().andReturn(resp2);
HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req3.setHeader("Range","bytes=0-49");
HttpResponse resp3 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_OK, "OK");
resp3.setEntity(makeBody(128));
resp3.setHeader("Server","MockServer/1.0");
resp3.setHeader("Date", DateUtils.formatDate(now));
// must make this request; cannot serve from cache
backendExpectsAnyRequest().andReturn(resp3);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
impl.execute(host, req3);
verifyMocks();
}
@Test
public void testMustDiscardLeastRecentPartialResponseIfCacheValidatorsDoNotStronglyMatchEvenIfResponsesOutOfOrder()
throws Exception {
Date now = new Date();
Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req1.setHeader("Range","bytes=0-49");
HttpResponse resp1 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
resp1.setEntity(makeBody(50));
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("Content-Range","bytes 0-49/128");
resp1.setHeader("Etag","\"etag1\"");
resp1.setHeader("Server","MockServer/1.0");
resp1.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req2.setHeader("Range","bytes=50-127");
HttpResponse resp2 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
resp2.setEntity(makeBody(78));
resp2.setHeader("Cache-Control","max-age=3600");
resp2.setHeader("Content-Range","bytes 50-127/128");
resp2.setHeader("ETag","\"etag2\"");
resp2.setHeader("Server","MockServer/1.0");
resp2.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
backendExpectsAnyRequest().andReturn(resp2);
HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req3.setHeader("Range","bytes=50-127");
HttpResponse resp3 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_OK, "OK");
resp3.setEntity(makeBody(128));
resp3.setHeader("Server","MockServer/1.0");
resp3.setHeader("Date", DateUtils.formatDate(now));
// must make this request; cannot serve from cache
backendExpectsAnyRequest().andReturn(resp3);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
impl.execute(host, req3);
verifyMocks();
}
@Test
public void testMustDiscardCachedPartialResponseIfCacheValidatorsDoNotStronglyMatchAndDateHeadersAreEqual()
throws Exception {
Date now = new Date();
Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req1.setHeader("Range","bytes=0-49");
HttpResponse resp1 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
resp1.setEntity(makeBody(50));
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("Content-Range","bytes 0-49/128");
resp1.setHeader("Etag","\"etag1\"");
resp1.setHeader("Server","MockServer/1.0");
resp1.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req2.setHeader("Range","bytes=50-127");
HttpResponse resp2 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
resp2.setEntity(makeBody(78));
resp2.setHeader("Cache-Control","max-age=3600");
resp2.setHeader("Content-Range","bytes 50-127/128");
resp2.setHeader("ETag","\"etag2\"");
resp2.setHeader("Server","MockServer/1.0");
resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
backendExpectsAnyRequest().andReturn(resp2);
HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1);
req3.setHeader("Range","bytes=0-49");
HttpResponse resp3 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_OK, "OK");
resp3.setEntity(makeBody(128));
resp3.setHeader("Server","MockServer/1.0");
resp3.setHeader("Date", DateUtils.formatDate(now));
// must make this request; cannot serve from cache
backendExpectsAnyRequest().andReturn(resp3);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
impl.execute(host, req3);
verifyMocks();
}
/* "When the cache receives a subsequent request whose Request-URI
* specifies one or more cache entries including a Vary header
* field, the cache MUST NOT use such a cache entry to construct a
* response to the new request unless all of the selecting
* request-headers present in the new request match the
* corresponding stored request-headers in the original request."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
*/
@Test
public void testCannotUseVariantCacheEntryIfNotAllSelectingRequestHeadersMatch()
throws Exception {
HttpRequest req1 = new BasicHttpRequest("GET","/",HTTP_1_1);
req1.setHeader("Accept-Encoding","gzip");
HttpResponse resp1 = make200Response();
resp1.setHeader("ETag","\"etag1\"");
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("Vary","Accept-Encoding");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET","/",HTTP_1_1);
req2.removeHeaders("Accept-Encoding");
HttpResponse resp2 = make200Response();
resp2.setHeader("ETag","\"etag1\"");
resp2.setHeader("Cache-Control","max-age=3600");
// not allowed to have a cache hit; must forward request
backendExpectsAnyRequest().andReturn(resp2);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
verifyMocks();
}
/* "A Vary header field-value of "*" always fails to match and
* subsequent requests on that resource can only be properly
* interpreted by the origin server."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
*/
@Test
public void testCannotServeFromCacheForVaryStar() throws Exception {
HttpRequest req1 = new BasicHttpRequest("GET","/",HTTP_1_1);
HttpResponse resp1 = make200Response();
resp1.setHeader("ETag","\"etag1\"");
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("Vary","*");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET","/",HTTP_1_1);
HttpResponse resp2 = make200Response();
resp2.setHeader("ETag","\"etag1\"");
resp2.setHeader("Cache-Control","max-age=3600");
// not allowed to have a cache hit; must forward request
backendExpectsAnyRequest().andReturn(resp2);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
verifyMocks();
}
/* " If the selecting request header fields for the cached entry
* do not match the selecting request header fields of the new
* request, then the cache MUST NOT use a cached entry to satisfy
* the request unless it first relays the new request to the
* origin server in a conditional request and the server responds
* with 304 (Not Modified), including an entity tag or
* Content-Location that indicates the entity to be used.
*
* If an entity tag was assigned to a cached representation, the
* forwarded request SHOULD be conditional and include the entity
* tags in an If-None-Match header field from all its cache
* entries for the resource. This conveys to the server the set of
* entities currently held by the cache, so that if any one of
* these entities matches the requested entity, the server can use
* the ETag header field in its 304 (Not Modified) response to
* tell the cache which entry is appropriate. If the entity-tag of
* the new response matches that of an existing entry, the new
* response SHOULD be used to update the header fields of the
* existing entry, and the result MUST be returned to the client.
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
*/
@Test
public void testNonmatchingVariantCannotBeServedFromCacheUnlessConditionallyValidated()
throws Exception {
HttpRequest req1 = new BasicHttpRequest("GET","/",HTTP_1_1);
req1.setHeader("User-Agent","MyBrowser/1.0");
HttpResponse resp1 = make200Response();
resp1.setHeader("ETag","\"etag1\"");
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("Vary","User-Agent");
resp1.setHeader("Content-Type","application/octet-stream");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET","/",HTTP_1_1);
req2.setHeader("User-Agent","MyBrowser/1.5");
HttpRequest conditional = new BasicHttpRequest("GET","/",HTTP_1_1);
conditional.setHeader("User-Agent","MyBrowser/1.5");
conditional.setHeader("If-None-Match","\"etag1\"");
HttpResponse resp200 = make200Response();
resp200.setHeader("ETag","\"etag1\"");
resp200.setHeader("Vary","User-Agent");
HttpResponse resp304 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
resp304.setHeader("ETag","\"etag1\"");
resp304.setHeader("Vary","User-Agent");
Capture<HttpRequest> condCap = new Capture<HttpRequest>();
Capture<HttpRequest> uncondCap = new Capture<HttpRequest>();
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
EasyMock.and(eqRequest(conditional),
EasyMock.capture(condCap)),
(HttpContext)EasyMock.isNull()))
.andReturn(resp304).times(0,1);
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
EasyMock.and(eqRequest(req2),
EasyMock.capture(uncondCap)),
(HttpContext)EasyMock.isNull()))
.andReturn(resp200).times(0,1);
replayMocks();
impl.execute(host, req1);
HttpResponse result = impl.execute(host, req2);
verifyMocks();
if (HttpStatus.SC_OK == result.getStatusLine().getStatusCode()) {
Assert.assertTrue(condCap.hasCaptured()
|| uncondCap.hasCaptured());
if (uncondCap.hasCaptured()) {
Assert.assertTrue(HttpTestUtils.semanticallyTransparent(resp200, result));
}
}
}
/* "A cache that receives an incomplete response (for example,
* with fewer bytes of data than specified in a Content-Length
* header) MAY store the response. However, the cache MUST treat
* this as a partial response. Partial responses MAY be combined
* as described in section 13.5.4; the result might be a full
* response or might still be partial. A cache MUST NOT return a
* partial response to a client without explicitly marking it as
* such, using the 206 (Partial Content) status code. A cache MUST
* NOT return a partial response using a status code of 200 (OK)."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.8
*/
@Test
public void testIncompleteResponseMustNotBeReturnedToClientWithoutMarkingItAs206() throws Exception {
originResponse.setEntity(makeBody(128));
originResponse.setHeader("Content-Length","256");
backendExpectsAnyRequest().andReturn(originResponse);
replayMocks();
HttpResponse result = impl.execute(host, request);
verifyMocks();
int status = result.getStatusLine().getStatusCode();
Assert.assertFalse(HttpStatus.SC_OK == status);
if (status > 200 && status <= 299
&& HttpTestUtils.equivalent(originResponse.getEntity(),
result.getEntity())) {
Assert.assertTrue(HttpStatus.SC_PARTIAL_CONTENT == status);
}
}
private class FakeHeaderGroup extends HeaderGroup{
public void addHeader(String name, String value){