summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Wilson <jessewilson@google.com>2011-04-29 17:47:37 -0700
committerJesse Wilson <jessewilson@google.com>2011-04-29 17:47:37 -0700
commitadb64fbba2b781467e055706c3de0873dfc01166 (patch)
treef5ac451218b706ebdcce647133a57a5cbcd53c95
parentfe2dea5d0747cbe711fcf64f89845735f4da10c2 (diff)
downloadlibcore-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
-rw-r--r--luni/src/main/java/libcore/net/http/CacheHeader.java74
-rw-r--r--luni/src/main/java/libcore/net/http/HttpURLConnectionImpl.java8
-rw-r--r--luni/src/test/java/libcore/java/net/HttpResponseCacheTest.java44
-rw-r--r--support/src/test/java/tests/http/MockWebServer.java2
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);
}