HTTPCLIENT-992: cache should not generate stale responses to requests explicitly requesting first-hand or fresh ones
Contributed by Jonathan Moore <jonathan_moore at comcast.com> git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@995246 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
18d3966ed8
commit
d78939ec1a
|
@ -34,6 +34,8 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
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.HeaderElement;
|
||||||
import org.apache.http.HttpHost;
|
import org.apache.http.HttpHost;
|
||||||
import org.apache.http.HttpMessage;
|
import org.apache.http.HttpMessage;
|
||||||
import org.apache.http.HttpRequest;
|
import org.apache.http.HttpRequest;
|
||||||
|
@ -418,7 +420,8 @@ public class CachingHttpClient implements HttpClient {
|
||||||
return revalidateCacheEntry(target, request, context, entry);
|
return revalidateCacheEntry(target, request, context, entry);
|
||||||
} catch (IOException ioex) {
|
} catch (IOException ioex) {
|
||||||
if (validityPolicy.mustRevalidate(entry)
|
if (validityPolicy.mustRevalidate(entry)
|
||||||
|| (isSharedCache() && validityPolicy.proxyRevalidate(entry))) {
|
|| (isSharedCache() && validityPolicy.proxyRevalidate(entry))
|
||||||
|
|| explicitFreshnessRequest(request, entry)) {
|
||||||
setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
|
setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
|
||||||
return new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout");
|
return new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout");
|
||||||
} else {
|
} else {
|
||||||
|
@ -435,6 +438,27 @@ public class CachingHttpClient implements HttpClient {
|
||||||
return callBackend(target, request, context);
|
return callBackend(target, request, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean explicitFreshnessRequest(HttpRequest request, HttpCacheEntry entry) {
|
||||||
|
for(Header h : request.getHeaders("Cache-Control")) {
|
||||||
|
for(HeaderElement elt : h.getElements()) {
|
||||||
|
if ("max-stale".equals(elt.getName())) {
|
||||||
|
try {
|
||||||
|
int maxstale = Integer.parseInt(elt.getValue());
|
||||||
|
long age = validityPolicy.getCurrentAgeSecs(entry);
|
||||||
|
long lifetime = validityPolicy.getFreshnessLifetimeSecs(entry);
|
||||||
|
if (age - lifetime > maxstale) return true;
|
||||||
|
} catch (NumberFormatException nfe) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if ("min-fresh".equals(elt.getName())
|
||||||
|
|| "max-age".equals(elt.getName())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private String generateViaHeader(HttpMessage msg) {
|
private String generateViaHeader(HttpMessage msg) {
|
||||||
final VersionInfo vi = VersionInfo.loadVersionInfo("org.apache.http.client", getClass().getClassLoader());
|
final VersionInfo vi = VersionInfo.loadVersionInfo("org.apache.http.client", getClass().getClassLoader());
|
||||||
final String release = (vi != null) ? vi.getRelease() : VersionInfo.UNAVAILABLE;
|
final String release = (vi != null) ? vi.getRelease() : VersionInfo.UNAVAILABLE;
|
||||||
|
|
|
@ -74,6 +74,113 @@ public class TestProtocolRecommendations extends AbstractProtocolTest {
|
||||||
assertFalse(foundIdentity);
|
assertFalse(foundIdentity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* "For this reason, a cache SHOULD NOT return a stale response if the
|
||||||
|
* client explicitly requests a first-hand or fresh one, unless it is
|
||||||
|
* impossible to comply for technical or policy reasons."
|
||||||
|
*/
|
||||||
|
private HttpRequest requestToPopulateStaleCacheEntry() throws Exception {
|
||||||
|
HttpRequest req1 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
|
||||||
|
HttpResponse resp1 = make200Response();
|
||||||
|
Date now = new Date();
|
||||||
|
Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
|
||||||
|
resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
|
||||||
|
resp1.setHeader("Cache-Control","public,max-age=5");
|
||||||
|
resp1.setHeader("Etag","\"etag\"");
|
||||||
|
|
||||||
|
backendExpectsAnyRequest().andReturn(resp1);
|
||||||
|
return req1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testDoesNotReturnStaleResponseOnError(HttpRequest req2)
|
||||||
|
throws Exception, IOException {
|
||||||
|
HttpRequest req1 = requestToPopulateStaleCacheEntry();
|
||||||
|
|
||||||
|
backendExpectsAnyRequest().andThrow(new IOException());
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
impl.execute(host, req1);
|
||||||
|
HttpResponse result = null;
|
||||||
|
try {
|
||||||
|
result = impl.execute(host, req2);
|
||||||
|
} catch (IOException acceptable) {
|
||||||
|
}
|
||||||
|
verifyMocks();
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
assertFalse(result.getStatusLine().getStatusCode() == HttpStatus.SC_OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFirstHandOneWithCacheControl()
|
||||||
|
throws Exception {
|
||||||
|
HttpRequest req = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
|
||||||
|
req.setHeader("Cache-Control","no-cache");
|
||||||
|
testDoesNotReturnStaleResponseOnError(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFirstHandOneWithPragma()
|
||||||
|
throws Exception {
|
||||||
|
HttpRequest req = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
|
||||||
|
req.setHeader("Pragma","no-cache");
|
||||||
|
testDoesNotReturnStaleResponseOnError(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFreshWithMaxAge()
|
||||||
|
throws Exception {
|
||||||
|
HttpRequest req = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
|
||||||
|
req.setHeader("Cache-Control","max-age=0");
|
||||||
|
testDoesNotReturnStaleResponseOnError(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDoesNotReturnStaleResponseIfClientExplicitlySpecifiesLargerMaxAge()
|
||||||
|
throws Exception {
|
||||||
|
HttpRequest req = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
|
||||||
|
req.setHeader("Cache-Control","max-age=20");
|
||||||
|
testDoesNotReturnStaleResponseOnError(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFreshWithMinFresh()
|
||||||
|
throws Exception {
|
||||||
|
HttpRequest req = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
|
||||||
|
req.setHeader("Cache-Control","min-fresh=2");
|
||||||
|
|
||||||
|
testDoesNotReturnStaleResponseOnError(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFreshWithMaxStale()
|
||||||
|
throws Exception {
|
||||||
|
HttpRequest req = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
|
||||||
|
req.setHeader("Cache-Control","max-stale=2");
|
||||||
|
|
||||||
|
testDoesNotReturnStaleResponseOnError(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMayReturnStaleResponseIfClientExplicitlySpecifiesAcceptableMaxStale()
|
||||||
|
throws Exception {
|
||||||
|
HttpRequest req1 = requestToPopulateStaleCacheEntry();
|
||||||
|
HttpRequest req2 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
|
||||||
|
req2.setHeader("Cache-Control","max-stale=20");
|
||||||
|
|
||||||
|
backendExpectsAnyRequest().andThrow(new IOException());
|
||||||
|
|
||||||
|
replayMocks();
|
||||||
|
impl.execute(host, req1);
|
||||||
|
HttpResponse result = impl.execute(host, req2);
|
||||||
|
verifyMocks();
|
||||||
|
|
||||||
|
assertEquals(HttpStatus.SC_OK, result.getStatusLine().getStatusCode());
|
||||||
|
assertNotNull(result.getFirstHeader("Warning"));
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* "A correct cache MUST respond to a request with the most up-to-date
|
* "A correct cache MUST respond to a request with the most up-to-date
|
||||||
* response held by the cache that is appropriate to the request
|
* response held by the cache that is appropriate to the request
|
||||||
|
|
Loading…
Reference in New Issue