### Eclipse Workspace Patch 1.0 #P httpcomponents-client Index: httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCacheEntry.java =================================================================== --- httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCacheEntry.java (revision 959977) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCacheEntry.java (working copy) @@ -503,5 +503,34 @@ Assert.assertNull(expirationDate); } + @Test + public void testMustRevalidateIsFalseIfDirectiveNotPresent() { + Header[] headers = new Header[] { new BasicHeader("Cache-Control","public") }; + CacheEntry entry = getEntry(headers); + Assert.assertFalse(entry.mustRevalidate()); + } + + @Test + public void testMustRevalidateIsTrueWhenDirectiveIsPresent() { + Header[] headers = new Header[] { new BasicHeader("Cache-Control","public, must-revalidate") }; + CacheEntry entry = getEntry(headers); + Assert.assertTrue(entry.mustRevalidate()); + } + + @Test + public void testProxyRevalidateIsFalseIfDirectiveNotPresent() { + Header[] headers = new Header[] { new BasicHeader("Cache-Control","public") }; + CacheEntry entry = getEntry(headers); + Assert.assertFalse(entry.proxyRevalidate()); + } + + @Test + public void testProxyRevalidateIsTrueWhenDirectiveIsPresent() { + Header[] headers = new Header[] { new BasicHeader("Cache-Control","public, proxy-revalidate") }; + CacheEntry entry = getEntry(headers); + Assert.assertTrue(entry.proxyRevalidate()); + } + + } Index: httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CacheEntry.java =================================================================== --- httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CacheEntry.java (revision 959977) +++ httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CacheEntry.java (working copy) @@ -100,6 +100,7 @@ this.reason = reason; this.body = body; this.variantURIs = new HashSet(); + } /** @@ -391,4 +392,22 @@ + "; status=" + status + "]"; } + protected boolean hasCacheControlDirective(String directive) { + for(Header h : responseHeaders.getHeaders("Cache-Control")) { + for(HeaderElement elt : h.getElements()) { + if (directive.equalsIgnoreCase(elt.getName())) { + return true; + } + } + } + return false; + } + + public boolean mustRevalidate() { + return hasCacheControlDirective("must-revalidate"); + } + public boolean proxyRevalidate() { + return hasCacheControlDirective("proxy-revalidate"); + } + } Index: httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ConditionalRequestBuilder.java =================================================================== --- httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ConditionalRequestBuilder.java (revision 959977) +++ httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ConditionalRequestBuilder.java (working copy) @@ -27,6 +27,7 @@ package org.apache.http.impl.client.cache; import org.apache.http.Header; +import org.apache.http.HeaderElement; import org.apache.http.HttpRequest; import org.apache.http.ProtocolException; import org.apache.http.annotation.Immutable; @@ -37,7 +38,7 @@ */ @Immutable public class ConditionalRequestBuilder { - + /** * When a {@link CacheEntry} is stale but 'might' be used as a response * to an {@link HttpRequest} we will attempt to revalidate the entry with @@ -59,6 +60,19 @@ Header lastModified = cacheEntry.getFirstHeader(HeaderConstants.LAST_MODIFIED); wrapperRequest.setHeader(HeaderConstants.IF_MODIFIED_SINCE, lastModified.getValue()); } + boolean mustRevalidate = false; + for(Header h : cacheEntry.getHeaders(HeaderConstants.CACHE_CONTROL)) { + for(HeaderElement elt : h.getElements()) { + if (HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE.equalsIgnoreCase(elt.getName()) + || HeaderConstants.CACHE_CONTROL_PROXY_REVALIDATE.equalsIgnoreCase(elt.getName())) { + mustRevalidate = true; + break; + } + } + } + if (mustRevalidate) { + wrapperRequest.addHeader("Cache-Control","max-age=0"); + } return wrapperRequest; } Index: httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java =================================================================== --- httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java (revision 959977) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java (working copy) @@ -28,6 +28,7 @@ import java.io.IOException; import java.io.InputStream; +import java.net.SocketTimeoutException; import java.util.Date; import java.util.Random; @@ -4268,6 +4269,191 @@ } } + /* "The request includes a "no-cache" cache-control directive or, for + * compatibility with HTTP/1.0 clients, "Pragma: no-cache".... The + * server MUST NOT use a cached copy when responding to such a request." + * + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4 + */ + protected void testCacheIsNotUsedWhenRespondingToRequest(HttpRequest req) + throws Exception { + HttpRequest req1 = new BasicHttpRequest("GET","/",HTTP_1_1); + HttpResponse resp1 = make200Response(); + resp1.setHeader("Etag","\"etag\""); + resp1.setHeader("Cache-Control","max-age=3600"); + + backendExpectsAnyRequest().andReturn(resp1); + + HttpResponse resp2 = make200Response(); + resp2.setHeader("Etag","\"etag2\""); + resp2.setHeader("Cache-Control","max-age=1200"); + + Capture cap = new Capture(); + EasyMock.expect(mockBackend.execute(EasyMock.eq(host), + EasyMock.capture(cap), + (HttpContext)EasyMock.isNull())) + .andReturn(resp2); + + replayMocks(); + impl.execute(host,req1); + HttpResponse result = impl.execute(host,req); + verifyMocks(); + + Assert.assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result)); + HttpRequest captured = cap.getValue(); + Assert.assertTrue(HttpTestUtils.equivalent(req, captured)); + } + + @Test + public void testCacheIsNotUsedWhenRespondingToRequestWithCacheControlNoCache() + throws Exception { + HttpRequest req = new BasicHttpRequest("GET","/",HTTP_1_1); + req.setHeader("Cache-Control","no-cache"); + testCacheIsNotUsedWhenRespondingToRequest(req); + } + + @Test + public void testCacheIsNotUsedWhenRespondingToRequestWithPragmaNoCache() + throws Exception { + HttpRequest req = new BasicHttpRequest("GET","/",HTTP_1_1); + req.setHeader("Pragma","no-cache"); + testCacheIsNotUsedWhenRespondingToRequest(req); + } + + /* "When the must-revalidate directive is present in a response received + * by a cache, that cache MUST NOT use the entry after it becomes stale + * to respond to a subsequent request without first revalidating it with + * the origin server. (I.e., the cache MUST do an end-to-end + * revalidation every time, if, based solely on the origin server's + * Expires or max-age value, the cached response is stale.)" + * + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4 + */ + protected void testStaleCacheResponseMustBeRevalidatedWithOrigin( + HttpResponse staleResponse) throws Exception { + HttpRequest req1 = new BasicHttpRequest("GET","/",HTTP_1_1); + + backendExpectsAnyRequest().andReturn(staleResponse); + + HttpRequest req2 = new BasicHttpRequest("GET","/",HTTP_1_1); + req2.setHeader("Cache-Control","max-stale=3600"); + HttpResponse resp2 = make200Response(); + resp2.setHeader("ETag","\"etag2\""); + resp2.setHeader("Cache-Control","max-age=5, must-revalidate"); + + Capture cap = new Capture(); + // this request MUST happen + EasyMock.expect(mockBackend.execute(EasyMock.eq(host), + EasyMock.capture(cap), + (HttpContext)EasyMock.isNull())) + .andReturn(resp2); + + replayMocks(); + impl.execute(host,req1); + impl.execute(host,req2); + verifyMocks(); + + HttpRequest reval = cap.getValue(); + boolean foundMaxAge0 = false; + for(Header h : reval.getHeaders("Cache-Control")) { + for(HeaderElement elt : h.getElements()) { + if ("max-age".equalsIgnoreCase(elt.getName()) + && "0".equals(elt.getValue())) { + foundMaxAge0 = true; + } + } + } + Assert.assertTrue(foundMaxAge0); + } + + @Test + public void testStaleEntryWithMustRevalidateIsNotUsedWithoutRevalidatingWithOrigin() + throws Exception { + HttpResponse response = make200Response(); + Date now = new Date(); + Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); + response.setHeader("Date",DateUtils.formatDate(tenSecondsAgo)); + response.setHeader("ETag","\"etag1\""); + response.setHeader("Cache-Control","max-age=5, must-revalidate"); + + testStaleCacheResponseMustBeRevalidatedWithOrigin(response); + } + + + /* "In all circumstances an HTTP/1.1 cache MUST obey the must-revalidate + * directive; in particular, if the cache cannot reach the origin server + * for any reason, it MUST generate a 504 (Gateway Timeout) response." + */ + protected void testGenerates504IfCannotRevalidateStaleResponse( + HttpResponse staleResponse) throws Exception { + HttpRequest req1 = new BasicHttpRequest("GET","/",HTTP_1_1); + + backendExpectsAnyRequest().andReturn(staleResponse); + + HttpRequest req2 = new BasicHttpRequest("GET","/",HTTP_1_1); + + backendExpectsAnyRequest().andThrow(new SocketTimeoutException()); + + replayMocks(); + impl.execute(host,req1); + HttpResponse result = impl.execute(host,req2); + verifyMocks(); + + Assert.assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT, + result.getStatusLine().getStatusCode()); + } + + @Test + public void testGenerates504IfCannotRevalidateAMustRevalidateEntry() + throws Exception { + HttpResponse resp1 = make200Response(); + Date now = new Date(); + Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); + resp1.setHeader("ETag","\"etag\""); + resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo)); + resp1.setHeader("Cache-Control","max-age=5,must-revalidate"); + + testGenerates504IfCannotRevalidateStaleResponse(resp1); + } + + /* "The proxy-revalidate directive has the same meaning as the must- + * revalidate directive, except that it does not apply to non-shared + * user agent caches." + * + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4 + */ + @Test + public void testStaleEntryWithProxyRevalidateOnSharedCacheIsNotUsedWithoutRevalidatingWithOrigin() + throws Exception { + if (impl.isSharedCache()) { + HttpResponse response = make200Response(); + Date now = new Date(); + Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); + response.setHeader("Date",DateUtils.formatDate(tenSecondsAgo)); + response.setHeader("ETag","\"etag1\""); + response.setHeader("Cache-Control","max-age=5, proxy-revalidate"); + + testStaleCacheResponseMustBeRevalidatedWithOrigin(response); + } + } + + @Test + public void testGenerates504IfSharedCacheCannotRevalidateAProxyRevalidateEntry() + throws Exception { + if (impl.isSharedCache()) { + HttpResponse resp1 = make200Response(); + Date now = new Date(); + Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); + resp1.setHeader("ETag","\"etag\""); + resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo)); + resp1.setHeader("Cache-Control","max-age=5,proxy-revalidate"); + + testGenerates504IfCannotRevalidateStaleResponse(resp1); + } + } + + + private class FakeHeaderGroup extends HeaderGroup{ public void addHeader(String name, String value){ Index: httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java =================================================================== --- httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java (revision 959977) +++ httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java (working copy) @@ -411,10 +411,15 @@ try { return revalidateCacheEntry(target, request, context, entry); } catch (IOException ioex) { - HttpResponse response = responseGenerator.generateResponse(entry); - response.addHeader(HeaderConstants.WARNING, "111 Revalidation Failed - " + ioex.getMessage()); - log.debug("111 revalidation failed due to exception: " + ioex); - return response; + if (entry.mustRevalidate() + || (isSharedCache() && entry.proxyRevalidate())) { + return new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout"); + } else { + HttpResponse response = responseGenerator.generateResponse(entry); + response.addHeader(HeaderConstants.WARNING, "111 Revalidation Failed - " + ioex.getMessage()); + log.debug("111 revalidation failed due to exception: " + ioex); + return response; + } } catch (ProtocolException e) { throw new ClientProtocolException(e); } @@ -612,4 +617,8 @@ return SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS; } + public boolean isSharedCache() { + return true; + } + } Index: httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCachingHttpClient.java =================================================================== --- httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCachingHttpClient.java (revision 959977) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCachingHttpClient.java (working copy) @@ -1053,6 +1053,10 @@ } } + @Test + public void testIsSharedCache() { + Assert.assertTrue(impl.isSharedCache()); + } private byte[] readResponse(HttpResponse response) { try { Index: httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestConditionalRequestBuilder.java =================================================================== --- httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestConditionalRequestBuilder.java (revision 959977) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestConditionalRequestBuilder.java (working copy) @@ -29,7 +29,9 @@ import java.util.Date; import org.apache.http.Header; +import org.apache.http.HeaderElement; import org.apache.http.HttpRequest; +import org.apache.http.HttpStatus; import org.apache.http.ProtocolException; import org.apache.http.ProtocolVersion; import org.apache.http.entity.ByteArrayEntity; @@ -118,5 +120,64 @@ Assert.assertEquals("If-None-Match", newRequest.getAllHeaders()[1].getName()); Assert.assertEquals(theETag, newRequest.getAllHeaders()[1].getValue()); } + + @Test + public void testCacheEntryWithMustRevalidateDoesEndToEndRevalidation() throws Exception { + HttpRequest request = new BasicHttpRequest("GET","/",CachingHttpClient.HTTP_1_1); + Date now = new Date(); + Date elevenSecondsAgo = new Date(now.getTime() - 11 * 1000L); + Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); + Date nineSecondsAgo = new Date(now.getTime() - 9 * 1000L); + + Header[] cacheEntryHeaders = new Header[] { + new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)), + new BasicHeader("ETag", "\"etag\""), + new BasicHeader("Cache-Control","max-age=5, must-revalidate") }; + CacheEntry cacheEntry = new CacheEntry(elevenSecondsAgo, nineSecondsAgo, + CachingHttpClient.HTTP_1_1, cacheEntryHeaders, new ByteArrayEntity(new byte[0]), + HttpStatus.SC_OK, "OK"); + + HttpRequest result = impl.buildConditionalRequest(request, cacheEntry); + + boolean foundMaxAge0 = false; + for(Header h : result.getHeaders("Cache-Control")) { + for(HeaderElement elt : h.getElements()) { + if ("max-age".equalsIgnoreCase(elt.getName()) + && "0".equals(elt.getValue())) { + foundMaxAge0 = true; + } + } + } + Assert.assertTrue(foundMaxAge0); + } + @Test + public void testCacheEntryWithProxyRevalidateDoesEndToEndRevalidation() throws Exception { + HttpRequest request = new BasicHttpRequest("GET","/",CachingHttpClient.HTTP_1_1); + Date now = new Date(); + Date elevenSecondsAgo = new Date(now.getTime() - 11 * 1000L); + Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); + Date nineSecondsAgo = new Date(now.getTime() - 9 * 1000L); + + Header[] cacheEntryHeaders = new Header[] { + new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)), + new BasicHeader("ETag", "\"etag\""), + new BasicHeader("Cache-Control","max-age=5, proxy-revalidate") }; + CacheEntry cacheEntry = new CacheEntry(elevenSecondsAgo, nineSecondsAgo, + CachingHttpClient.HTTP_1_1, cacheEntryHeaders, new ByteArrayEntity(new byte[0]), + HttpStatus.SC_OK, "OK"); + + HttpRequest result = impl.buildConditionalRequest(request, cacheEntry); + + boolean foundMaxAge0 = false; + for(Header h : result.getHeaders("Cache-Control")) { + for(HeaderElement elt : h.getElements()) { + if ("max-age".equalsIgnoreCase(elt.getName()) + && "0".equals(elt.getValue())) { + foundMaxAge0 = true; + } + } + } + Assert.assertTrue(foundMaxAge0); + } } Index: httpclient-cache/src/main/java/org/apache/http/impl/client/cache/HeaderConstants.java =================================================================== --- httpclient-cache/src/main/java/org/apache/http/impl/client/cache/HeaderConstants.java (revision 959977) +++ httpclient-cache/src/main/java/org/apache/http/impl/client/cache/HeaderConstants.java (working copy) @@ -62,6 +62,8 @@ public static final String CACHE_CONTROL_MAX_AGE = "max-age"; public static final String CACHE_CONTROL_MAX_STALE = "max-stale"; public static final String CACHE_CONTROL_MIN_FRESH = "min-fresh"; + public static final String CACHE_CONTROL_MUST_REVALIDATE = "must-revalidate"; + public static final String CACHE_CONTROL_PROXY_REVALIDATE = "proxy-revalidate"; public static final String WARNING = "Warning"; public static final String RANGE = "Range";