### Eclipse Workspace Patch 1.0 #P httpcomponents-client 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 995084) +++ httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java (working copy) @@ -34,6 +34,8 @@ import org.apache.commons.logging.Log; 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.HttpMessage; import org.apache.http.HttpRequest; @@ -418,7 +420,8 @@ return revalidateCacheEntry(target, request, context, entry); } catch (IOException ioex) { if (validityPolicy.mustRevalidate(entry) - || (isSharedCache() && validityPolicy.proxyRevalidate(entry))) { + || (isSharedCache() && validityPolicy.proxyRevalidate(entry)) + || explicitFreshnessRequest(request, entry)) { setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE); return new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout"); } else { @@ -435,7 +438,34 @@ return callBackend(target, request, context); } - private String generateViaHeader(HttpMessage msg) { + private boolean explicitFreshnessRequest(HttpRequest request, HttpCacheEntry entry) { + for(Header h : request.getHeaders("Cache-Control")) { + for(HeaderElement elt : h.getElements()) { + if ("max-age".equals(elt.getName())) { + try { + int maxage = Integer.parseInt(elt.getValue()); + if (maxage <= validityPolicy.getFreshnessLifetimeSecs(entry)) return true; + } catch (NumberFormatException nfe) { + return true; + } + } else 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())) { + return true; + } + } + } + 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; final ProtocolVersion pv = msg.getProtocolVersion(); Index: httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRecommendations.java =================================================================== --- httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRecommendations.java (revision 995084) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRecommendations.java (working copy) @@ -28,9 +28,17 @@ import static org.junit.Assert.*; +import java.io.IOException; +import java.util.Date; + import org.apache.http.Header; import org.apache.http.HeaderElement; +import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.HttpVersion; +import org.apache.http.impl.cookie.DateUtils; +import org.apache.http.message.BasicHttpRequest; import org.junit.Test; /* @@ -65,4 +73,121 @@ } 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 testMayReturnStaleResponseIfClientExplicitlySpecifiesAcceptableMaxAge() + throws Exception { + HttpRequest req1 = requestToPopulateStaleCacheEntry(); + HttpRequest req2 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1); + req2.setHeader("Cache-Control","max-age=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")); + } + + + @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")); + } }