diff --git a/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CacheValidityPolicy.java b/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CacheValidityPolicy.java index 3349e072e..ea9f4d763 100644 --- a/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CacheValidityPolicy.java +++ b/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CacheValidityPolicy.java @@ -30,6 +30,7 @@ import java.util.Date; import org.apache.http.Header; import org.apache.http.HeaderElement; +import org.apache.http.HttpRequest; import org.apache.http.annotation.Immutable; import org.apache.http.client.cache.HeaderConstants; import org.apache.http.client.cache.HttpCacheEntry; @@ -118,6 +119,35 @@ class CacheValidityPolicy { public boolean proxyRevalidate(final HttpCacheEntry entry) { return hasCacheControlDirective(entry, "proxy-revalidate"); } + + public boolean mayReturnStaleIfError(HttpRequest request, + HttpCacheEntry entry, Date now) { + long stalenessSecs = getStalenessSecs(entry, now); + return mayReturnStaleIfError(request.getHeaders("Cache-Control"), + stalenessSecs) + || mayReturnStaleIfError(entry.getHeaders("Cache-Control"), + stalenessSecs); + } + + private boolean mayReturnStaleIfError(Header[] headers, long stalenessSecs) { + boolean result = false; + for(Header h : headers) { + for(HeaderElement elt : h.getElements()) { + if ("stale-if-error".equals(elt.getName())) { + try { + int staleIfErrorSecs = Integer.parseInt(elt.getValue()); + if (stalenessSecs <= staleIfErrorSecs) { + result = true; + break; + } + } catch (NumberFormatException nfe) { + // skip malformed directive + } + } + } + } + return result; + } protected Date getDateValue(final HttpCacheEntry entry) { Header dateHdr = entry.getFirstHeader(HTTP.DATE_HEADER); diff --git a/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java b/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java index f3b31f26b..d4cc31b02 100644 --- a/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java +++ b/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java @@ -513,7 +513,7 @@ public class CachingHttpClient implements HttpClient { } return false; } - + private String generateViaHeader(HttpMessage msg) { final VersionInfo vi = VersionInfo.loadVersionInfo("org.apache.http.client", getClass().getClassLoader()); final String release = (vi != null) ? vi.getRelease() : VersionInfo.UNAVAILABLE; @@ -691,11 +691,25 @@ public class CachingHttpClient implements HttpClient { } return responseGenerator.generateResponse(updatedEntry); } + + if (staleIfErrorAppliesTo(statusCode) + && validityPolicy.mayReturnStaleIfError(request, cacheEntry, responseDate)) { + final HttpResponse cachedResponse = responseGenerator.generateResponse(cacheEntry); + cachedResponse.addHeader(HeaderConstants.WARNING, "110 localhost \"Response is stale\""); + return cachedResponse; + } return handleBackendResponse(target, conditionalRequest, requestDate, responseDate, backendResponse); } + private boolean staleIfErrorAppliesTo(int statusCode) { + return statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR + || statusCode == HttpStatus.SC_BAD_GATEWAY + || statusCode == HttpStatus.SC_SERVICE_UNAVAILABLE + || statusCode == HttpStatus.SC_GATEWAY_TIMEOUT; + } + HttpResponse handleBackendResponse( HttpHost target, HttpRequest request, diff --git a/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/HttpTestUtils.java b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/HttpTestUtils.java index cff909652..b80f929f2 100644 --- a/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/HttpTestUtils.java +++ b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/HttpTestUtils.java @@ -31,6 +31,7 @@ import java.util.Date; import java.util.Map; import java.util.Random; import org.apache.http.Header; +import org.apache.http.HeaderElement; import org.apache.http.HttpEntity; import org.apache.http.HttpMessage; import org.apache.http.HttpRequest; @@ -43,8 +44,10 @@ import org.apache.http.client.cache.HttpCacheEntry; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.impl.cookie.DateUtils; import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicHttpRequest; import org.apache.http.message.BasicHttpResponse; import org.apache.http.message.BasicStatusLine; +import org.junit.Assert; public class HttpTestUtils { @@ -54,7 +57,7 @@ public class HttpTestUtils { * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1 */ private static final String[] HOP_BY_HOP_HEADERS = { "Connection", "Keep-Alive", "Proxy-Authenticate", - "Proxy-Authorization", "TE", "Trailers", "Transfer-Encoding", "Upgrade" }; + "Proxy-Authorization", "TE", "Trailers", "Transfer-Encoding", "Upgrade" }; /* * "Multiple message-header fields with the same field-name MAY be present @@ -64,15 +67,15 @@ public class HttpTestUtils { * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 */ private static final String[] MULTI_HEADERS = { "Accept", "Accept-Charset", "Accept-Encoding", - "Accept-Language", "Allow", "Cache-Control", "Connection", "Content-Encoding", - "Content-Language", "Expect", "Pragma", "Proxy-Authenticate", "TE", "Trailer", - "Transfer-Encoding", "Upgrade", "Via", "Warning", "WWW-Authenticate" }; + "Accept-Language", "Allow", "Cache-Control", "Connection", "Content-Encoding", + "Content-Language", "Expect", "Pragma", "Proxy-Authenticate", "TE", "Trailer", + "Transfer-Encoding", "Upgrade", "Via", "Warning", "WWW-Authenticate" }; private static final String[] SINGLE_HEADERS = { "Accept-Ranges", "Age", "Authorization", - "Content-Length", "Content-Location", "Content-MD5", "Content-Range", "Content-Type", - "Date", "ETag", "Expires", "From", "Host", "If-Match", "If-Modified-Since", - "If-None-Match", "If-Range", "If-Unmodified-Since", "Last-Modified", "Location", - "Max-Forwards", "Proxy-Authorization", "Range", "Referer", "Retry-After", "Server", - "User-Agent", "Vary" }; + "Content-Length", "Content-Location", "Content-MD5", "Content-Range", "Content-Type", + "Date", "ETag", "Expires", "From", "Host", "If-Match", "If-Modified-Since", + "If-None-Match", "If-Range", "If-Unmodified-Since", "Last-Modified", "Location", + "Max-Forwards", "Proxy-Authorization", "Range", "Referer", "Retry-After", "Server", + "User-Agent", "Vary" }; /* * Determines whether the given header name is considered a hop-by-hop @@ -150,7 +153,7 @@ public class HttpTestUtils { public static boolean equivalent(RequestLine l1, RequestLine l2) { return (l1.getMethod().equals(l2.getMethod()) && l1.getProtocolVersion().equals(l2.getProtocolVersion()) && l1.getUri().equals( - l2.getUri())); + l2.getUri())); } /* @@ -202,13 +205,13 @@ public class HttpTestUtils { * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec1.html#sec1.3 */ public static boolean semanticallyTransparent(HttpResponse r1, HttpResponse r2) - throws Exception { + throws Exception { final boolean entitiesEquivalent = equivalent(r1.getEntity(), r2.getEntity()); if (!entitiesEquivalent) return false; final boolean statusLinesEquivalent = semanticallyTransparent(r1.getStatusLine(), r2.getStatusLine()); if (!statusLinesEquivalent) return false; final boolean e2eHeadersEquivalentSubset = isEndToEndHeaderSubset( - r1, r2); + r1, r2); return e2eHeadersEquivalentSubset; } @@ -263,14 +266,14 @@ public class HttpTestUtils { return makeCacheEntry(now, now, getStockHeaders(now), getRandomBytes(128), variantMap); } - + public static HttpCacheEntry makeCacheEntry(Date requestDate, Date responseDate, Header[] headers, byte[] bytes, Map variantMap) { StatusLine statusLine = new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK"); return new HttpCacheEntry(requestDate, responseDate, statusLine, headers, new HeapResource(bytes), variantMap); } - + public static HttpCacheEntry makeCacheEntry(Header[] headers, byte[] bytes) { Date now = new Date(); return makeCacheEntry(now, now, headers, bytes); @@ -297,4 +300,35 @@ public class HttpTestUtils { out.setEntity(makeBody(128)); return out; } + + public static final HttpResponse make200Response(Date date, String cacheControl) { + HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("Date", DateUtils.formatDate(date)); + response.setHeader("Cache-Control",cacheControl); + response.setHeader("Etag","\"etag\""); + return response; + } + + public static final void assert110WarningFound(HttpResponse response) { + boolean found110Warning = false; + for(Header h : response.getHeaders("Warning")) { + for(HeaderElement elt : h.getElements()) { + String[] parts = elt.getName().split("\\s"); + if ("110".equals(parts[0])) { + found110Warning = true; + break; + } + } + } + Assert.assertTrue(found110Warning); + } + + public static HttpRequest makeDefaultRequest() { + return new BasicHttpRequest("GET","/",HttpVersion.HTTP_1_1); + } + + public static HttpResponse make500Response() { + return new BasicHttpResponse(HttpVersion.HTTP_1_1, + HttpStatus.SC_INTERNAL_SERVER_ERROR, "Internal Server Error"); + } } \ No newline at end of file diff --git a/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCacheValidityPolicy.java b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCacheValidityPolicy.java index 49e209043..aa58dc9b6 100644 --- a/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCacheValidityPolicy.java +++ b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCacheValidityPolicy.java @@ -26,107 +26,101 @@ */ package org.apache.http.impl.client.cache; +import static org.junit.Assert.*; + import java.util.Date; import org.apache.http.Header; +import org.apache.http.HttpRequest; +import org.apache.http.HttpVersion; import org.apache.http.client.cache.HttpCacheEntry; import org.apache.http.impl.cookie.DateUtils; import org.apache.http.message.BasicHeader; -import org.junit.Assert; +import org.apache.http.message.BasicHttpRequest; +import org.junit.Before; import org.junit.Test; public class TestCacheValidityPolicy { + private CacheValidityPolicy impl; + private Date now; + private Date oneSecondAgo; + private Date sixSecondsAgo; + private Date tenSecondsAgo; + private Date elevenSecondsAgo; + + @Before + public void setUp() { + impl = new CacheValidityPolicy(); + now = new Date(); + oneSecondAgo = new Date(now.getTime() - 1 * 1000L); + sixSecondsAgo = new Date(now.getTime() - 6 * 1000L); + tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); + elevenSecondsAgo = new Date(now.getTime() - 11 * 1000L); + } + @Test public void testApparentAgeIsMaxIntIfDateHeaderNotPresent() { Header[] headers = { new BasicHeader("Server", "MockServer/1.0") }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - CacheValidityPolicy impl = new CacheValidityPolicy(); - Assert.assertEquals(2147483648L, impl.getApparentAgeSecs(entry)); + assertEquals(2147483648L, impl.getApparentAgeSecs(entry)); } @Test public void testApparentAgeIsResponseReceivedTimeLessDateHeader() { - Date now = new Date(); - Date sixSecondsAgo = new Date(now.getTime() - 6 * 1000L); - Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); - Header[] headers = new Header[] { new BasicHeader("Date", DateUtils .formatDate(tenSecondsAgo)) }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, sixSecondsAgo, headers); - CacheValidityPolicy impl = new CacheValidityPolicy(); - - Assert.assertEquals(4, impl.getApparentAgeSecs(entry)); + assertEquals(4, impl.getApparentAgeSecs(entry)); } @Test public void testNegativeApparentAgeIsBroughtUpToZero() { - Date now = new Date(); - Date sixSecondsAgo = new Date(now.getTime() - 6 * 1000L); - Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); - Header[] headers = new Header[] { new BasicHeader("Date", DateUtils .formatDate(sixSecondsAgo)) }; - HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now,tenSecondsAgo,headers); - CacheValidityPolicy impl = new CacheValidityPolicy(); - Assert.assertEquals(0, impl.getApparentAgeSecs(entry)); + assertEquals(0, impl.getApparentAgeSecs(entry)); } @Test public void testCorrectedReceivedAgeIsAgeHeaderIfLarger() { Header[] headers = new Header[] { new BasicHeader("Age", "10"), }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - - CacheValidityPolicy impl = new CacheValidityPolicy() { - + impl = new CacheValidityPolicy() { @Override protected long getApparentAgeSecs(HttpCacheEntry entry) { return 6; } - }; - - Assert.assertEquals(10, impl.getCorrectedReceivedAgeSecs(entry)); + assertEquals(10, impl.getCorrectedReceivedAgeSecs(entry)); } @Test public void testCorrectedReceivedAgeIsApparentAgeIfLarger() { Header[] headers = new Header[] { new BasicHeader("Age", "6"), }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - - CacheValidityPolicy impl = new CacheValidityPolicy() { - + impl = new CacheValidityPolicy() { @Override protected long getApparentAgeSecs(HttpCacheEntry entry) { return 10; } - }; - - Assert.assertEquals(10, impl.getCorrectedReceivedAgeSecs(entry)); + assertEquals(10, impl.getCorrectedReceivedAgeSecs(entry)); } @Test public void testResponseDelayIsDifferenceBetweenResponseAndRequestTimes() { - Date now = new Date(); - Date sixSecondsAgo = new Date(now.getTime() - 6 * 1000L); - Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); - HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, sixSecondsAgo); - CacheValidityPolicy impl = new CacheValidityPolicy(); - - Assert.assertEquals(4, impl.getResponseDelaySecs(entry)); + assertEquals(4, impl.getResponseDelaySecs(entry)); } @Test public void testCorrectedInitialAgeIsCorrectedReceivedAgePlusResponseDelay() { HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(); - CacheValidityPolicy impl = new CacheValidityPolicy() { - + impl = new CacheValidityPolicy() { @Override protected long getCorrectedReceivedAgeSecs(HttpCacheEntry entry) { return 7; @@ -136,62 +130,50 @@ public class TestCacheValidityPolicy { protected long getResponseDelaySecs(HttpCacheEntry entry) { return 13; } - }; - Assert.assertEquals(20, impl.getCorrectedInitialAgeSecs(entry)); + assertEquals(20, impl.getCorrectedInitialAgeSecs(entry)); } @Test public void testResidentTimeSecondsIsTimeSinceResponseTime() { - final Date now = new Date(); - final Date sixSecondsAgo = new Date(now.getTime() - 6 * 1000L); - HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, sixSecondsAgo); - - CacheValidityPolicy impl = new CacheValidityPolicy() { - + impl = new CacheValidityPolicy() { @Override protected Date getCurrentDate() { return now; } - }; - - Assert.assertEquals(6, impl.getResidentTimeSecs(entry, now)); + assertEquals(6, impl.getResidentTimeSecs(entry, now)); } @Test public void testCurrentAgeIsCorrectedInitialAgePlusResidentTime() { HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(); - CacheValidityPolicy impl = new CacheValidityPolicy() { - + impl = new CacheValidityPolicy() { @Override protected long getCorrectedInitialAgeSecs(HttpCacheEntry entry) { return 11; } - @Override protected long getResidentTimeSecs(HttpCacheEntry entry, Date d) { return 17; } }; - Assert.assertEquals(28, impl.getCurrentAgeSecs(entry, new Date())); + assertEquals(28, impl.getCurrentAgeSecs(entry, new Date())); } @Test public void testFreshnessLifetimeIsSMaxAgeIfPresent() { Header[] headers = new Header[] { new BasicHeader("Cache-Control", "s-maxage=10") }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - CacheValidityPolicy impl = new CacheValidityPolicy(); - Assert.assertEquals(10, impl.getFreshnessLifetimeSecs(entry)); + assertEquals(10, impl.getFreshnessLifetimeSecs(entry)); } @Test public void testFreshnessLifetimeIsMaxAgeIfPresent() { Header[] headers = new Header[] { new BasicHeader("Cache-Control", "max-age=10") }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - CacheValidityPolicy impl = new CacheValidityPolicy(); - Assert.assertEquals(10, impl.getFreshnessLifetimeSecs(entry)); + assertEquals(10, impl.getFreshnessLifetimeSecs(entry)); } @Test @@ -199,312 +181,269 @@ public class TestCacheValidityPolicy { Header[] headers = new Header[] { new BasicHeader("Cache-Control", "max-age=10"), new BasicHeader("Cache-Control", "s-maxage=20") }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - CacheValidityPolicy impl = new CacheValidityPolicy(); - Assert.assertEquals(10, impl.getFreshnessLifetimeSecs(entry)); + assertEquals(10, impl.getFreshnessLifetimeSecs(entry)); headers = new Header[] { new BasicHeader("Cache-Control", "max-age=20"), new BasicHeader("Cache-Control", "s-maxage=10") }; entry = HttpTestUtils.makeCacheEntry(headers); - Assert.assertEquals(10, impl.getFreshnessLifetimeSecs(entry)); + assertEquals(10, impl.getFreshnessLifetimeSecs(entry)); } @Test public void testFreshnessLifetimeIsMaxAgeEvenIfExpiresIsPresent() { - Date now = new Date(); - Date sixSecondsAgo = new Date(now.getTime() - 6 * 1000L); - Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); Header[] headers = new Header[] { new BasicHeader("Cache-Control", "max-age=10"), new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)), new BasicHeader("Expires", DateUtils.formatDate(sixSecondsAgo)) }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - CacheValidityPolicy impl = new CacheValidityPolicy(); - Assert.assertEquals(10, impl.getFreshnessLifetimeSecs(entry)); + assertEquals(10, impl.getFreshnessLifetimeSecs(entry)); } @Test public void testFreshnessLifetimeIsSMaxAgeEvenIfExpiresIsPresent() { - Date now = new Date(); - Date sixSecondsAgo = new Date(now.getTime() - 6 * 1000L); - Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); Header[] headers = new Header[] { new BasicHeader("Cache-Control", "s-maxage=10"), new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)), new BasicHeader("Expires", DateUtils.formatDate(sixSecondsAgo)) }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - CacheValidityPolicy impl = new CacheValidityPolicy(); - Assert.assertEquals(10, impl.getFreshnessLifetimeSecs(entry)); + assertEquals(10, impl.getFreshnessLifetimeSecs(entry)); } @Test public void testFreshnessLifetimeIsFromExpiresHeaderIfNoMaxAge() { - Date now = new Date(); - Date sixSecondsAgo = new Date(now.getTime() - 6 * 1000L); - Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); Header[] headers = new Header[] { new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)), new BasicHeader("Expires", DateUtils.formatDate(sixSecondsAgo)) }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - CacheValidityPolicy impl = new CacheValidityPolicy(); - Assert.assertEquals(4, impl.getFreshnessLifetimeSecs(entry)); + assertEquals(4, impl.getFreshnessLifetimeSecs(entry)); } @Test public void testHeuristicFreshnessLifetime() { - Date now = new Date(); - Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L); - Date elevenSecondsAgo = new Date(now.getTime() - 11 * 1000L); - Header[] headers = new Header[] { new BasicHeader("Date", DateUtils.formatDate(oneSecondAgo)), new BasicHeader("Last-Modified", DateUtils.formatDate(elevenSecondsAgo)) }; - HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - CacheValidityPolicy impl = new CacheValidityPolicy(); - Assert.assertEquals(1, impl.getHeuristicFreshnessLifetimeSecs(entry, 0.1f, 0)); + assertEquals(1, impl.getHeuristicFreshnessLifetimeSecs(entry, 0.1f, 0)); } @Test public void testHeuristicFreshnessLifetimeDefaultsProperly() { long defaultFreshness = 10; - HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(); - - CacheValidityPolicy impl = new CacheValidityPolicy(); - Assert.assertEquals(defaultFreshness, impl.getHeuristicFreshnessLifetimeSecs(entry, 0.1f, defaultFreshness)); + assertEquals(defaultFreshness, impl.getHeuristicFreshnessLifetimeSecs(entry, 0.1f, defaultFreshness)); } @Test public void testHeuristicFreshnessLifetimeIsNonNegative() { - Date now = new Date(); - Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L); - Date elevenSecondsAgo = new Date(now.getTime() - 1 * 1000L); - Header[] headers = new Header[] { new BasicHeader("Date", DateUtils.formatDate(elevenSecondsAgo)), new BasicHeader("Last-Modified", DateUtils.formatDate(oneSecondAgo)) }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - CacheValidityPolicy impl = new CacheValidityPolicy(); - Assert.assertTrue(impl.getHeuristicFreshnessLifetimeSecs(entry, 0.1f, 10) >= 0); + assertTrue(impl.getHeuristicFreshnessLifetimeSecs(entry, 0.1f, 10) >= 0); } @Test public void testResponseIsFreshIfFreshnessLifetimeExceedsCurrentAge() { final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(); - final Date now = new Date(); - CacheValidityPolicy impl = new CacheValidityPolicy() { - + impl = new CacheValidityPolicy() { @Override public long getCurrentAgeSecs(HttpCacheEntry e, Date d) { - Assert.assertSame(entry, e); - Assert.assertEquals(now, d); + assertSame(entry, e); + assertEquals(now, d); return 6; } - @Override public long getFreshnessLifetimeSecs(HttpCacheEntry e) { - Assert.assertSame(entry, e); + assertSame(entry, e); return 10; } }; - - Assert.assertTrue(impl.isResponseFresh(entry, now)); + assertTrue(impl.isResponseFresh(entry, now)); } @Test public void testResponseIsNotFreshIfFreshnessLifetimeEqualsCurrentAge() { final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(); - final Date now = new Date(); - CacheValidityPolicy impl = new CacheValidityPolicy() { - + impl = new CacheValidityPolicy() { @Override public long getCurrentAgeSecs(HttpCacheEntry e, Date d) { - Assert.assertEquals(now, d); - Assert.assertSame(entry, e); + assertEquals(now, d); + assertSame(entry, e); return 6; } - @Override public long getFreshnessLifetimeSecs(HttpCacheEntry e) { - Assert.assertSame(entry, e); + assertSame(entry, e); return 6; } }; - - Assert.assertFalse(impl.isResponseFresh(entry, now)); + assertFalse(impl.isResponseFresh(entry, now)); } @Test public void testResponseIsNotFreshIfCurrentAgeExceedsFreshnessLifetime() { final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(); - final Date now = new Date(); - CacheValidityPolicy impl = new CacheValidityPolicy() { - + impl = new CacheValidityPolicy() { @Override public long getCurrentAgeSecs(HttpCacheEntry e, Date d) { - Assert.assertEquals(now, d); - Assert.assertSame(entry, e); + assertEquals(now, d); + assertSame(entry, e); return 10; } - @Override public long getFreshnessLifetimeSecs(HttpCacheEntry e) { - Assert.assertSame(entry, e); + assertSame(entry, e); return 6; } }; - - Assert.assertFalse(impl.isResponseFresh(entry, now)); + assertFalse(impl.isResponseFresh(entry, now)); } @Test public void testCacheEntryIsRevalidatableIfHeadersIncludeETag() { - Header[] headers = { new BasicHeader("Expires", DateUtils.formatDate(new Date())), new BasicHeader("ETag", "somevalue")}; - HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - CacheValidityPolicy impl = new CacheValidityPolicy(); - - Assert.assertTrue(impl.isRevalidatable(entry)); + assertTrue(impl.isRevalidatable(entry)); } @Test public void testCacheEntryIsRevalidatableIfHeadersIncludeLastModifiedDate() { - Header[] headers = { new BasicHeader("Expires", DateUtils.formatDate(new Date())), new BasicHeader("Last-Modified", DateUtils.formatDate(new Date())) }; - HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - CacheValidityPolicy impl = new CacheValidityPolicy(); - - Assert.assertTrue(impl.isRevalidatable(entry)); + assertTrue(impl.isRevalidatable(entry)); } @Test public void testCacheEntryIsNotRevalidatableIfNoAppropriateHeaders() { - Header[] headers = { new BasicHeader("Expires", DateUtils.formatDate(new Date())), new BasicHeader("Cache-Control", "public") }; - HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - CacheValidityPolicy impl = new CacheValidityPolicy(); - - Assert.assertFalse(impl.isRevalidatable(entry)); + assertFalse(impl.isRevalidatable(entry)); } @Test public void testMalformedDateHeaderIsIgnored() { - Header[] headers = new Header[] { new BasicHeader("Date", "asdf") }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - - CacheValidityPolicy impl = new CacheValidityPolicy(); - Date d = impl.getDateValue(entry); - - Assert.assertNull(d); + assertNull(impl.getDateValue(entry)); } @Test public void testMalformedContentLengthReturnsNegativeOne() { - Header[] headers = new Header[] { new BasicHeader("Content-Length", "asdf") }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - - CacheValidityPolicy impl = new CacheValidityPolicy(); - long length = impl.getContentLengthValue(entry); - - Assert.assertEquals(-1, length); + assertEquals(-1, impl.getContentLengthValue(entry)); } @Test public void testNegativeAgeHeaderValueReturnsMaxAge() { - Header[] headers = new Header[] { new BasicHeader("Age", "-100") }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - - CacheValidityPolicy impl = new CacheValidityPolicy(); - long length = impl.getAgeValue(entry); - - Assert.assertEquals(CacheValidityPolicy.MAX_AGE, length); + assertEquals(CacheValidityPolicy.MAX_AGE, impl.getAgeValue(entry)); } @Test public void testMalformedAgeHeaderValueReturnsMaxAge() { - Header[] headers = new Header[] { new BasicHeader("Age", "asdf") }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - - CacheValidityPolicy impl = new CacheValidityPolicy(); - long length = impl.getAgeValue(entry); - - Assert.assertEquals(CacheValidityPolicy.MAX_AGE, length); + assertEquals(CacheValidityPolicy.MAX_AGE, impl.getAgeValue(entry)); } @Test public void testMalformedCacheControlMaxAgeHeaderReturnsZero() { - Header[] headers = new Header[] { new BasicHeader("Cache-Control", "max-age=asdf") }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - - CacheValidityPolicy impl = new CacheValidityPolicy(); - long maxage = impl.getMaxAge(entry); - - Assert.assertEquals(0, maxage); + assertEquals(0, impl.getMaxAge(entry)); } @Test public void testMalformedExpirationDateReturnsNull() { Header[] headers = new Header[] { new BasicHeader("Expires", "asdf") }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - - CacheValidityPolicy impl = new CacheValidityPolicy(); - Date expirationDate = impl.getExpirationDate(entry); - - Assert.assertNull(expirationDate); + assertNull(impl.getExpirationDate(entry)); } @Test public void testMustRevalidateIsFalseIfDirectiveNotPresent() { Header[] headers = new Header[] { new BasicHeader("Cache-Control","public") }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - CacheValidityPolicy impl = new CacheValidityPolicy(); - - Assert.assertFalse(impl.mustRevalidate(entry)); + assertFalse(impl.mustRevalidate(entry)); } @Test public void testMustRevalidateIsTrueWhenDirectiveIsPresent() { Header[] headers = new Header[] { new BasicHeader("Cache-Control","public, must-revalidate") }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - CacheValidityPolicy impl = new CacheValidityPolicy(); - - Assert.assertTrue(impl.mustRevalidate(entry)); + assertTrue(impl.mustRevalidate(entry)); } @Test public void testProxyRevalidateIsFalseIfDirectiveNotPresent() { Header[] headers = new Header[] { new BasicHeader("Cache-Control","public") }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - CacheValidityPolicy impl = new CacheValidityPolicy(); - - Assert.assertFalse(impl.proxyRevalidate(entry)); + assertFalse(impl.proxyRevalidate(entry)); } @Test public void testProxyRevalidateIsTrueWhenDirectiveIsPresent() { Header[] headers = new Header[] { new BasicHeader("Cache-Control","public, proxy-revalidate") }; HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers); - CacheValidityPolicy impl = new CacheValidityPolicy(); - - Assert.assertTrue(impl.proxyRevalidate(entry)); + assertTrue(impl.proxyRevalidate(entry)); } + @Test + public void testMayReturnStaleIfErrorInResponseIsTrueWithinStaleness(){ + Header[] headers = new Header[] { + new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)), + new BasicHeader("Cache-Control", "max-age=5, stale-if-error=15") + }; + HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now, headers); + HttpRequest req = new BasicHttpRequest("GET","/",HttpVersion.HTTP_1_1); + assertTrue(impl.mayReturnStaleIfError(req, entry, now)); + } + + @Test + public void testMayReturnStaleIfErrorInRequestIsTrueWithinStaleness(){ + Header[] headers = new Header[] { + new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)), + new BasicHeader("Cache-Control", "max-age=5") + }; + HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now, headers); + HttpRequest req = new BasicHttpRequest("GET","/",HttpVersion.HTTP_1_1); + req.setHeader("Cache-Control","stale-if-error=15"); + assertTrue(impl.mayReturnStaleIfError(req, entry, now)); + } + + @Test + public void testMayNotReturnStaleIfErrorInResponseAndAfterResponseWindow(){ + Header[] headers = new Header[] { + new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)), + new BasicHeader("Cache-Control", "max-age=5, stale-if-error=1") + }; + HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now, headers); + HttpRequest req = new BasicHttpRequest("GET","/",HttpVersion.HTTP_1_1); + assertFalse(impl.mayReturnStaleIfError(req, entry, now)); + } + + @Test + public void testMayNotReturnStaleIfErrorInResponseAndAfterRequestWindow(){ + Header[] headers = new Header[] { + new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)), + new BasicHeader("Cache-Control", "max-age=5") + }; + HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now, headers); + HttpRequest req = new BasicHttpRequest("GET","/",HttpVersion.HTTP_1_1); + req.setHeader("Cache-Control","stale-if-error=1"); + assertFalse(impl.mayReturnStaleIfError(req, entry, now)); + } } diff --git a/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRecommendations.java b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRecommendations.java index 1174b5e1a..8f3b5b57b 100644 --- a/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRecommendations.java +++ b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRecommendations.java @@ -307,7 +307,7 @@ public class TestProtocolRecommendations extends AbstractProtocolTest { assertEquals(warning, result.getFirstHeader("Warning").getValue()); } - + /* * "A transparent proxy SHOULD NOT modify an end-to-end header unless * the definition of that header requires or specifically allows that." diff --git a/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestRFC5861Compliance.java b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestRFC5861Compliance.java new file mode 100644 index 000000000..5e94bd81e --- /dev/null +++ b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestRFC5861Compliance.java @@ -0,0 +1,157 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.http.impl.client.cache; + +import static org.junit.Assert.assertEquals; + +import java.util.Date; + +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.junit.Test; + +/** + * A suite of acceptance tests for compliance with RFC5861, which + * describes the stale-if-error and stale-while-revalidate + * Cache-Control extensions. + */ +public class TestRFC5861Compliance extends AbstractProtocolTest { + + /* + * "The stale-if-error Cache-Control extension indicates that when an + * error is encountered, a cached stale response MAY be used to satisfy + * the request, regardless of other freshness information.When used as a + * request Cache-Control extension, its scope of application is the request + * it appears in; when used as a response Cache-Control extension, its + * scope is any request applicable to the cached response in which it + * occurs.Its value indicates the upper limit to staleness; when the cached + * response is more stale than the indicated amount, the cached response + * SHOULD NOT be used to satisfy the request, absent other information. + * In this context, an error is any situation that would result in a + * 500, 502, 503, or 504 HTTP response status code being returned." + * + * http://tools.ietf.org/html/rfc5861 + */ + @Test + public void testStaleIfErrorInResponseIsTrueReturnsStaleEntryWithWarning() + throws Exception{ + Date tenSecondsAgo = new Date(new Date().getTime() - 10 * 1000L); + HttpRequest req1 = HttpTestUtils.makeDefaultRequest(); + HttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo, + "public, max-age=5, stale-if-error=60"); + + backendExpectsAnyRequest().andReturn(resp1); + + HttpRequest req2 = HttpTestUtils.makeDefaultRequest(); + HttpResponse resp2 = HttpTestUtils.make500Response(); + + backendExpectsAnyRequest().andReturn(resp2); + + replayMocks(); + impl.execute(host,req1); + HttpResponse result = impl.execute(host,req2); + verifyMocks(); + + HttpTestUtils.assert110WarningFound(result); + } + + @Test + public void testStaleIfErrorInRequestIsTrueReturnsStaleEntryWithWarning() + throws Exception{ + Date tenSecondsAgo = new Date(new Date().getTime() - 10 * 1000L); + HttpRequest req1 = HttpTestUtils.makeDefaultRequest(); + HttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo, + "public, max-age=5"); + + backendExpectsAnyRequest().andReturn(resp1); + + HttpRequest req2 = HttpTestUtils.makeDefaultRequest(); + req2.setHeader("Cache-Control","public, max-age=5, stale-if-error=60"); + HttpResponse resp2 = HttpTestUtils.make500Response(); + + backendExpectsAnyRequest().andReturn(resp2); + + replayMocks(); + impl.execute(host,req1); + HttpResponse result = impl.execute(host,req2); + verifyMocks(); + + HttpTestUtils.assert110WarningFound(result); + } + + @Test + public void testStaleIfErrorInResponseIsFalseReturnsError() + throws Exception{ + Date now = new Date(); + Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); + HttpRequest req1 = HttpTestUtils.makeDefaultRequest(); + HttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo, + "public, max-age=5, stale-if-error=2"); + + backendExpectsAnyRequest().andReturn(resp1); + + HttpRequest req2 = HttpTestUtils.makeDefaultRequest(); + HttpResponse resp2 = HttpTestUtils.make500Response(); + + backendExpectsAnyRequest().andReturn(resp2); + + replayMocks(); + impl.execute(host,req1); + HttpResponse result = impl.execute(host,req2); + verifyMocks(); + + assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, + result.getStatusLine().getStatusCode()); + } + + @Test + public void testStaleIfErrorInRequestIsFalseReturnsError() + throws Exception{ + Date now = new Date(); + Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); + HttpRequest req1 = HttpTestUtils.makeDefaultRequest(); + HttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo, + "public, max-age=5"); + + backendExpectsAnyRequest().andReturn(resp1); + + HttpRequest req2 = HttpTestUtils.makeDefaultRequest(); + req2.setHeader("Cache-Control","stale-if-error=2"); + HttpResponse resp2 = HttpTestUtils.make500Response(); + + backendExpectsAnyRequest().andReturn(resp2); + + replayMocks(); + impl.execute(host,req1); + HttpResponse result = impl.execute(host,req2); + verifyMocks(); + + assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, + result.getStatusLine().getStatusCode()); + } +}