diff options
author | Jesse Wilson <jessewilson@google.com> | 2011-04-29 17:47:37 -0700 |
---|---|---|
committer | Jesse Wilson <jessewilson@google.com> | 2011-04-29 17:47:37 -0700 |
commit | adb64fbba2b781467e055706c3de0873dfc01166 (patch) | |
tree | f5ac451218b706ebdcce647133a57a5cbcd53c95 | |
parent | fe2dea5d0747cbe711fcf64f89845735f4da10c2 (diff) | |
download | libcore-adb64fbba2b781467e055706c3de0873dfc01166.zip libcore-adb64fbba2b781467e055706c3de0873dfc01166.tar.gz libcore-adb64fbba2b781467e055706c3de0873dfc01166.tar.bz2 |
Honor max-age and min-fresh request headers.
We now honor headers from both the server's response (which
we have cached) and the client's request.
Change-Id: Ib46e4fc0c5dd5b3e74cff8f45eea2dda51d20b94
http://b/3180373
4 files changed, 113 insertions, 15 deletions
diff --git a/luni/src/main/java/libcore/net/http/CacheHeader.java b/luni/src/main/java/libcore/net/http/CacheHeader.java index 44e6d89..bcc4846 100644 --- a/luni/src/main/java/libcore/net/http/CacheHeader.java +++ b/luni/src/main/java/libcore/net/http/CacheHeader.java @@ -24,6 +24,13 @@ import java.util.concurrent.TimeUnit; * Caching aspects of an HTTP request or response. */ final class CacheHeader { + + /** HTTP header name for the local time when the request was sent. */ + public static final String SENT_MILLIS = "X-Android-Sent-Millis"; + + /** HTTP header name for the local time when the response was received. */ + public static final String RECEIVED_MILLIS = "X-Android-Received-Millis"; + int responseCode; Date servedDate; Date lastModified; @@ -50,6 +57,9 @@ final class CacheHeader { boolean proxyRevalidate; int sMaxAgeSeconds; String etag; + int ageSeconds = -1; + long sentRequestMillis; + long receivedResponseMillis; public CacheHeader(HttpHeaders headers) { this.responseCode = headers.getResponseCode(); @@ -68,6 +78,12 @@ final class CacheHeader { if (headers.getValue(i).equalsIgnoreCase("no-cache")) { noCache = true; } + } else if ("Age".equalsIgnoreCase(headers.getKey(i))) { + ageSeconds = parseSeconds(headers.getValue(i)); + } else if (SENT_MILLIS.equalsIgnoreCase(headers.getKey(i))) { + sentRequestMillis = Long.parseLong(headers.getValue(i)); + } else if (RECEIVED_MILLIS.equalsIgnoreCase(headers.getKey(i))) { + receivedResponseMillis = Long.parseLong(headers.getValue(i)); } } } @@ -186,20 +202,35 @@ final class CacheHeader { } /** - * Returns the time at which this response will require validation. + * Returns the current age of the response, in milliseconds. The calculation + * is specified by RFC 2616, 13.2.3 Age Calculations. + */ + private long computeAge(long nowMillis) { + long apparentReceivedAge = servedDate != null + ? Math.max(0, receivedResponseMillis - servedDate.getTime()) + : 0; + long receivedAge = ageSeconds != -1 + ? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(ageSeconds)) + : apparentReceivedAge; + long responseDuration = receivedResponseMillis - sentRequestMillis; + long residentDuration = nowMillis - receivedResponseMillis; + return receivedAge + responseDuration + residentDuration; + } + + /** + * Returns the number of milliseconds that the response was fresh for, + * starting from the served date. */ - private long getExpiresTimeMillis() { - if (servedDate != null && maxAgeSeconds != -1) { - return servedDate.getTime() + TimeUnit.SECONDS.toMillis(maxAgeSeconds); - } else if (expires != null) { - return expires.getTime(); - } else { - /* - * This response doesn't specify an expiration time, so for semantic - * transparency we just assume it's expired. - */ - return 0; + private long computeFreshnessLifetime() { + if (maxAgeSeconds != -1) { + return TimeUnit.SECONDS.toMillis(maxAgeSeconds); } + if (expires != null) { + long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis; + long delta = expires.getTime() - servedMillis; + return delta > 0 ? delta : 0; + } + return 0; } /** @@ -211,7 +242,7 @@ final class CacheHeader { // TODO: if a "If-Modified-Since" or "If-None-Match" header exists, assume the user // knows better and just return CONDITIONAL_CACHE - // TODO: honor request headers, like the client's requested max-age + // TODO: honor request headers, like the client's requested max-stale if (noStore) { return ResponseSource.NETWORK; @@ -222,7 +253,22 @@ final class CacheHeader { return ResponseSource.NETWORK; } - if (!noCache && nowMillis < getExpiresTimeMillis()) { + long ageMillis = computeAge(nowMillis); + long freshMillis = computeFreshnessLifetime(); + + CacheHeader requestCacheHeader = new CacheHeader(request); + + long minFreshMillis = 0; + if (requestCacheHeader.minFreshSeconds != -1) { + minFreshMillis = TimeUnit.SECONDS.toMillis(requestCacheHeader.minFreshSeconds); + } + + if (requestCacheHeader.maxAgeSeconds != -1) { + freshMillis = Math.min(freshMillis, + TimeUnit.SECONDS.toMillis(requestCacheHeader.maxAgeSeconds)); + } + + if (!noCache && ageMillis + minFreshMillis < freshMillis) { return ResponseSource.CACHE; } diff --git a/luni/src/main/java/libcore/net/http/HttpURLConnectionImpl.java b/luni/src/main/java/libcore/net/http/HttpURLConnectionImpl.java index f92d832..1660ce7 100644 --- a/luni/src/main/java/libcore/net/http/HttpURLConnectionImpl.java +++ b/luni/src/main/java/libcore/net/http/HttpURLConnectionImpl.java @@ -143,6 +143,8 @@ public class HttpURLConnectionImpl extends HttpURLConnection { private ResponseSource responseSource; + private long sentRequestMillis; + private long receivedResponseMillis; private boolean sentRequestHeaders; /** @@ -452,6 +454,8 @@ public class HttpURLConnectionImpl extends HttpURLConnection { try { discardResponseBody(responseBodyIn); responseBodyIn = null; + sentRequestMillis = 0; + receivedResponseMillis = 0; sentRequestHeaders = false; responseHeader = null; responseCode = -1; @@ -833,6 +837,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection { requestOut = new BufferedOutputStream(socketOut, headerBytes.length + contentLength); } + sentRequestMillis = System.currentTimeMillis(); requestOut.write(headerBytes); sentRequestHeaders = true; } @@ -1109,6 +1114,9 @@ public class HttpURLConnectionImpl extends HttpURLConnection { requestOut = socketOut; readResponseHeaders(); + receivedResponseMillis = System.currentTimeMillis(); + responseHeader.add(CacheHeader.SENT_MILLIS, Long.toString(sentRequestMillis)); + responseHeader.add(CacheHeader.RECEIVED_MILLIS, Long.toString(receivedResponseMillis)); if (responseSource == ResponseSource.CONDITIONAL_CACHE) { if (cacheResponseHeader.validate(requestHeader, responseHeader)) { diff --git a/luni/src/test/java/libcore/java/net/HttpResponseCacheTest.java b/luni/src/test/java/libcore/java/net/HttpResponseCacheTest.java index b1988c4..cd36a3e 100644 --- a/luni/src/test/java/libcore/java/net/HttpResponseCacheTest.java +++ b/luni/src/test/java/libcore/java/net/HttpResponseCacheTest.java @@ -514,6 +514,8 @@ public final class HttpResponseCacheTest extends TestCase { } public void testMaxAgeInThePastWithDateHeaderButNoLastModifiedHeader() throws Exception { + // TODO: this is bogus; we interpret 'max-age' relative to the server's clock; + // But Chrome uses the local clock assertNotCached(new MockResponse() .addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS)) .addHeader("Cache-Control: max-age=60")); @@ -531,6 +533,19 @@ public final class HttpResponseCacheTest extends TestCase { .addHeader("Cache-Control: max-age=60")); } + public void testMaxAgeWithLastModifiedButNoServedDate() throws Exception { + assertFullyCached(new MockResponse() + .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) + .addHeader("Cache-Control: max-age=60")); + } + + public void testMaxAgeInTheFutureWithDateAndLastModifiedHeaders() throws Exception { + assertFullyCached(new MockResponse() + .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) + .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) + .addHeader("Cache-Control: max-age=60")); + } + public void testRequestMethodOptionsIsNotCached() throws Exception { testRequestMethod("OPTIONS", false); } @@ -751,6 +766,35 @@ public final class HttpResponseCacheTest extends TestCase { .addHeader("Expires: " + formatDate(-2, TimeUnit.HOURS))); } + public void testRequestMaxAge() throws IOException { + server.enqueue(new MockResponse().setBody("A") + .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) + .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES)) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); + server.enqueue(new MockResponse().setBody("B")); + + server.play(); + assertEquals("A", readAscii(server.getUrl("/").openConnection())); + + URLConnection connection = server.getUrl("/").openConnection(); + connection.addRequestProperty("Cache-Control", "max-age=30"); + assertEquals("B", readAscii(connection)); + } + + public void testRequestMinFresh() throws IOException { + server.enqueue(new MockResponse().setBody("A") + .addHeader("Cache-Control: max-age=60") + .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); + server.enqueue(new MockResponse().setBody("B")); + + server.play(); + assertEquals("A", readAscii(server.getUrl("/").openConnection())); + + URLConnection connection = server.getUrl("/").openConnection(); + connection.addRequestProperty("Cache-Control", "min-fresh=120"); + assertEquals("B", readAscii(connection)); + } + /** * @param delta the offset from the current date to use. Negative * values yield dates in the past; positive values yield dates in the diff --git a/support/src/test/java/tests/http/MockWebServer.java b/support/src/test/java/tests/http/MockWebServer.java index 6de6c51..2d8215c 100644 --- a/support/src/test/java/tests/http/MockWebServer.java +++ b/support/src/test/java/tests/http/MockWebServer.java @@ -360,7 +360,7 @@ public final class MockWebServer { if (request.startsWith("OPTIONS ") || request.startsWith("GET ") || request.startsWith("HEAD ") || request.startsWith("DELETE ") - || request .startsWith("TRACE ")) { + || request .startsWith("TRACE ") || request.startsWith("CONNECT ")) { if (hasBody) { throw new IllegalArgumentException("Request must not have a body: " + request); } |