Bug 428916 - support Cache-control directives in HTTP requests, r=mcmanus+michal+froydnj
authorHonza Bambas <honzab.moz@firemni.cz>
Fri, 20 May 2016 08:33:00 +0200
changeset 299681 26a475c764e27b36d50199e15389e371e2e30061
parent 299680 5f50f2c7e8136ee2ca4726dd356f4be5e0c9a7da
child 299682 453431d7a2c8842d757e98dd566e81c5a9419dd1
push id77659
push usercbook@mozilla.com
push dateTue, 31 May 2016 14:55:18 +0000
treeherdermozilla-inbound@26a475c764e2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmcmanus
bugs428916
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 428916 - support Cache-control directives in HTTP requests, r=mcmanus+michal+froydnj
netwerk/protocol/http/CacheControlParser.cpp
netwerk/protocol/http/CacheControlParser.h
netwerk/protocol/http/moz.build
netwerk/protocol/http/nsHttpChannel.cpp
netwerk/protocol/http/nsHttpHeaderArray.cpp
netwerk/test/unit/head_cache2.js
netwerk/test/unit/test_cache-control_request.js
netwerk/test/unit/xpcshell.ini
new file mode 100644
--- /dev/null
+++ b/netwerk/protocol/http/CacheControlParser.cpp
@@ -0,0 +1,123 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CacheControlParser.h"
+
+namespace mozilla {
+namespace net {
+
+CacheControlParser::CacheControlParser(nsACString const &aHeader)
+  : Tokenizer(aHeader, nullptr, "-_")
+  , mMaxAgeSet(false)
+  , mMaxAge(0)
+  , mMaxStaleSet(false)
+  , mMaxStale(0)
+  , mMinFreshSet(false)
+  , mMinFresh(0)
+  , mNoCache(false)
+  , mNoStore(false)
+{
+  SkipWhites();
+  if (!CheckEOF()) {
+    Directive();
+  }
+}
+
+void CacheControlParser::Directive()
+{
+  if (CheckWord("no-cache")) {
+    mNoCache = true;
+    IgnoreDirective(); // ignore any optionally added values
+  } else if (CheckWord("no-store")) {
+    mNoStore = true;
+  } else if (CheckWord("max-age")) {
+    mMaxAgeSet = SecondsValue(&mMaxAge);
+  } else if (CheckWord("max-stale")) {
+    mMaxStaleSet = SecondsValue(&mMaxStale, PR_UINT32_MAX);
+  } else if (CheckWord("min-fresh")) {
+    mMinFreshSet = SecondsValue(&mMinFresh);
+  } else {
+    IgnoreDirective();
+  }
+
+  SkipWhites();
+  if (CheckEOF()) {
+    return;
+  }
+  if (CheckChar(',')) {
+    SkipWhites();
+    Directive();
+    return;
+  }
+
+  NS_WARNING("Unexpected input in Cache-control header value");
+}
+
+bool CacheControlParser::SecondsValue(uint32_t *seconds, uint32_t defaultVal)
+{
+  SkipWhites();
+  if (!CheckChar('=')) {
+    *seconds = defaultVal;
+    return !!defaultVal;
+  }
+
+  SkipWhites();
+  if (!ReadInteger(seconds)) {
+    NS_WARNING("Unexpected value in Cache-control header value");
+    return false;
+  }
+
+  return true;
+}
+
+void CacheControlParser::IgnoreDirective()
+{
+  Token t;
+  while (Next(t)) {
+    if (t.Equals(Token::Char(',')) || t.Equals(Token::EndOfFile())) {
+      Rollback();
+      break;
+    }
+    if (t.Equals(Token::Char('"'))) {
+      SkipUntil(Token::Char('"'));
+      if (!CheckChar('"')) {
+        NS_WARNING("Missing quoted string expansion in Cache-control header value");
+        break;
+      }
+    }
+  }
+}
+
+bool CacheControlParser::MaxAge(uint32_t *seconds)
+{
+  *seconds = mMaxAge;
+  return mMaxAgeSet;
+}
+
+bool CacheControlParser::MaxStale(uint32_t *seconds)
+{
+  *seconds = mMaxStale;
+  return mMaxStaleSet;
+}
+
+bool CacheControlParser::MinFresh(uint32_t *seconds)
+{
+  *seconds = mMinFresh;
+  return mMinFreshSet;
+}
+
+bool CacheControlParser::NoCache()
+{
+  return mNoCache;
+}
+
+bool CacheControlParser::NoStore()
+{
+  return mNoStore;
+}
+
+} // net
+} // mozilla
new file mode 100644
--- /dev/null
+++ b/netwerk/protocol/http/CacheControlParser.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CacheControlParser_h__
+#define CacheControlParser_h__
+
+#include "mozilla/Tokenizer.h"
+
+namespace mozilla {
+namespace net {
+
+class CacheControlParser final : Tokenizer
+{
+public:
+  explicit CacheControlParser(nsACString const &header);
+
+  bool MaxAge(uint32_t *seconds);
+  bool MaxStale(uint32_t *seconds);
+  bool MinFresh(uint32_t *seconds);
+  bool NoCache();
+  bool NoStore();
+
+private:
+  void Directive();
+  void IgnoreDirective();
+  bool SecondsValue(uint32_t *seconds, uint32_t defaultVal = 0);
+
+  bool mMaxAgeSet;
+  uint32_t mMaxAge;
+  bool mMaxStaleSet;
+  uint32_t mMaxStale;
+  bool mMinFreshSet;
+  uint32_t mMinFresh;
+  bool mNoCache;
+  bool mNoStore;
+};
+
+} // net
+} // mozilla
+
+#endif
--- a/netwerk/protocol/http/moz.build
+++ b/netwerk/protocol/http/moz.build
@@ -47,16 +47,17 @@ EXPORTS.mozilla.net += [
 SOURCES += [
     'AlternateServices.cpp',
     'ASpdySession.cpp',
     'nsHttpAuthCache.cpp',
     'nsHttpChannelAuthProvider.cpp', # redefines GetAuthType
 ]
 
 UNIFIED_SOURCES += [
+    'CacheControlParser.cpp',
     'ConnectionDiagnostics.cpp',
     'Http2Compression.cpp',
     'Http2Push.cpp',
     'Http2Session.cpp',
     'Http2Stream.cpp',
     'HttpBaseChannel.cpp',
     'HttpChannelChild.cpp',
     'HttpChannelParent.cpp',
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -90,16 +90,17 @@
 #include "nsNullPrincipal.h"
 #include "nsIPackagedAppService.h"
 #include "nsIDeprecationWarner.h"
 #include "nsIDocument.h"
 #include "nsICompressConvStats.h"
 #include "nsCORSListenerProxy.h"
 #include "nsISocketProvider.h"
 #include "mozilla/net/Predictor.h"
+#include "CacheControlParser.h"
 
 namespace mozilla { namespace net {
 
 namespace {
 
 // Monotonically increasing ID for generating unique cache entries per
 // intercepted channel.
 static uint64_t gNumIntercepted = 0;
@@ -3141,16 +3142,24 @@ nsHttpChannel::OpenCacheEntry(bool isHtt
 
     if (appId != NECKO_NO_APP_ID) {
         gIOService->IsAppOffline(appId, &appOffline);
         LOG(("nsHttpChannel::OpenCacheEntry appId: %u, offline: %d\n", appId, appOffline));
     }
 
     uint32_t cacheEntryOpenFlags;
     bool offline = gIOService->IsOffline() || appOffline;
+
+    nsAutoCString cacheControlRequestHeader;
+    mRequestHead.GetHeader(nsHttp::Cache_Control, cacheControlRequestHeader);
+    CacheControlParser cacheControlRequest(cacheControlRequestHeader);
+    if (cacheControlRequest.NoStore() && !PossiblyIntercepted()) {
+        goto bypassCacheEntryOpen;
+    }
+
     if (offline || (mLoadFlags & INHIBIT_CACHING)) {
         if (BYPASS_LOCAL_CACHE(mLoadFlags) && !offline && !PossiblyIntercepted()) {
             goto bypassCacheEntryOpen;
         }
         cacheEntryOpenFlags = nsICacheStorage::OPEN_READONLY;
         mCacheEntryIsReadOnly = true;
     }
     else if (BYPASS_LOCAL_CACHE(mLoadFlags) && !mApplicationCache) {
@@ -3309,16 +3318,26 @@ NS_IMETHODIMP
 nsHttpChannel::OnCacheEntryCheck(nsICacheEntry* entry, nsIApplicationCache* appCache,
                                  uint32_t* aResult)
 {
     nsresult rv = NS_OK;
 
     LOG(("nsHttpChannel::OnCacheEntryCheck enter [channel=%p entry=%p]",
         this, entry));
 
+    nsAutoCString cacheControlRequestHeader;
+    mRequestHead.GetHeader(nsHttp::Cache_Control, cacheControlRequestHeader);
+    CacheControlParser cacheControlRequest(cacheControlRequestHeader);
+
+    if (cacheControlRequest.NoStore()) {
+        LOG(("Not using cached response based on no-store request cache directive\n"));
+        *aResult = ENTRY_NOT_WANTED;
+        return NS_OK;
+    }
+
     // Remember the request is a custom conditional request so that we can
     // process any 304 response correctly.
     mCustomConditionalRequest =
         mRequestHead.HasHeader(nsHttp::If_Modified_Since) ||
         mRequestHead.HasHeader(nsHttp::If_None_Match) ||
         mRequestHead.HasHeader(nsHttp::If_Unmodified_Since) ||
         mRequestHead.HasHeader(nsHttp::If_Match) ||
         mRequestHead.HasHeader(nsHttp::If_Range);
@@ -3517,36 +3536,62 @@ nsHttpChannel::OnCacheEntryCheck(nsICach
     else if (mCachedResponseHead->MustValidate()) {
         LOG(("Validating based on MustValidate() returning TRUE\n"));
         doValidation = true;
     } else {
         // previously we also checked for a query-url w/out expiration
         // and didn't do heuristic on it. but defacto that is allowed now.
         //
         // Check if the cache entry has expired...
-        uint32_t time = 0; // a temporary variable for storing time values...
-
-        rv = entry->GetExpirationTime(&time);
+
+        uint32_t now = NowInSeconds();
+
+        uint32_t age = 0;
+        rv = mCachedResponseHead->ComputeCurrentAge(now, now, &age);
+        NS_ENSURE_SUCCESS(rv, rv);
+
+        uint32_t freshness = 0;
+        rv = mCachedResponseHead->ComputeFreshnessLifetime(&freshness);
+        NS_ENSURE_SUCCESS(rv, rv);
+
+        uint32_t expiration = 0;
+        rv = entry->GetExpirationTime(&expiration);
         NS_ENSURE_SUCCESS(rv, rv);
 
-        LOG(("  NowInSeconds()=%u, time=%u", NowInSeconds(), time));
-        if (NowInSeconds() <= time)
+        uint32_t maxAgeRequest, maxStaleRequest, minFreshRequest;
+
+        LOG(("  NowInSeconds()=%u, expiration time=%u, freshness lifetime=%u, age=%u",
+             now, expiration, freshness, age));
+
+        if (cacheControlRequest.NoCache()) {
+            LOG(("  validating, no-cache request"));
+            doValidation = true;
+        } else if (cacheControlRequest.MaxStale(&maxStaleRequest)) {
+            uint32_t staleTime = age > freshness ? age - freshness : 0;
+            doValidation = staleTime > maxStaleRequest;
+            LOG(("  validating=%d, max-stale=%u requested", doValidation, maxStaleRequest));
+        } else if (cacheControlRequest.MaxAge(&maxAgeRequest)) {
+            doValidation = age > maxAgeRequest;
+            LOG(("  validating=%d, max-age=%u requested", doValidation, maxAgeRequest));
+        } else if (cacheControlRequest.MinFresh(&minFreshRequest)) {
+            uint32_t freshTime = freshness > age ? freshness - age : 0;
+            doValidation = freshTime < minFreshRequest;
+            LOG(("  validating=%d, min-fresh=%u requested", doValidation, minFreshRequest));
+        } else if (now <= expiration) {
             doValidation = false;
-        else if (mCachedResponseHead->MustValidateIfExpired())
+            LOG(("  not validating, expire time not in the past"));
+        } else if (mCachedResponseHead->MustValidateIfExpired()) {
             doValidation = true;
-        else if (mLoadFlags & nsIRequest::VALIDATE_ONCE_PER_SESSION) {
+        } else if (mLoadFlags & nsIRequest::VALIDATE_ONCE_PER_SESSION) {
             // If the cached response does not include expiration infor-
             // mation, then we must validate the response, despite whether
             // or not this is the first access this session.  This behavior
             // is consistent with existing browsers and is generally expected
             // by web authors.
-            rv = mCachedResponseHead->ComputeFreshnessLifetime(&time);
-            NS_ENSURE_SUCCESS(rv, rv);
-
-            if (time == 0)
+            if (freshness == 0)
                 doValidation = true;
             else
                 doValidation = fromPreviousSession;
         }
         else
             doValidation = true;
 
         LOG(("%salidating based on expiration time\n", doValidation ? "V" : "Not v"));
--- a/netwerk/protocol/http/nsHttpHeaderArray.cpp
+++ b/netwerk/protocol/http/nsHttpHeaderArray.cpp
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // HttpLog.h should generally be included first
 #include "HttpLog.h"
 
 #include "nsHttpHeaderArray.h"
 #include "nsURLHelper.h"
 #include "nsIHttpHeaderVisitor.h"
+#include "nsHttpHandler.h"
 
 namespace mozilla {
 namespace net {
 
 //-----------------------------------------------------------------------------
 // nsHttpHeaderArray <public>
 //-----------------------------------------------------------------------------
 nsresult
--- a/netwerk/test/unit/head_cache2.js
+++ b/netwerk/test/unit/head_cache2.js
@@ -405,15 +405,25 @@ MultipleCallbacks.prototype =
 
 function MultipleCallbacks(number, goon, delayed)
 {
   this.pending = number;
   this.goon = goon;
   this.delayed = delayed;
 }
 
+function wait_for_cache_index(continue_func)
+{
+  // This callback will not fire before the index is in the ready state.  nsICacheStorage.exists() will
+  // no longer throw after this point.
+  get_cache_service().asyncGetDiskConsumption({
+    onNetworkCacheDiskConsumption: function() { continue_func(); },
+    QueryInterface() { return this; }
+  });
+}
+
 function finish_cache2_test()
 {
   callbacks.forEach(function(callback, index) {
     callback.selfCheck();
   });
   do_test_finished();
 }
new file mode 100644
--- /dev/null
+++ b/netwerk/test/unit/test_cache-control_request.js
@@ -0,0 +1,379 @@
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+var httpserver = new HttpServer();
+httpserver.start(-1);
+var cache = null;
+
+var base_url = "http://localhost:" + httpserver.identity.primaryPort;
+var resource_age_100 = "/resource_age_100";
+var resource_age_100_url = base_url + resource_age_100;
+var resource_stale_100 = "/resource_stale_100";
+var resource_stale_100_url = base_url + resource_stale_100;
+var resource_fresh_100 = "/resource_fresh_100";
+var resource_fresh_100_url = base_url + resource_fresh_100;
+
+// Test flags
+var hit_server = false;
+
+
+function make_channel(url, cache_control)
+{
+  // Reset test global status
+  hit_server = false;
+
+  var req = NetUtil.newChannel({uri: url, loadUsingSystemPrincipal: true});
+  req.QueryInterface(Ci.nsIHttpChannel);
+  if (cache_control) {
+    req.setRequestHeader("Cache-control", cache_control, false);
+  }
+
+  return req;
+}
+
+function make_uri(url) {
+  var ios = Cc["@mozilla.org/network/io-service;1"].
+            getService(Ci.nsIIOService);
+  return ios.newURI(url, null, null);
+}
+
+function resource_age_100_handler(metadata, response)
+{
+  hit_server = true;
+
+  response.setStatusLine(metadata.httpVersion, 200, "OK");
+  response.setHeader("Content-Type", "text/plain", false);
+  response.setHeader("Age", "100", false);
+  response.setHeader("Last-Modified", date_string_from_now(-100), false);
+  response.setHeader("Expires", date_string_from_now(+9999), false);
+
+  const body = "data1";
+  response.bodyOutputStream.write(body, body.length);
+}
+
+function resource_stale_100_handler(metadata, response)
+{
+  hit_server = true;
+
+  response.setStatusLine(metadata.httpVersion, 200, "OK");
+  response.setHeader("Content-Type", "text/plain", false);
+  response.setHeader("Date", date_string_from_now(-200), false);
+  response.setHeader("Last-Modified", date_string_from_now(-200), false);
+  response.setHeader("Cache-Control", "max-age=100", false);
+  response.setHeader("Expires", date_string_from_now(-100), false);
+
+  const body = "data2";
+  response.bodyOutputStream.write(body, body.length);
+}
+
+function resource_fresh_100_handler(metadata, response)
+{
+  hit_server = true;
+
+  response.setStatusLine(metadata.httpVersion, 200, "OK");
+  response.setHeader("Content-Type", "text/plain", false);
+  response.setHeader("Last-Modified", date_string_from_now(0), false);
+  response.setHeader("Cache-Control", "max-age=100", false);
+  response.setHeader("Expires", date_string_from_now(+100), false);
+
+  const body = "data3";
+  response.bodyOutputStream.write(body, body.length);
+}
+
+
+function run_test()
+{
+  do_get_profile();
+  do_test_pending();
+
+  httpserver.registerPathHandler(resource_age_100, resource_age_100_handler);
+  httpserver.registerPathHandler(resource_stale_100, resource_stale_100_handler);
+  httpserver.registerPathHandler(resource_fresh_100, resource_fresh_100_handler);
+  cache = getCacheStorage("disk");
+
+  wait_for_cache_index(run_next_test);
+}
+
+// Here starts the list of tests
+
+// ============================================================================
+// Cache-Control: no-store
+
+add_test(() => {
+  // Must not create a cache entry
+  var ch = make_channel(resource_age_100_url, "no-store");
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_true(hit_server);
+    do_check_false(cache.exists(make_uri(resource_age_100_url), ""));
+
+    run_next_test();
+  }, null), null);
+});
+
+add_test(() => {
+  // Prepare state only, cache the entry
+  var ch = make_channel(resource_age_100_url);
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_true(hit_server);
+    do_check_true(cache.exists(make_uri(resource_age_100_url), ""));
+
+    run_next_test();
+  }, null), null);
+});
+
+add_test(() => {
+  // Check the prepared cache entry is used when no special directives are added
+  var ch = make_channel(resource_age_100_url);
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_false(hit_server);
+    do_check_true(cache.exists(make_uri(resource_age_100_url), ""));
+
+    run_next_test();
+  }, null), null);
+});
+
+add_test(() => {
+  // Try again, while we already keep a cache entry,
+  // the channel must not use it, entry should stay in the cache
+  var ch = make_channel(resource_age_100_url, "no-store");
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_true(hit_server);
+    do_check_true(cache.exists(make_uri(resource_age_100_url), ""));
+
+    run_next_test();
+  }, null), null);
+});
+
+// ============================================================================
+// Cache-Control: no-cache
+
+add_test(() => {
+  // Check the prepared cache entry is used when no special directives are added
+  var ch = make_channel(resource_age_100_url);
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_false(hit_server);
+    do_check_true(cache.exists(make_uri(resource_age_100_url), ""));
+
+    run_next_test();
+  }, null), null);
+});
+
+add_test(() => {
+  // The existing entry should be revalidated (we expect a server hit)
+  var ch = make_channel(resource_age_100_url, "no-cache");
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_true(hit_server);
+    do_check_true(cache.exists(make_uri(resource_age_100_url), ""));
+
+    run_next_test();
+  }, null), null);
+});
+
+// ============================================================================
+// Cache-Control: max-age
+
+add_test(() => {
+  // Check the prepared cache entry is used when no special directives are added
+  var ch = make_channel(resource_age_100_url);
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_false(hit_server);
+    do_check_true(cache.exists(make_uri(resource_age_100_url), ""));
+
+    run_next_test();
+  }, null), null);
+});
+
+add_test(() => {
+  // The existing entry's age is greater than the maximum requested,
+  // should hit server
+  var ch = make_channel(resource_age_100_url, "max-age=10");
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_true(hit_server);
+    do_check_true(cache.exists(make_uri(resource_age_100_url), ""));
+
+    run_next_test();
+  }, null), null);
+});
+
+add_test(() => {
+  // The existing entry's age is greater than the maximum requested,
+  // but the max-stale directive says to use it when it's fresh enough
+  var ch = make_channel(resource_age_100_url, "max-age=10, max-stale=99999");
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_false(hit_server);
+    do_check_true(cache.exists(make_uri(resource_age_100_url), ""));
+
+    run_next_test();
+  }, null), null);
+});
+
+add_test(() => {
+  // The existing entry's age is lesser than the maximum requested,
+  // should go from cache
+  var ch = make_channel(resource_age_100_url, "max-age=1000");
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_false(hit_server);
+    do_check_true(cache.exists(make_uri(resource_age_100_url), ""));
+
+    run_next_test();
+  }, null), null);
+});
+
+// ============================================================================
+// Cache-Control: max-stale
+
+add_test(() => {
+  // Preprate the entry first
+  var ch = make_channel(resource_stale_100_url);
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_true(hit_server);
+    do_check_true(cache.exists(make_uri(resource_stale_100_url), ""));
+
+    // Must shift the expiration time set on the entry to |now| be in the past
+    do_timeout(1500, run_next_test);
+  }, null), null);
+});
+
+add_test(() => {
+  // Check it's not reused (as it's stale) when no special directives
+  // are provided
+  var ch = make_channel(resource_stale_100_url);
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_true(hit_server);
+    do_check_true(cache.exists(make_uri(resource_stale_100_url), ""));
+
+    do_timeout(1500, run_next_test);
+  }, null), null);
+});
+
+add_test(() => {
+  // Accept cached responses of any stale time
+  var ch = make_channel(resource_stale_100_url, "max-stale");
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_false(hit_server);
+    do_check_true(cache.exists(make_uri(resource_stale_100_url), ""));
+
+    do_timeout(1500, run_next_test);
+  }, null), null);
+});
+
+add_test(() => {
+  // The entry is stale only by 100 seconds, accept it
+  var ch = make_channel(resource_stale_100_url, "max-stale=1000");
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_false(hit_server);
+    do_check_true(cache.exists(make_uri(resource_stale_100_url), ""));
+
+    do_timeout(1500, run_next_test);
+  }, null), null);
+});
+
+add_test(() => {
+  // The entry is stale by 100 seconds but we only accept a 10 seconds stale
+  // entry, go from server
+  var ch = make_channel(resource_stale_100_url, "max-stale=10");
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_true(hit_server);
+    do_check_true(cache.exists(make_uri(resource_stale_100_url), ""));
+
+    run_next_test();
+  }, null), null);
+});
+
+// ============================================================================
+// Cache-Control: min-fresh
+
+add_test(() => {
+  // Preprate the entry first
+  var ch = make_channel(resource_fresh_100_url);
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_true(hit_server);
+    do_check_true(cache.exists(make_uri(resource_fresh_100_url), ""));
+
+    run_next_test();
+  }, null), null);
+});
+
+add_test(() => {
+  // Check it's reused when no special directives are provided
+  var ch = make_channel(resource_fresh_100_url);
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_false(hit_server);
+    do_check_true(cache.exists(make_uri(resource_fresh_100_url), ""));
+
+    run_next_test();
+  }, null), null);
+});
+
+add_test(() => {
+  // Entry fresh enough to be served from the cache
+  var ch = make_channel(resource_fresh_100_url, "min-fresh=10");
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_false(hit_server);
+    do_check_true(cache.exists(make_uri(resource_fresh_100_url), ""));
+
+    run_next_test();
+  }, null), null);
+});
+
+add_test(() => {
+  // The entry is not fresh enough
+  var ch = make_channel(resource_fresh_100_url, "min-fresh=1000");
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_true(hit_server);
+    do_check_true(cache.exists(make_uri(resource_fresh_100_url), ""));
+
+    run_next_test();
+  }, null), null);
+});
+
+// ============================================================================
+// Parser test, if the Cache-Control header would not parse correctly, the entry
+// doesn't load from the server.
+
+add_test(() => {
+  var ch = make_channel(resource_fresh_100_url, "unknown1,unknown2 = \"a,b\",  min-fresh = 1000 ");
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_true(hit_server);
+    do_check_true(cache.exists(make_uri(resource_fresh_100_url), ""));
+
+    run_next_test();
+  }, null), null);
+});
+
+add_test(() => {
+  var ch = make_channel(resource_fresh_100_url, "no-cache = , min-fresh = 10");
+  ch.asyncOpen(new ChannelListener(function(request, data) {
+    do_check_true(hit_server);
+    do_check_true(cache.exists(make_uri(resource_fresh_100_url), ""));
+
+    run_next_test();
+  }, null), null);
+});
+
+// ============================================================================
+// Done
+
+add_test(() => {
+  run_next_test();
+  httpserver.stop(do_test_finished);
+});
+
+// ============================================================================
+// Helpers
+
+function date_string_from_now(delta_secs) {
+    var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug',
+                  'Sep', 'Oct', 'Nov', 'Dec'];
+    var days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
+
+    var d = new Date();
+    d.setTime(d.getTime() + delta_secs * 1000);
+    return days[d.getUTCDay()] + ", " +
+           d.getUTCDate() + " " +
+           months[d.getUTCMonth()] + " " +
+           d.getUTCFullYear() + " " +
+           d.getUTCHours() + ":" +
+           d.getUTCMinutes() + ":" +
+           d.getUTCSeconds() + " UTC";
+}
--- a/netwerk/test/unit/xpcshell.ini
+++ b/netwerk/test/unit/xpcshell.ini
@@ -351,9 +351,9 @@ skip-if = os == "android"
 [test_dns_disable_ipv6.js]
 [test_packaged_app_service_paths.js]
 [test_bug1195415.js]
 [test_cookie_blacklist.js]
 [test_getHost.js]
 [test_packaged_app_bug1214079.js]
 [test_bug412457.js]
 [test_bug464591.js]
-
+[test_cache-control_request.js]