HTTPCLIENT-995: cache returns cached responses even if validators not consistent with all conditional headers

Contributed by Jonathan Moore <jonathan_moore at comcast.com>


git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@996644 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Oleg Kalnichevski 2010-09-13 19:05:19 +00:00
parent 05deb28605
commit ff55577526
4 changed files with 131 additions and 40 deletions

View File

@ -30,7 +30,6 @@ import java.util.Date;
import org.apache.http.Header; import org.apache.http.Header;
import org.apache.http.HeaderElement; import org.apache.http.HeaderElement;
import org.apache.http.HttpRequest;
import org.apache.http.annotation.Immutable; import org.apache.http.annotation.Immutable;
import org.apache.http.client.cache.HeaderConstants; import org.apache.http.client.cache.HeaderConstants;
import org.apache.http.client.cache.HttpCacheEntry; import org.apache.http.client.cache.HttpCacheEntry;
@ -79,28 +78,6 @@ class CacheValidityPolicy {
|| entry.getFirstHeader(HeaderConstants.LAST_MODIFIED) != null; || entry.getFirstHeader(HeaderConstants.LAST_MODIFIED) != null;
} }
public boolean modifiedSince(final HttpCacheEntry entry, final HttpRequest request) {
Header unmodHeader = request.getFirstHeader(HeaderConstants.IF_UNMODIFIED_SINCE);
if (unmodHeader == null) {
return false;
}
try {
Date unmodifiedSinceDate = DateUtils.parseDate(unmodHeader.getValue());
Date lastModifiedDate = DateUtils.parseDate(entry.getFirstHeader(
HeaderConstants.LAST_MODIFIED).getValue());
if (unmodifiedSinceDate.before(lastModifiedDate)) {
return true;
}
} catch (DateParseException e) {
return false;
}
return false;
}
public boolean mustRevalidate(final HttpCacheEntry entry) { public boolean mustRevalidate(final HttpCacheEntry entry) {
return hasCacheControlDirective(entry, "must-revalidate"); return hasCacheControlDirective(entry, "must-revalidate");
} }

View File

@ -26,6 +26,8 @@
*/ */
package org.apache.http.impl.client.cache; package org.apache.http.impl.client.cache;
import java.util.Date;
import org.apache.commons.logging.Log; 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;
@ -35,6 +37,8 @@ import org.apache.http.HttpRequest;
import org.apache.http.annotation.Immutable; import org.apache.http.annotation.Immutable;
import org.apache.http.client.cache.HeaderConstants; import org.apache.http.client.cache.HeaderConstants;
import org.apache.http.client.cache.HttpCacheEntry; import org.apache.http.client.cache.HttpCacheEntry;
import org.apache.http.impl.cookie.DateParseException;
import org.apache.http.impl.cookie.DateUtils;
/** /**
* Determines whether a given {@link HttpCacheEntry} is suitable to be * Determines whether a given {@link HttpCacheEntry} is suitable to be
@ -81,8 +85,13 @@ class CachedResponseSuitabilityChecker {
return false; return false;
} }
if (validityStrategy.modifiedSince(entry, request)) { if (hasUnsupportedConditionalHeaders(request)) {
log.debug("Cache entry modified times didn't line up. Cache Entry should not be used"); log.debug("Request contained conditional headers we don't handle");
return false;
}
if (containsEtagAndLastModifiedValidators(request)
&& !allConditionalsMatch(request, entry)) {
return false; return false;
} }
@ -146,4 +155,93 @@ class CachedResponseSuitabilityChecker {
log.debug("Response from cache was suitable"); log.debug("Response from cache was suitable");
return true; return true;
} }
private boolean hasUnsupportedConditionalHeaders(HttpRequest request) {
return (request.getFirstHeader("If-Range") != null
|| request.getFirstHeader("If-Match") != null
|| hasValidDateField(request, "If-Unmodified-Since"));
}
/**
* Should return false if some conditionals would allow a
* normal request but some would not.
* @param request
* @param entry
* @return
*/
private boolean allConditionalsMatch(HttpRequest request,
HttpCacheEntry entry) {
Header etagHeader = entry.getFirstHeader("ETag");
String etag = (etagHeader != null) ? etagHeader.getValue() : null;
Header[] ifNoneMatch = request.getHeaders("If-None-Match");
if (ifNoneMatch != null && ifNoneMatch.length > 0) {
boolean matched = false;
for(Header h : ifNoneMatch) {
for(HeaderElement elt : h.getElements()) {
String reqEtag = elt.toString();
if (("*".equals(reqEtag) && etag != null)
|| reqEtag.equals(etag)) {
matched = true;
break;
}
}
}
if (!matched) return false;
}
Header lmHeader = entry.getFirstHeader("Last-Modified");
Date lastModified = null;
try {
if (lmHeader != null) {
lastModified = DateUtils.parseDate(lmHeader.getValue());
}
} catch (DateParseException dpe) {
// nop
}
for(Header h : request.getHeaders("If-Modified-Since")) {
try {
Date cond = DateUtils.parseDate(h.getValue());
if (lastModified == null
|| lastModified.after(cond)) {
return false;
}
} catch (DateParseException dpe) {
}
}
return true;
}
private boolean containsEtagAndLastModifiedValidators(HttpRequest request) {
boolean hasEtagValidators = (hasEtagIfRangeHeader(request)
|| request.getFirstHeader("If-Match") != null
|| request.getFirstHeader("If-None-Match") != null);
if (!hasEtagValidators) return false;
final boolean hasLastModifiedValidators =
hasValidDateField(request, "If-Modified-Since")
|| hasValidDateField(request, "If-Unmodified-Since")
|| hasValidDateField(request, "If-Range");
return hasLastModifiedValidators;
}
private boolean hasEtagIfRangeHeader(HttpRequest request) {
for(Header h : request.getHeaders("If-Range")) {
try {
DateUtils.parseDate(h.getValue());
} catch (DateParseException dpe) {
return true;
}
}
return false;
}
private boolean hasValidDateField(HttpRequest request, String headerName) {
for(Header h : request.getHeaders(headerName)) {
try {
DateUtils.parseDate(h.getValue());
return true;
} catch (DateParseException dpe) {
// ignore malformed dates
}
}
return false;
}
} }

View File

@ -78,7 +78,6 @@ public class TestCachedResponseSuitabilityChecker {
public void testSuitableIfContentLengthHeaderIsRight() { public void testSuitableIfContentLengthHeaderIsRight() {
responseIsFresh(true); responseIsFresh(true);
contentLengthMatchesActualLength(true); contentLengthMatchesActualLength(true);
modifiedSince(false, request);
replayMocks(); replayMocks();
boolean result = impl.canCachedResponseBeUsed(host, request, entry); boolean result = impl.canCachedResponseBeUsed(host, request, entry);
@ -92,7 +91,6 @@ public class TestCachedResponseSuitabilityChecker {
public void testSuitableIfCacheEntryIsFresh() { public void testSuitableIfCacheEntryIsFresh() {
responseIsFresh(true); responseIsFresh(true);
contentLengthMatchesActualLength(true); contentLengthMatchesActualLength(true);
modifiedSince(false, request);
replayMocks(); replayMocks();
@ -120,7 +118,6 @@ public class TestCachedResponseSuitabilityChecker {
request.addHeader("Cache-Control", "no-cache"); request.addHeader("Cache-Control", "no-cache");
responseIsFresh(true); responseIsFresh(true);
contentLengthMatchesActualLength(true); contentLengthMatchesActualLength(true);
modifiedSince(false, request);
replayMocks(); replayMocks();
@ -134,7 +131,6 @@ public class TestCachedResponseSuitabilityChecker {
request.addHeader("Cache-Control", "max-age=10"); request.addHeader("Cache-Control", "max-age=10");
responseIsFresh(true); responseIsFresh(true);
contentLengthMatchesActualLength(true); contentLengthMatchesActualLength(true);
modifiedSince(false, request);
currentAge(20L); currentAge(20L);
replayMocks(); replayMocks();
@ -149,7 +145,6 @@ public class TestCachedResponseSuitabilityChecker {
request.addHeader("Cache-Control", "max-age=10"); request.addHeader("Cache-Control", "max-age=10");
responseIsFresh(true); responseIsFresh(true);
contentLengthMatchesActualLength(true); contentLengthMatchesActualLength(true);
modifiedSince(false, request);
currentAge(5L); currentAge(5L);
replayMocks(); replayMocks();
@ -164,7 +159,6 @@ public class TestCachedResponseSuitabilityChecker {
request.addHeader("Cache-Control", "min-fresh=10"); request.addHeader("Cache-Control", "min-fresh=10");
responseIsFresh(true); responseIsFresh(true);
contentLengthMatchesActualLength(true); contentLengthMatchesActualLength(true);
modifiedSince(false, request);
freshnessLifetime(15L); freshnessLifetime(15L);
replayMocks(); replayMocks();
@ -179,7 +173,6 @@ public class TestCachedResponseSuitabilityChecker {
request.addHeader("Cache-Control", "min-fresh=10"); request.addHeader("Cache-Control", "min-fresh=10");
responseIsFresh(true); responseIsFresh(true);
contentLengthMatchesActualLength(true); contentLengthMatchesActualLength(true);
modifiedSince(false, request);
freshnessLifetime(5L); freshnessLifetime(5L);
replayMocks(); replayMocks();
@ -208,7 +201,6 @@ public class TestCachedResponseSuitabilityChecker {
request.addHeader(new BasicHeader("Cache-Control", "max-age=foo")); request.addHeader(new BasicHeader("Cache-Control", "max-age=foo"));
responseIsFresh(true); responseIsFresh(true);
contentLengthMatchesActualLength(true); contentLengthMatchesActualLength(true);
modifiedSince(false, request);
replayMocks(); replayMocks();
@ -225,7 +217,6 @@ public class TestCachedResponseSuitabilityChecker {
responseIsFresh(true); responseIsFresh(true);
contentLengthMatchesActualLength(true); contentLengthMatchesActualLength(true);
modifiedSince(false, request);
replayMocks(); replayMocks();
@ -251,11 +242,6 @@ public class TestCachedResponseSuitabilityChecker {
mockValidityPolicy.isResponseFresh(entry)).andReturn(fresh); mockValidityPolicy.isResponseFresh(entry)).andReturn(fresh);
} }
private void modifiedSince(boolean modified, HttpRequest request) {
EasyMock.expect(
mockValidityPolicy.modifiedSince(entry, request)).andReturn(modified);
}
private void contentLengthMatchesActualLength(boolean b) { private void contentLengthMatchesActualLength(boolean b) {
EasyMock.expect( EasyMock.expect(
mockValidityPolicy.contentLengthHeaderMatchesActualLength(entry)).andReturn(b); mockValidityPolicy.contentLengthHeaderMatchesActualLength(entry)).andReturn(b);

View File

@ -2878,7 +2878,7 @@ public class TestProtocolRequirements extends AbstractProtocolTest {
HttpRequest req2 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1); HttpRequest req2 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req2.setHeader("If-None-Match", "W/\"etag\""); req2.setHeader("If-None-Match", "W/\"etag\"");
req2.setHeader("If-Unmodified-Since", DateUtils.formatDate(twentySecondsAgo)); req2.setHeader("If-Modified-Since", DateUtils.formatDate(twentySecondsAgo));
// must hit the origin again for the second request // must hit the origin again for the second request
EasyMock.expect( EasyMock.expect(
@ -2893,6 +2893,36 @@ public class TestProtocolRequirements extends AbstractProtocolTest {
Assert.assertFalse(HttpStatus.SC_NOT_MODIFIED == result.getStatusLine().getStatusCode()); Assert.assertFalse(HttpStatus.SC_NOT_MODIFIED == result.getStatusLine().getStatusCode());
} }
@Test
public void testConditionalRequestWhereAllValidatorsMatchMayBeServedFromCache()
throws Exception {
Date now = new Date();
Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
HttpRequest req1 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
HttpResponse resp1 = make200Response();
resp1.setHeader("Date", DateUtils.formatDate(now));
resp1.setHeader("Cache-Control", "max-age=3600");
resp1.setHeader("Last-Modified", DateUtils.formatDate(tenSecondsAgo));
resp1.setHeader("ETag", "W/\"etag\"");
HttpRequest req2 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req2.setHeader("If-None-Match", "W/\"etag\"");
req2.setHeader("If-Modified-Since", DateUtils.formatDate(tenSecondsAgo));
// may hit the origin again for the second request
EasyMock.expect(
mockBackend.execute(EasyMock.isA(HttpHost.class),
EasyMock.isA(HttpRequest.class),
(HttpContext) EasyMock.isNull())).andReturn(resp1).times(1,2);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
verifyMocks();
}
/* /*
* "However, a cache that does not support the Range and Content-Range * "However, a cache that does not support the Range and Content-Range
* headers MUST NOT cache 206 (Partial Content) responses." * headers MUST NOT cache 206 (Partial Content) responses."