Index: src/test/java/org/apache/http/impl/client/cache/HttpTestUtils.java =================================================================== --- src/test/java/org/apache/http/impl/client/cache/HttpTestUtils.java (revision 1042796) +++ src/test/java/org/apache/http/impl/client/cache/HttpTestUtils.java (working copy) @@ -31,7 +31,10 @@ import java.util.Random; import java.util.Set; +import junit.framework.Assert; + 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; @@ -44,6 +47,7 @@ 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; @@ -298,4 +302,34 @@ 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 Index: src/test/java/org/apache/http/impl/client/cache/TestProtocolRecommendations.java =================================================================== --- src/test/java/org/apache/http/impl/client/cache/TestProtocolRecommendations.java (revision 1042796) +++ src/test/java/org/apache/http/impl/client/cache/TestProtocolRecommendations.java (working copy) @@ -47,6 +47,7 @@ import org.apache.http.protocol.HttpContext; import org.easymock.Capture; import org.easymock.EasyMock; +import org.junit.Assert; import org.junit.Test; /* @@ -307,7 +308,130 @@ assertEquals(warning, result.getFirstHeader("Warning").getValue()); } + + /* + * "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(); + + Capture cap = new Capture(); + 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(); + + 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(); + + Capture cap = new Capture(); + 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(); + + 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(); + + Capture cap = new Capture(); + EasyMock.expect(mockBackend.execute(EasyMock.eq(host), + EasyMock.capture(cap), + (HttpContext)EasyMock.isNull())) + .andReturn(resp2).times(1); + + replayMocks(); + impl.execute(host,req1); + HttpResponse result = impl.execute(host,req2); + verifyMocks(); + + Assert.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(); + + Capture cap = new Capture(); + EasyMock.expect(mockBackend.execute(EasyMock.eq(host), + EasyMock.capture(cap), + (HttpContext)EasyMock.isNull())) + .andReturn(resp2).times(1); + + replayMocks(); + impl.execute(host,req1); + HttpResponse result = impl.execute(host,req2); + verifyMocks(); + + Assert.assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, + result.getStatusLine().getStatusCode()); + } + /* * "A transparent proxy SHOULD NOT modify an end-to-end header unless * the definition of that header requires or specifically allows that." Index: src/test/java/org/apache/http/impl/client/cache/TestCacheValidityPolicy.java =================================================================== --- src/test/java/org/apache/http/impl/client/cache/TestCacheValidityPolicy.java (revision 1042796) +++ src/test/java/org/apache/http/impl/client/cache/TestCacheValidityPolicy.java (working copy) @@ -29,9 +29,18 @@ import java.util.Date; import org.apache.http.Header; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +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.apache.http.message.BasicHttpRequest; +import org.apache.http.message.BasicHttpResponse; +import org.apache.http.protocol.HttpContext; +import org.easymock.Capture; +import org.easymock.EasyMock; import org.junit.Assert; import org.junit.Test; @@ -506,5 +515,75 @@ Assert.assertTrue(impl.proxyRevalidate(entry)); } + + @Test + public void testMayReturnStaleIfErrorInResponseIsTrueWithinStaleness(){ + Date now = new Date(); + Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); + 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); + + CacheValidityPolicy impl = new CacheValidityPolicy(); + + HttpRequest req = new BasicHttpRequest("GET","/",HttpVersion.HTTP_1_1); + + Assert.assertTrue(impl.mayReturnStaleIfError(req, entry, now)); + } + + @Test + public void testMayReturnStaleIfErrorInRequestIsTrueWithinStaleness(){ + Date now = new Date(); + Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); + Header[] headers = new Header[] { + new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)), + new BasicHeader("Cache-Control", "max-age=5") + }; + HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now, headers); + + CacheValidityPolicy impl = new CacheValidityPolicy(); + + HttpRequest req = new BasicHttpRequest("GET","/",HttpVersion.HTTP_1_1); + req.setHeader("Cache-Control","stale-if-error=15"); + + Assert.assertTrue(impl.mayReturnStaleIfError(req, entry, now)); + } + + @Test + public void testMayReturnStaleIfErrorInResponseIsFalseAfterWindow(){ + Date now = new Date(); + Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); + 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); + + CacheValidityPolicy impl = new CacheValidityPolicy(); + + HttpRequest req = new BasicHttpRequest("GET","/",HttpVersion.HTTP_1_1); + + Assert.assertFalse(impl.mayReturnStaleIfError(req, entry, now)); + } + + @Test + public void testMayReturnStaleIfErrorInRequestIsFalseAfterWindow(){ + Date now = new Date(); + Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); + Header[] headers = new Header[] { + new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)), + new BasicHeader("Cache-Control", "max-age=5") + }; + HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now, headers); + + CacheValidityPolicy impl = new CacheValidityPolicy(); + + HttpRequest req = new BasicHttpRequest("GET","/",HttpVersion.HTTP_1_1); + req.setHeader("Cache-Control","stale-if-error=1"); + + Assert.assertFalse(impl.mayReturnStaleIfError(req, entry, now)); + } } Index: src/main/java/org/apache/http/impl/client/cache/CacheValidityPolicy.java =================================================================== --- src/main/java/org/apache/http/impl/client/cache/CacheValidityPolicy.java (revision 1042796) +++ src/main/java/org/apache/http/impl/client/cache/CacheValidityPolicy.java (working copy) @@ -30,6 +30,7 @@ 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,7 +119,34 @@ 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) {} + } + } + } + return result; + } + protected Date getDateValue(final HttpCacheEntry entry) { Header dateHdr = entry.getFirstHeader(HTTP.DATE_HEADER); if (dateHdr == null) Index: src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java =================================================================== --- src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java (revision 1042796) +++ src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java (working copy) @@ -513,7 +513,8 @@ } 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; @@ -711,11 +712,26 @@ } return responseGenerator.generateResponse(updatedEntry); } + + if (isErrorStatusAndServableFromCache(statusCode)) { + if (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 isErrorStatusAndServableFromCache(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,