Bug 1325081 - Change nsHttpChannel to be able to race network with cache r=michal draft
authorValentin Gosu <valentin.gosu@gmail.com>
Thu, 16 Feb 2017 15:20:13 +0100
changeset 485283 f908b168eb76781e3f398efed9b111a0aca63c29
parent 485282 a89470efe71442fb6af36e74ac582e232557f81d
child 545981 3d41541893ce46485e7aa30c0f2824ad309f6c8a
push id45694
push uservalentin.gosu@gmail.com
push dateThu, 16 Feb 2017 14:21:00 +0000
reviewersmichal
bugs1325081
milestone54.0a1
Bug 1325081 - Change nsHttpChannel to be able to race network with cache r=michal MozReview-Commit-ID: LmIK9RiKsKp
netwerk/protocol/http/nsHttpChannel.cpp
netwerk/protocol/http/nsHttpChannel.h
netwerk/test/unit/head_channels.js
netwerk/test/unit/test_race_cache_network.js
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -123,16 +123,21 @@ static uint64_t gNumIntercepted = 0;
         (loadFlags & (nsIRequest::LOAD_BYPASS_CACHE | \
                       nsICachingChannel::LOAD_BYPASS_LOCAL_CACHE))
 
 #define RECOVER_FROM_CACHE_FILE_ERROR(result) \
         ((result) == NS_ERROR_FILE_NOT_FOUND || \
          (result) == NS_ERROR_FILE_CORRUPTED || \
          (result) == NS_ERROR_OUT_OF_MEMORY)
 
+#define WRONG_RACING_RESPONSE_SOURCE(req)                                                  \
+    (mRacingNetAndCache &&                                                                 \
+        (((mFirstResponseSource == RESPONSE_FROM_CACHE) && (req != mCachePump)) ||         \
+         ((mFirstResponseSource == RESPONSE_FROM_NETWORK) && (req != mTransactionPump))))
+
 static NS_DEFINE_CID(kStreamListenerTeeCID, NS_STREAMLISTENERTEE_CID);
 static NS_DEFINE_CID(kStreamTransportServiceCID,
                      NS_STREAMTRANSPORTSERVICE_CID);
 
 enum CacheDisposition {
     kCacheHit = 1,
     kCacheHitViaReval = 2,
     kCacheMissedViaReval = 3,
@@ -275,16 +280,18 @@ nsHttpChannel::nsHttpChannel()
     , mHasAutoRedirectVetoNotifier(0)
     , mPinCacheContent(0)
     , mIsCorsPreflightDone(0)
     , mStronglyFramed(false)
     , mPushedStream(nullptr)
     , mLocalBlocklist(false)
     , mWarningReporter(nullptr)
     , mIsReadingFromCache(false)
+    , mOnCacheAvailableCalled(false)
+    , mRacingNetAndCache(false)
     , mDidReval(false)
 {
     LOG(("Creating nsHttpChannel [this=%p]\n", this));
     mChannelCreationTime = PR_Now();
     mChannelCreationTimestamp = TimeStamp::Now();
 }
 
 nsHttpChannel::~nsHttpChannel()
@@ -417,16 +424,24 @@ nsHttpChannel::Connect()
 
     // open a cache entry for this channel...
     rv = OpenCacheEntry(isHttps);
 
     // do not continue if asyncOpenCacheEntry is in progress
     if (AwaitingCacheCallbacks()) {
         LOG(("nsHttpChannel::Connect %p AwaitingCacheCallbacks forces async\n", this));
         MOZ_ASSERT(NS_SUCCEEDED(rv), "Unexpected state");
+
+        if (mNetworkTriggered && mWaitingForProxy) {
+            // Someone has called TriggerNetwork(), meaning we are racing the
+            // network with the cache.
+            mWaitingForProxy = false;
+            return TryHSTSPriming();
+        }
+
         return NS_OK;
     }
 
     if (NS_FAILED(rv)) {
         LOG(("OpenCacheEntry failed [rv=%x]\n", rv));
         // if this channel is only allowed to pull from the cache, then
         // we must fail if we were unable to open a cache entry.
         if (mLoadFlags & LOAD_ONLY_FROM_CACHE) {
@@ -435,17 +450,17 @@ nsHttpChannel::Connect()
             if (!mFallbackChannel && !mFallbackKey.IsEmpty()) {
                 return AsyncCall(&nsHttpChannel::HandleAsyncFallback);
             }
             return NS_ERROR_DOCUMENT_NOT_CACHED;
         }
         // otherwise, let's just proceed without using the cache.
     }
 
-    return TryHSTSPriming();
+    return TriggerNetwork(0);
 }
 
 nsresult
 nsHttpChannel::TryHSTSPriming()
 {
     if (mLoadInfo) {
         // HSTS priming requires the LoadInfo provided with AsyncOpen2
         bool requireHSTSPriming =
@@ -3745,16 +3760,22 @@ 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));
 
+    if (mRacingNetAndCache && mFirstResponseSource == RESPONSE_FROM_NETWORK) {
+        LOG(("Not using cached response because we've already got one from the network\n"));
+        *aResult = ENTRY_NOT_WANTED;
+        return NS_OK;
+    }
+
     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;
@@ -4182,18 +4203,19 @@ nsHttpChannel::OnCacheEntryCheck(nsICach
             mCachedContentIsValid = false;
         }
     }
 
     if (mDidReval)
         *aResult = ENTRY_NEEDS_REVALIDATION;
     else if (wantCompleteEntry)
         *aResult = RECHECK_AFTER_WRITE_FINISHED;
-    else
+    else {
         *aResult = ENTRY_WANTED;
+    }
 
     if (mCachedContentIsValid) {
         entry->MaybeMarkValid();
     }
 
     LOG(("nsHTTPChannel::OnCacheEntryCheck exit [this=%p doValidation=%d result=%d]\n",
          this, doValidation, *aResult));
     return rv;
@@ -4201,16 +4223,17 @@ nsHttpChannel::OnCacheEntryCheck(nsICach
 
 NS_IMETHODIMP
 nsHttpChannel::OnCacheEntryAvailable(nsICacheEntry *entry,
                                      bool aNew,
                                      nsIApplicationCache* aAppCache,
                                      nsresult status)
 {
     MOZ_ASSERT(NS_IsMainThread());
+    mOnCacheAvailableCalled = true;
 
     nsresult rv;
 
     LOG(("nsHttpChannel::OnCacheEntryAvailable [this=%p entry=%p "
          "new=%d appcache=%p status=%x mAppCache=%p mAppCacheForWrite=%p]\n",
          this, entry, aNew, aAppCache, status,
          mApplicationCache.get(), mApplicationCacheForWrite.get()));
 
@@ -4272,17 +4295,21 @@ nsHttpChannel::OnCacheEntryAvailableInte
         return rv;
     }
 
     // We may be waiting for more callbacks...
     if (AwaitingCacheCallbacks()) {
         return NS_OK;
     }
 
-    return TryHSTSPriming();
+    if (mCachedContentIsValid && mNetworkTriggered) {
+        ReadFromCache(true);
+    }
+
+    return TriggerNetwork(0);
 }
 
 nsresult
 nsHttpChannel::OnNormalCacheEntryAvailable(nsICacheEntry *aEntry,
                                            bool aNew,
                                            nsresult aEntryStatus)
 {
     mCacheEntriesToWaitFor &= ~WAIT_FOR_CACHE_ENTRY;
@@ -4724,16 +4751,17 @@ nsHttpChannel::OpenCacheInputStream(nsIC
 
 // Actually process the cached response that we started to handle in CheckCache
 // and/or StartBufferingCachedEntity.
 nsresult
 nsHttpChannel::ReadFromCache(bool alreadyMarkedValid)
 {
     NS_ENSURE_TRUE(mCacheEntry, NS_ERROR_FAILURE);
     NS_ENSURE_TRUE(mCachedContentIsValid, NS_ERROR_FAILURE);
+    NS_ENSURE_TRUE(!mCachePump, NS_OK); // already opened
 
     LOG(("nsHttpChannel::ReadFromCache [this=%p] "
          "Using cached copy of: %s\n", this, mSpec.get()));
 
     if (mCachedResponseHead)
         mResponseHead = Move(mCachedResponseHead);
 
     UpdateInhibitPersistentCachingFlag();
@@ -4790,17 +4818,16 @@ nsHttpChannel::ReadFromCache(bool alread
 
     MOZ_ASSERT(mCacheInputStream);
     if (!mCacheInputStream) {
         NS_ERROR("mCacheInputStream is null but we're expecting to "
                         "be able to read from it.");
         return NS_ERROR_UNEXPECTED;
     }
 
-
     nsCOMPtr<nsIInputStream> inputStream = mCacheInputStream.forget();
 
     rv = nsInputStreamPump::Create(getter_AddRefs(mCachePump), inputStream,
                                    int64_t(-1), int64_t(-1), 0, 0, true);
     if (NS_FAILED(rv)) {
         inputStream->Close();
         return rv;
     }
@@ -5704,30 +5731,36 @@ nsHttpChannel::Cancel(nsresult status)
     }
     if (mWaitingForRedirectCallback) {
         LOG(("channel canceled during wait for redirect callback"));
     }
     mCanceled = true;
     mStatus = status;
     if (mProxyRequest)
         mProxyRequest->Cancel(status);
-    if (mTransaction)
-        gHttpHandler->CancelTransaction(mTransaction, status);
-    if (mTransactionPump)
-        mTransactionPump->Cancel(status);
+    CancelNetworkRequest(status);
     mCacheInputStream.CloseAndRelease();
     if (mCachePump)
         mCachePump->Cancel(status);
     if (mAuthProvider)
         mAuthProvider->Cancel(status);
     if (mPreflightChannel)
         mPreflightChannel->Cancel(status);
     return NS_OK;
 }
 
+void
+nsHttpChannel::CancelNetworkRequest(nsresult aStatus)
+{
+    if (mTransaction)
+        gHttpHandler->CancelTransaction(mTransaction, aStatus);
+    if (mTransactionPump)
+        mTransactionPump->Cancel(aStatus);
+}
+
 NS_IMETHODIMP
 nsHttpChannel::Suspend()
 {
     nsresult rv = SuspendInternal();
 
     nsresult rvParentChannel = NS_OK;
     if (mParentChannel) {
       rvParentChannel = mParentChannel->SuspendMessageDiversion();
@@ -5773,16 +5806,17 @@ nsHttpChannel::AsyncOpen(nsIStreamListen
     MOZ_ASSERT(!mLoadInfo ||
                mLoadInfo->GetSecurityMode() == 0 ||
                mLoadInfo->GetInitialSecurityCheckDone() ||
                (mLoadInfo->GetSecurityMode() == nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL &&
                 nsContentUtils::IsSystemPrincipal(mLoadInfo->LoadingPrincipal())),
                "security flags in loadInfo but asyncOpen2() not called");
 
     LOG(("nsHttpChannel::AsyncOpen [this=%p]\n", this));
+
 #ifdef MOZ_TASK_TRACER
     {
         uint64_t sourceEventId, parentTaskId;
         tasktracer::SourceEventType sourceEventType;
         GetCurTraceInfo(&sourceEventId, &parentTaskId, &sourceEventType);
         nsCOMPtr<nsIURI> uri;
         GetURI(getter_AddRefs(uri));
         nsAutoCString urispec;
@@ -6518,29 +6552,50 @@ nsHttpChannel::GetRequestMethod(nsACStri
 NS_IMETHODIMP
 nsHttpChannel::OnStartRequest(nsIRequest *request, nsISupports *ctxt)
 {
     nsresult rv;
 
     PROFILER_LABEL("nsHttpChannel", "OnStartRequest",
         js::ProfileEntry::Category::NETWORK);
 
-    if (!(mCanceled || NS_FAILED(mStatus))) {
+    if (!(mCanceled || NS_FAILED(mStatus)) && !WRONG_RACING_RESPONSE_SOURCE(request)) {
         // capture the request's status, so our consumers will know ASAP of any
         // connection failures, etc - bug 93581
         request->GetStatus(&mStatus);
     }
 
     LOG(("nsHttpChannel::OnStartRequest [this=%p request=%p status=%x]\n",
         this, request, mStatus));
 
+    if (mRacingNetAndCache) {
+        LOG(("  racingNetAndCache - mFirstResponseSource:%d fromCache:%d fromNet:%d\n",
+             mFirstResponseSource, request == mCachePump, request == mTransactionPump));
+        if (mFirstResponseSource == RESPONSE_PENDING &&
+            request == mTransactionPump) {
+            LOG(("  First response from network\n"));
+            mFirstResponseSource = RESPONSE_FROM_NETWORK;
+        } else if (mFirstResponseSource == RESPONSE_PENDING &&
+            request == mCachePump) {
+            LOG(("  First response from cache\n"));
+            mFirstResponseSource = RESPONSE_FROM_CACHE;
+
+            // XXX: Consider cancelling H2 transactions  or H1 transactions
+            // that are not keep-alive.
+        } else if (WRONG_RACING_RESPONSE_SOURCE(request)) {
+            LOG(("  Early return when racing. This response not needed."));
+            return NS_OK;
+        }
+    }
+
     // Make sure things are what we expect them to be...
     MOZ_ASSERT(request == mCachePump || request == mTransactionPump,
                "Unexpected request");
-    MOZ_ASSERT(!(mTransactionPump && mCachePump) || mCachedContentIsPartial,
+
+    MOZ_ASSERT(mRacingNetAndCache || !(mTransactionPump && mCachePump) || mCachedContentIsPartial,
                "If we have both pumps, the cache content must be partial");
 
     mAfterOnStartRequestBegun = true;
     mOnStartRequestTimestamp = TimeStamp::Now();
 
     if (!mSecurityInfo && !mCachePump && mTransaction) {
         // grab the security info from the connection object; the transaction
         // is guaranteed to own a reference to the connection.
@@ -6667,19 +6722,25 @@ NS_IMETHODIMP
 nsHttpChannel::OnStopRequest(nsIRequest *request, nsISupports *ctxt, nsresult status)
 {
     PROFILER_LABEL("nsHttpChannel", "OnStopRequest",
         js::ProfileEntry::Category::NETWORK);
 
     LOG(("nsHttpChannel::OnStopRequest [this=%p request=%p status=%x]\n",
         this, request, status));
 
+    LOG(("OnStopRequest %p requestFromCache: %d mFirstResponseSource: %d\n", this, request == mCachePump, mFirstResponseSource));
+
     MOZ_ASSERT(NS_IsMainThread(),
                "OnStopRequest should only be called from the main thread");
 
+    if (WRONG_RACING_RESPONSE_SOURCE(request)) {
+        return NS_OK;
+    }
+
     if (NS_FAILED(status)) {
         ProcessSecurityReport(status);
     }
 
     // If this load failed because of a security error, it may be because we
     // are in a captive portal - trigger an async check to make sure.
     int32_t nsprError = -1 * NS_ERROR_GET_CODE(status);
     if (mozilla::psm::IsNSSErrorCode(nsprError)) {
@@ -6827,18 +6888,22 @@ nsHttpChannel::OnStopRequest(nsIRequest 
                 mListener->OnStartRequest(this, mListenerContext);
                 mOnStartRequestCalled = true;
             } else {
                 NS_WARNING("OnStartRequest skipped because of null listener");
             }
         }
 
         // if this transaction has been replaced, then bail.
-        if (mTransactionReplaced)
+        if (mTransactionReplaced) {
+            LOG(("Transaction replaced\n"));
+            // This was just the network check for a 304 response.
+            mFirstResponseSource = RESPONSE_PENDING;
             return NS_OK;
+        }
 
         if (mUpgradeProtocolCallback && stickyConn &&
             mResponseHead && mResponseHead->Status() == 101) {
             gHttpHandler->ConnMgr()->CompleteUpgrade(stickyConn,
                                                      mUpgradeProtocolCallback);
         }
     }
 
@@ -6976,26 +7041,29 @@ nsHttpChannel::OnDataAvailable(nsIReques
                                uint64_t offset, uint32_t count)
 {
     PROFILER_LABEL("nsHttpChannel", "OnDataAvailable",
         js::ProfileEntry::Category::NETWORK);
 
     LOG(("nsHttpChannel::OnDataAvailable [this=%p request=%p offset=%llu count=%u]\n",
         this, request, offset, count));
 
+    LOG(("OnDataAvailable %p requestFromCache: %d mFirstResponseSource: %d\n", this, request == mCachePump, mFirstResponseSource));
+
     // don't send out OnDataAvailable notifications if we've been canceled.
     if (mCanceled)
         return mStatus;
 
     MOZ_ASSERT(mResponseHead, "No response head in ODA!!");
 
     MOZ_ASSERT(!(mCachedContentIsPartial && (request == mTransactionPump)),
                "transaction pump not suspended");
 
-    if (mAuthRetryPending || (request == mTransactionPump && mTransactionReplaced)) {
+    if (mAuthRetryPending || WRONG_RACING_RESPONSE_SOURCE(request) ||
+        (request == mTransactionPump && mTransactionReplaced)) {
         uint32_t n;
         return input->ReadSegments(NS_DiscardSegment, nullptr, count, &n);
     }
 
     mIsReadingFromCache = (request == mCachePump);
 
     if (mListener) {
         //
@@ -7217,21 +7285,27 @@ nsHttpChannel::OnTransportStatus(nsITran
 //-----------------------------------------------------------------------------
 
 NS_IMETHODIMP
 nsHttpChannel::IsFromCache(bool *value)
 {
     if (!mIsPending)
         return NS_ERROR_NOT_AVAILABLE;
 
-    // return false if reading a partial cache entry; the data isn't entirely
-    // from the cache!
-
-    *value = (mCachePump || (mLoadFlags & LOAD_ONLY_IF_MODIFIED)) &&
-              mCachedContentIsValid && !mCachedContentIsPartial;
+    if (!mRacingNetAndCache) {
+        // return false if reading a partial cache entry; the data isn't
+        // entirely from the cache!
+        *value = (mCachePump || (mLoadFlags & LOAD_ONLY_IF_MODIFIED)) &&
+                  mCachedContentIsValid && !mCachedContentIsPartial;
+        return NS_OK;
+    }
+
+    // If we are racing network and cache (or skipping the cache)
+    // we just return the first response source.
+    *value = mFirstResponseSource == RESPONSE_FROM_CACHE;
 
     return NS_OK;
 }
 
 NS_IMETHODIMP
 nsHttpChannel::GetCacheTokenExpirationTime(uint32_t *_retval)
 {
     NS_ENSURE_ARG_POINTER(_retval);
@@ -8478,57 +8552,105 @@ nsHttpChannel::ReportNetVSCacheTelemetry
     } else {
         Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_LARGE_V2, onStopDiff);
     }
 }
 
 NS_IMETHODIMP
 nsHttpChannel::Test_delayCacheEntryOpeningBy(int32_t aTimeout)
 {
+    MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread");
     mCacheOpenDelay = aTimeout;
     return NS_OK;
 }
 
 NS_IMETHODIMP
 nsHttpChannel::Test_triggerDelayedOpenCacheEntry()
 {
+    MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread");
     nsresult rv;
     if (!mCacheOpenDelay) {
         // No delay was set.
         return NS_ERROR_NOT_AVAILABLE;
     }
     if (!mCacheOpenRunnable) {
         // There should be a runnable.
         return NS_ERROR_FAILURE;
     }
     if (mCacheOpenTimer) {
         rv = mCacheOpenTimer->Cancel();
         if (NS_FAILED(rv)) {
             return rv;
         }
         mCacheOpenTimer = nullptr;
     }
+    nsCOMPtr<nsIRunnable> runnable;
+    mCacheOpenRunnable.swap(runnable);
     mCacheOpenDelay = 0;
-    mCacheOpenRunnable->Run();
-    mCacheOpenRunnable = nullptr;
+    runnable->Run();
+
+    return NS_OK;
+}
+
+nsresult
+nsHttpChannel::TriggerNetwork(int32_t aTimeout)
+{
+    MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread");
+    // If a network request has already gone out, there is no point in
+    // doing this again.
+    if (mNetworkTriggered) {
+        return NS_OK;
+    }
+
+    if (!aTimeout) {
+        mNetworkTriggered = true;
+        if (!mOnCacheAvailableCalled) {
+            // If the network was triggered before onCacheEntryAvailable was
+            // called, we are either racing network and cache, or the load is
+            // bypassing the cache.
+            mRacingNetAndCache = true;
+        }
+        if (mNetworkTriggerTimer) {
+            mNetworkTriggerTimer->Cancel();
+            mNetworkTriggerTimer = nullptr;
+        }
+
+        // If we are waiting for a proxy request, that means we can't trigger
+        // the next step just yet. We need for mConnectionInfo to be non-null
+        // before we call TryHSTSPriming. OnProxyAvailable will trigger
+        // BeginConnect, and Connect will call TryHSTSPriming even if it's
+        // for the cache callbacks.
+        if (mProxyRequest) {
+            mWaitingForProxy = true;
+            return NS_OK;
+        }
+
+        return TryHSTSPriming();
+    }
+
+    mNetworkTriggerTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
+    mNetworkTriggerTimer->InitWithCallback(this, aTimeout, nsITimer::TYPE_ONE_SHOT);
     return NS_OK;
 }
 
 NS_IMETHODIMP
 nsHttpChannel::Test_triggerNetwork(int32_t aTimeout)
 {
-    return TryHSTSPriming();
+    MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread");
+    return TriggerNetwork(aTimeout);
 }
 
 NS_IMETHODIMP
 nsHttpChannel::Notify(nsITimer *aTimer)
 {
     RefPtr<nsHttpChannel> self(this);
     if (aTimer == mCacheOpenTimer) {
         return Test_triggerDelayedOpenCacheEntry();
+    } else if (aTimer == mNetworkTriggerTimer) {
+        return TriggerNetwork(0);
     } else {
         MOZ_CRASH("Unknown timer");
     }
 
     return NS_OK;
 }
 
 } // namespace net
--- a/netwerk/protocol/http/nsHttpChannel.h
+++ b/netwerk/protocol/http/nsHttpChannel.h
@@ -24,17 +24,17 @@
 #include "TimingStruct.h"
 #include "ADivertableParentChannel.h"
 #include "AutoClose.h"
 #include "nsIStreamListener.h"
 #include "nsISupportsPrimitives.h"
 #include "nsICorsPreflightCallback.h"
 #include "AlternateServices.h"
 #include "nsIHstsPrimingCallback.h"
-#include <nsIRaceCacheWithNetwork.h>
+#include "nsIRaceCacheWithNetwork.h"
 
 class nsDNSPrefetch;
 class nsICancelable;
 class nsIHttpChannelAuthProvider;
 class nsInputStreamPump;
 class nsISSLStatus;
 
 namespace mozilla { namespace net {
@@ -616,16 +616,39 @@ private:
     // True if the channel is reading from cache.
     Atomic<bool> mIsReadingFromCache;
 
     // These next members are only used in unit tests to delay the call to
     // cache->AsyncOpenURI in order to race the cache with the network.
     nsCOMPtr<nsITimer> mCacheOpenTimer;
     nsCOMPtr<nsIRunnable> mCacheOpenRunnable;
     uint32_t mCacheOpenDelay = 0;
+
+    // We need to remember which is the source of the response we are using.
+    enum {
+        RESPONSE_PENDING,           // response is pending
+        RESPONSE_FROM_CACHE,        // response coming from cache. no network.
+        RESPONSE_FROM_NETWORK,      // response coming from the network
+    } mFirstResponseSource = RESPONSE_PENDING;
+
+    nsresult TriggerNetwork(int32_t aTimeout);
+    void CancelNetworkRequest(nsresult aStatus);
+    // Timer used to delay the network request, or to trigger the network
+    // request if retrieving the cache entry takes too long.
+    nsCOMPtr<nsITimer> mNetworkTriggerTimer;
+    // Is true if the network request has been triggered.
+    bool mNetworkTriggered = false;
+    bool mWaitingForProxy = false;
+    // Is true if the onCacheEntryAvailable callback has been called.
+    Atomic<bool> mOnCacheAvailableCalled;
+    // Will be true if the onCacheEntryAvailable callback is not called by the
+    // time we send the network request. This could also be true when we are
+    // bypassing the cache.
+    Atomic<bool> mRacingNetAndCache;
+
 protected:
     virtual void DoNotifyListenerCleanup() override;
 
 private: // cache telemetry
     bool mDidReval;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(nsHttpChannel, NS_HTTPCHANNEL_IID)
--- a/netwerk/test/unit/head_channels.js
+++ b/netwerk/test/unit/head_channels.js
@@ -47,16 +47,17 @@ const SUSPEND_DELAY = 3000;
  *
  * Note that it also requires a valid content length on the channel and
  * is thus not fully generic.
  */
 function ChannelListener(closure, ctx, flags) {
   this._closure = closure;
   this._closurectx = ctx;
   this._flags = flags;
+  this._isFromCache = false;
 }
 ChannelListener.prototype = {
   _closure: null,
   _closurectx: null,
   _buffer: "",
   _got_onstartrequest: false,
   _got_onstoprequest: false,
   _contentLen: -1,
@@ -72,16 +73,20 @@ ChannelListener.prototype = {
 
   onStartRequest: function(request, context) {
     try {
       if (this._got_onstartrequest)
         do_throw("Got second onStartRequest event!");
       this._got_onstartrequest = true;
       this._lastEvent = Date.now();
 
+      try {
+        this._isFromCache = request.QueryInterface(Ci.nsICachingChannel).isFromCache();
+      } catch (e) {}
+
       request.QueryInterface(Components.interfaces.nsIChannel);
       try {
         this._contentLen = request.contentLength;
       }
       catch (ex) {
         if (!(this._flags & (CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL)))
           do_throw("Could not get contentLength");
       }
@@ -102,17 +107,16 @@ ChannelListener.prototype = {
           do_throw("Response is from the cache (CL_NOT_FROM_CACHE)");
         }
       }
 
       if (this._flags & CL_SUSPEND) {
         request.suspend();
         do_timeout(SUSPEND_DELAY, function() { request.resume(); });
       }
-
     } catch (ex) {
       do_throw("Error in onStartRequest: " + ex);
     }
   },
 
   onDataAvailable: function(request, context, stream, offset, count) {
     try {
       let current = Date.now();
@@ -162,17 +166,17 @@ ChannelListener.prototype = {
       if (!(this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE | CL_IGNORE_CL)) &&
           !(this._flags & CL_EXPECT_GZIP) &&
           this._contentLen != -1)
           do_check_eq(this._buffer.length, this._contentLen)
     } catch (ex) {
       do_throw("Error in onStopRequest: " + ex);
     }
     try {
-      this._closure(request, this._buffer, this._closurectx);
+      this._closure(request, this._buffer, this._closurectx, this._isFromCache);
     } catch (ex) {
       do_throw("Error in closure function: " + ex);
     }
   }
 };
 
 var ES_ABORT_REDIRECT = 0x01;
 
--- a/netwerk/test/unit/test_race_cache_network.js
+++ b/netwerk/test/unit/test_race_cache_network.js
@@ -37,26 +37,43 @@ function test_handler(metadata, response
     g304Counter++;
   } else {
     response.setStatusLine(metadata.httpVersion, 200, "OK");
     response.bodyOutputStream.write(gResponseBody, gResponseBody.length);
     g200Counter++;
   }
 }
 
+function cached_handler(metadata, response) {
+  response.setHeader("Content-Type", "text/plain");
+  response.setHeader("Cache-Control", "Cache-Control: max-age=3600");
+  response.setHeader("ETag", "test-etag1");
+
+  response.setStatusLine(metadata.httpVersion, 200, "OK");
+  response.bodyOutputStream.write(gResponseBody, gResponseBody.length);
+
+  g200Counter++;
+}
+
 let gResponseCounter = 0;
-function checkContent(request, buffer)
+let gIsFromCache = 0;
+function checkContent(request, buffer, context, isFromCache)
 {
   do_check_eq(buffer, gResponseBody);
   gResponseCounter++;
-  testGenerator.next();
+  if (isFromCache) {
+    gIsFromCache++;
+  }
+  do_execute_soon(() => { testGenerator.next(); });
 }
 
 function run_test() {
+  do_get_profile();
   httpserver.registerPathHandler("/rcwn", test_handler);
+  httpserver.registerPathHandler("/rcwn_cached", cached_handler);
   testGenerator.next();
   do_test_pending();
 }
 
 let testGenerator = testSteps();
 function *testSteps() {
   // Initial request. Stores the response in the cache.
   var channel = make_channel("http://localhost:" + PORT + "/rcwn");
@@ -71,42 +88,137 @@ function *testSteps() {
   channel.asyncOpen2(new ChannelListener(checkContent, null));
   yield undefined;
   equal(gResponseCounter, 2);
   equal(g200Counter, 1, "check number of 200 responses");
   equal(g304Counter, 1, "check number of 304 responses");
 
   // Checks that delaying the response from the cache works.
   var channel = make_channel("http://localhost:" + PORT + "/rcwn");
-  channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_delayCacheEntryOpeningBy(1000);
+  channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_delayCacheEntryOpeningBy(200);
   let startTime = Date.now();
   channel.asyncOpen2(new ChannelListener(checkContent, null));
   yield undefined;
-  greater(Date.now() - startTime, 1000, "Check that timer works properly");
+  greater(Date.now() - startTime, 200, "Check that timer works properly");
   equal(gResponseCounter, 3);
   equal(g200Counter, 1, "check number of 200 responses");
   equal(g304Counter, 2, "check number of 304 responses");
 
   // Checks that we can trigger the cache open immediately, even if the cache delay is set very high.
   var channel = make_channel("http://localhost:" + PORT + "/rcwn");
   channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_delayCacheEntryOpeningBy(100000);
   channel.asyncOpen2(new ChannelListener(checkContent, null));
-  do_timeout(500, function() {
+  do_timeout(50, function() {
     channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerDelayedOpenCacheEntry();
   });
   yield undefined;
   equal(gResponseCounter, 4);
   equal(g200Counter, 1, "check number of 200 responses");
   equal(g304Counter, 3, "check number of 304 responses");
 
   // Sets a high delay for the cache fetch, and triggers the network activity.
   var channel = make_channel("http://localhost:" + PORT + "/rcwn");
   channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_delayCacheEntryOpeningBy(100000);
   channel.asyncOpen2(new ChannelListener(checkContent, null));
-  // Trigger network after 500 ms.
-  channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerNetwork(500);
+  // Trigger network after 50 ms.
+  channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerNetwork(50);
   yield undefined;
   equal(gResponseCounter, 5);
   equal(g200Counter, 2, "check number of 200 responses");
   equal(g304Counter, 3, "check number of 304 responses");
 
+  // Sets a high delay for the cache fetch, and triggers the network activity.
+  // While the network response is produced, we trigger the cache fetch.
+  // Because the network response was the first
+  var channel = make_channel("http://localhost:" + PORT + "/rcwn");
+  channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_delayCacheEntryOpeningBy(100000);
+  channel.asyncOpen2(new ChannelListener(checkContent, null));
+  do_timeout(50, function() {
+    channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerNetwork(0);
+    channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerDelayedOpenCacheEntry();
+  });
+  yield undefined;
+  equal(gResponseCounter, 6);
+  equal(g200Counter, 3, "check number of 200 responses");
+  equal(g304Counter, 3, "check number of 304 responses");
+
+  // Triggers cache open before triggering network.
+  var channel = make_channel("http://localhost:" + PORT + "/rcwn");
+  channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_delayCacheEntryOpeningBy(100000);
+  channel.asyncOpen2(new ChannelListener(checkContent, null));
+  do_timeout(50, function() {
+    channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerDelayedOpenCacheEntry();
+    channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerNetwork(0);
+  });
+  yield undefined;
+  equal(gResponseCounter, 7);
+  equal(g200Counter, 3, "check number of 200 responses");
+  equal(g304Counter, 4, "check number of 304 responses");
+
+  // Load the cached handler so we don't need to revalidate
+  var channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
+  channel.asyncOpen2(new ChannelListener(checkContent, null));
+  yield undefined;
+  equal(gResponseCounter, 8);
+  equal(g200Counter, 4, "check number of 200 responses");
+  equal(g304Counter, 4, "check number of 304 responses");
+
+  // Make sure response is loaded from the cache, not the network
+  var channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
+  channel.asyncOpen2(new ChannelListener(checkContent, null));
+  yield undefined;
+  equal(gResponseCounter, 9);
+  equal(g200Counter, 4, "check number of 200 responses");
+  equal(g304Counter, 4, "check number of 304 responses");
+
+  // Cache times out, so we trigger the network
+  gIsFromCache = 0;
+  var channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
+  channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_delayCacheEntryOpeningBy(100000);
+  channel.asyncOpen2(new ChannelListener(checkContent, null));
+  // trigger network after 50 ms
+  channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerNetwork(50);
+  yield undefined;
+  equal(gResponseCounter, 10);
+  equal(gIsFromCache, 0, "should be from the network");
+  equal(g200Counter, 5, "check number of 200 responses");
+  equal(g304Counter, 4, "check number of 304 responses");
+
+  // Cache callback comes back right after network is triggered.
+  var channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
+  channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_delayCacheEntryOpeningBy(100000);
+  channel.asyncOpen2(new ChannelListener(checkContent, null));
+  do_timeout(50, function() {
+    channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerNetwork(0);
+    channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerDelayedOpenCacheEntry();
+  });
+  yield undefined;
+  equal(gResponseCounter, 11);
+  do_print("IsFromCache: " + gIsFromCache + "\n");
+  do_print("Number of 200 responses: " + g200Counter + "\n");
+  equal(g304Counter, 4, "check number of 304 responses");
+
+  // Set an increasingly high timeout to trigger opening the cache entry
+  // This way we ensure that some of the entries we will get from the network,
+  // and some we will get from the cache.
+  gIsFromCache = 0;
+  for (var i = 0; i < 50; i++) {
+    var channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
+    channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_delayCacheEntryOpeningBy(100000);
+    channel.asyncOpen2(new ChannelListener(checkContent, null));
+    channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerNetwork(10);
+    // This may be racy. The delay was chosen because the distribution of net-cache
+    // results was around 25-25 on my machine.
+    do_timeout(i*100, function() {
+      try {
+        channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerDelayedOpenCacheEntry();
+      } catch (e) {}
+
+    });
+
+    yield undefined;
+  }
+
+  greater(gIsFromCache, 0, "Some of the responses should be from the cache");
+  less(gIsFromCache, 50, "Some of the responses should be from the net");
+
   httpserver.stop(do_test_finished);
 }