HTTPCLIENT-963: Fixed handling of 'Cache-Control: no-store' on requests

Contributed by Jonathan Moore <jonathan_moore at comcast.com>



git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@963495 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Oleg Kalnichevski 2010-07-12 21:36:47 +00:00
parent e2efb17400
commit 6aad9ccf99
4 changed files with 227 additions and 4 deletions

View File

@ -5,10 +5,13 @@ Changes since 4.1 ALPHA2
proxy-revalidate Cache-Control directives. proxy-revalidate Cache-Control directives.
Contributed by Jonathan Moore <jonathan_moore at comcast.com> Contributed by Jonathan Moore <jonathan_moore at comcast.com>
* [HTTPCLIENT-964] no-cache directives with field names are no longer transmitted * [HTTPCLIENT-964] 'no-cache' directives with field names are no longer transmitted
downstream. downstream.
Contributed by Jonathan Moore <jonathan_moore at comcast.com> Contributed by Jonathan Moore <jonathan_moore at comcast.com>
* [HTTPCLIENT-963] Fixed handling of 'Cache-Control: no-store' on requests.
Contributed by Jonathan Moore <jonathan_moore at comcast.com>
* [HTTPCLIENT-962] Fixed handling of Authorization headers in shared cache mode. * [HTTPCLIENT-962] Fixed handling of Authorization headers in shared cache mode.
Contributed by Jonathan Moore <jonathan_moore at comcast.com> Contributed by Jonathan Moore <jonathan_moore at comcast.com>

View File

@ -30,6 +30,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.apache.http.Header; import org.apache.http.Header;
import org.apache.http.HeaderElement; import org.apache.http.HeaderElement;
import org.apache.http.HttpMessage;
import org.apache.http.HttpRequest; import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
@ -153,12 +154,12 @@ public class ResponseCachingPolicy {
return false; return false;
} }
protected boolean hasCacheControlParameterFrom(HttpResponse response, String[] params) { protected boolean hasCacheControlParameterFrom(HttpMessage msg, String[] params) {
Header[] cacheControlHeaders = response.getHeaders(HeaderConstants.CACHE_CONTROL); Header[] cacheControlHeaders = msg.getHeaders(HeaderConstants.CACHE_CONTROL);
for (Header header : cacheControlHeaders) { for (Header header : cacheControlHeaders) {
for (HeaderElement elem : header.getElements()) { for (HeaderElement elem : header.getElements()) {
for (String param : params) { for (String param : params) {
if (param.equals(elem.getName())) { if (param.equalsIgnoreCase(elem.getName())) {
return true; return true;
} }
} }
@ -189,6 +190,10 @@ public class ResponseCachingPolicy {
log.debug("Response was not cacheable."); log.debug("Response was not cacheable.");
return false; return false;
} }
String[] uncacheableRequestDirectives = { "no-store" };
if (hasCacheControlParameterFrom(request,uncacheableRequestDirectives)) {
return false;
}
if (request.getRequestLine().getUri().contains("?") && !isExplicitlyCacheable(response)) { if (request.getRequestLine().getUri().contains("?") && !isExplicitlyCacheable(response)) {
log.debug("Response was not cacheable."); log.debug("Response was not cacheable.");

View File

@ -5047,7 +5047,214 @@ public class TestProtocolRequirements {
} }
} }
/* "[The cache control directive] "private" Indicates that all or part of
* the response message is intended for a single user and MUST NOT be
* cached by a shared cache."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
*/
@Test
public void testCacheControlPrivateIsNotCacheableBySharedCache()
throws Exception {
if (impl.isSharedCache()) {
HttpRequest req1 = new BasicHttpRequest("GET","/",HTTP_1_1);
HttpResponse resp1 = make200Response();
resp1.setHeader("Cache-Control","private,max-age=3600");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET","/",HTTP_1_1);
HttpResponse resp2 = make200Response();
// this backend request MUST happen
backendExpectsAnyRequest().andReturn(resp2);
replayMocks();
impl.execute(host,req1);
impl.execute(host,req2);
verifyMocks();
}
}
@Test
public void testCacheControlPrivateOnFieldIsNotReturnedBySharedCache()
throws Exception {
if (impl.isSharedCache()) {
HttpRequest req1 = new BasicHttpRequest("GET","/",HTTP_1_1);
HttpResponse resp1 = make200Response();
resp1.setHeader("X-Personal","stuff");
resp1.setHeader("Cache-Control","private=\"X-Personal\",s-maxage=3600");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET","/",HTTP_1_1);
HttpResponse resp2 = make200Response();
// this backend request MAY happen
backendExpectsAnyRequest().andReturn(resp2).times(0,1);
replayMocks();
impl.execute(host,req1);
HttpResponse result = impl.execute(host,req2);
verifyMocks();
Assert.assertNull(result.getFirstHeader("X-Personal"));
}
}
/* "If the no-cache directive does not specify a field-name, then a
* cache MUST NOT use the response to satisfy a subsequent request
* without successful revalidation with the origin server. This allows
* an origin server to prevent caching even by caches that have been
* configured to return stale responses to client requests."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
*/
@Test
public void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidation()
throws Exception {
HttpRequest req1 = new BasicHttpRequest("GET","/",HTTP_1_1);
HttpResponse resp1 = make200Response();
resp1.setHeader("ETag","\"etag\"");
resp1.setHeader("Cache-Control","no-cache");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET","/",HTTP_1_1);
HttpResponse resp2 = make200Response();
// this MUST happen
backendExpectsAnyRequest().andReturn(resp2);
replayMocks();
impl.execute(host,req1);
impl.execute(host,req2);
verifyMocks();
}
@Test
public void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidationEvenWithContraryIndications()
throws Exception {
HttpRequest req1 = new BasicHttpRequest("GET","/",HTTP_1_1);
HttpResponse resp1 = make200Response();
resp1.setHeader("ETag","\"etag\"");
resp1.setHeader("Cache-Control","no-cache,s-maxage=3600");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET","/",HTTP_1_1);
req2.setHeader("Cache-Control","max-stale=7200");
HttpResponse resp2 = make200Response();
// this MUST happen
backendExpectsAnyRequest().andReturn(resp2);
replayMocks();
impl.execute(host,req1);
impl.execute(host,req2);
verifyMocks();
}
/* "If the no-cache directive does specify one or more field-names, then
* a cache MAY use the response to satisfy a subsequent request, subject
* to any other restrictions on caching. However, the specified
* field-name(s) MUST NOT be sent in the response to a subsequent request
* without successful revalidation with the origin server."
*/
@Test
public void testNoCacheOnFieldIsNotReturnedWithoutRevalidation()
throws Exception {
HttpRequest req1 = new BasicHttpRequest("GET","/",HTTP_1_1);
HttpResponse resp1 = make200Response();
resp1.setHeader("ETag","\"etag\"");
resp1.setHeader("X-Stuff","things");
resp1.setHeader("Cache-Control","no-cache=\"X-Stuff\", max-age=3600");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET","/",HTTP_1_1);
HttpResponse resp2 = make200Response();
resp2.setHeader("ETag","\"etag\"");
resp2.setHeader("X-Stuff","things");
resp2.setHeader("Cache-Control","no-cache=\"X-Stuff\",max-age=3600");
Capture<HttpRequest> cap = new Capture<HttpRequest>();
EasyMock.expect(mockBackend.execute(EasyMock.eq(host),
EasyMock.capture(cap),
(HttpContext)EasyMock.isNull()))
.andReturn(resp2).times(0,1);
replayMocks();
impl.execute(host,req1);
HttpResponse result = impl.execute(host,req2);
verifyMocks();
if (!cap.hasCaptured()) {
Assert.assertNull(result.getFirstHeader("X-Stuff"));
}
}
/* "The purpose of the no-store directive is to prevent the inadvertent
* release or retention of sensitive information (for example, on backup
* tapes). The no-store directive applies to the entire message, and MAY
* be sent either in a response or in a request. If sent in a request, a
* cache MUST NOT store any part of either this request or any response
* to it. If sent in a response, a cache MUST NOT store any part of
* either this response or the request that elicited it. This directive
* applies to both non- shared and shared caches. "MUST NOT store" in
* this context means that the cache MUST NOT intentionally store the
* information in non-volatile storage, and MUST make a best-effort
* attempt to remove the information from volatile storage as promptly
* as possible after forwarding it."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.2
*/
@Test
public void testNoStoreOnRequestIsNotStoredInCache()
throws Exception {
emptyMockCacheExpectsNoPuts();
request.setHeader("Cache-Control","no-store");
backendExpectsAnyRequest().andReturn(originResponse);
replayMocks();
impl.execute(host,request);
verifyMocks();
}
@Test
public void testNoStoreOnRequestIsNotStoredInCacheEvenIfResponseMarkedCacheable()
throws Exception {
emptyMockCacheExpectsNoPuts();
request.setHeader("Cache-Control","no-store");
originResponse.setHeader("Cache-Control","max-age=3600");
backendExpectsAnyRequest().andReturn(originResponse);
replayMocks();
impl.execute(host,request);
verifyMocks();
}
@Test
public void testNoStoreOnResponseIsNotStoredInCache()
throws Exception {
emptyMockCacheExpectsNoPuts();
originResponse.setHeader("Cache-Control","no-store");
backendExpectsAnyRequest().andReturn(originResponse);
replayMocks();
impl.execute(host,request);
verifyMocks();
}
@Test
public void testNoStoreOnResponseIsNotStoredInCacheEvenWithContraryIndicators()
throws Exception {
emptyMockCacheExpectsNoPuts();
originResponse.setHeader("Cache-Control","no-store,max-age=3600");
backendExpectsAnyRequest().andReturn(originResponse);
replayMocks();
impl.execute(host,request);
verifyMocks();
}
private class FakeHeaderGroup extends HeaderGroup{ private class FakeHeaderGroup extends HeaderGroup{

View File

@ -54,6 +54,7 @@ public class TestResponseCachingPolicy {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
policy = new ResponseCachingPolicy(0); policy = new ResponseCachingPolicy(0);
request = new BasicHttpRequest("GET","/",HTTP_1_1);
response = new BasicHttpResponse( response = new BasicHttpResponse(
new BasicStatusLine(HTTP_1_1, HttpStatus.SC_OK, "")); new BasicStatusLine(HTTP_1_1, HttpStatus.SC_OK, ""));
response.setHeader("Date", DateUtils.formatDate(new Date())); response.setHeader("Date", DateUtils.formatDate(new Date()));
@ -299,6 +300,13 @@ public class TestResponseCachingPolicy {
Assert.assertFalse(policy.isResponseCacheable("get", response)); Assert.assertFalse(policy.isResponseCacheable("get", response));
} }
@Test
public void testResponsesToRequestsWithNoStoreAreNotCacheable() {
request.setHeader("Cache-Control","no-store");
response.setHeader("Cache-Control","public");
Assert.assertFalse(policy.isResponseCacheable(request,response));
}
@Test @Test
public void testResponsesWithMultipleAgeHeadersAreNotCacheable() { public void testResponsesWithMultipleAgeHeadersAreNotCacheable() {
response.addHeader("Age", "3"); response.addHeader("Age", "3");