Bug 497003 - Support delivery of OnDataAvailable off the main thread r=bz r=jduell
authorSteve Workman <sworkman@mozilla.com>
Mon, 08 Jul 2013 18:45:25 -0700
changeset 145274 57ffaa460a6bb8f0507f119a4b5b62e0c430d99a
parent 145273 d032499bafe18baa637d872cd251691d92cfd154
child 145275 056043e8570df862cf2cf43fbcbb2526d5c03438
push id4085
push userbbajaj@mozilla.com
push dateMon, 05 Aug 2013 20:29:25 +0000
treeherdermozilla-aurora@ede8780a15bc [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz, jduell
bugs497003
milestone25.0a1
Bug 497003 - Support delivery of OnDataAvailable off the main thread r=bz r=jduell
netwerk/base/public/moz.build
netwerk/base/public/nsIInputStreamPump.idl
netwerk/base/public/nsIThreadRetargetableRequest.idl
netwerk/base/public/nsIThreadRetargetableStreamListener.idl
netwerk/base/public/nsStreamListenerWrapper.h
netwerk/base/src/nsInputStreamPump.cpp
netwerk/base/src/nsInputStreamPump.h
netwerk/base/src/nsStreamListenerTee.cpp
netwerk/base/src/nsStreamListenerTee.h
netwerk/base/src/nsStreamListenerWrapper.cpp
netwerk/protocol/http/HttpBaseChannel.cpp
netwerk/protocol/http/nsHttpChannel.cpp
netwerk/protocol/http/nsHttpChannel.h
netwerk/test/mochitests/Makefile.in
netwerk/test/mochitests/moz.build
netwerk/test/mochitests/partial_content.sjs
netwerk/test/mochitests/test_partially_cached_content.html
netwerk/test/moz.build
--- a/netwerk/base/public/moz.build
+++ b/netwerk/base/public/moz.build
@@ -90,16 +90,18 @@ XPIDL_SOURCES += [
     'nsIStandardURL.idl',
     'nsIStreamListener.idl',
     'nsIStreamListenerTee.idl',
     'nsIStreamLoader.idl',
     'nsIStreamTransportService.idl',
     'nsIStrictTransportSecurityService.idl',
     'nsISyncStreamListener.idl',
     'nsISystemProxySettings.idl',
+    'nsIThreadRetargetableRequest.idl',
+    'nsIThreadRetargetableStreamListener.idl',
     'nsITimedChannel.idl',
     'nsITraceableChannel.idl',
     'nsITransport.idl',
     'nsIUDPServerSocket.idl',
     'nsIURI.idl',
     'nsIURIChecker.idl',
     'nsIURIClassifier.idl',
     'nsIURIWithPrincipal.idl',
--- a/netwerk/base/public/nsIInputStreamPump.idl
+++ b/netwerk/base/public/nsIInputStreamPump.idl
@@ -6,20 +6,21 @@
 
 interface nsIInputStream;
 interface nsIStreamListener;
 
 /**
  * nsIInputStreamPump
  *
  * This interface provides a means to configure and use a input stream pump
- * instance.  The input stream pump will asynchronously read from a input
- * stream, and push data to a nsIStreamListener instance.  It utilizes the
+ * instance.  The input stream pump will asynchronously read from an input
+ * stream, and push data to an nsIStreamListener instance.  It utilizes the
  * current thread's nsIEventTarget in order to make reading from the stream
- * asynchronous.
+ * asynchronous. A different thread can be used if the pump also implements
+ * nsIThreadRetargetableRequest.
  *
  * If the given stream supports nsIAsyncInputStream, then the stream pump will
  * call the stream's AsyncWait method to drive the stream listener.  Otherwise,
  * the stream will be read on a background thread utilizing the stream
  * transport service.  More details are provided below.
  */
 [scriptable, uuid(400F5468-97E7-4d2b-9C65-A82AECC7AE82)]
 interface nsIInputStreamPump : nsIRequest
new file mode 100644
--- /dev/null
+++ b/netwerk/base/public/nsIThreadRetargetableRequest.idl
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 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 "nsISupports.idl"
+#include "nsIEventTarget.idl"
+
+/**
+ * nsIThreadRetargetableRequest
+ *
+ * Should be implemented by requests that support retargeting delivery of
+ * OnDataAvailable off the main thread. Note, OnStopRequest will be delivered
+ * back on the main thread.
+ */
+[uuid(27b84c48-5a73-4ba4-a8a4-8b5e649a145e)]
+interface nsIThreadRetargetableRequest : nsISupports
+{
+  /**
+   * Called to retarget delivery of OnDataAvailable to another thread. Should
+   * only be called within the context of OnStartRequest on the main thread.
+   * OnStopRequest will be delivered back on the main thread.
+   *
+   * @param aNewTarget New event target, e.g. thread or threadpool.
+   *
+   * Note: no return value is given. If the retargeting cannot be handled,
+   * normal delivery to the main thread will continue. As such, listeners
+   * should be ready to deal with OnDataAvailable on either the main thread or
+   * the new target thread.
+   */
+  void retargetDeliveryTo(in nsIEventTarget aNewTarget);
+};
+
+
new file mode 100644
--- /dev/null
+++ b/netwerk/base/public/nsIThreadRetargetableStreamListener.idl
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 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 "nsISupports.idl"
+
+/**
+ * nsIThreadRetargetableStreamListener
+ *
+ * To be used by classes which implement nsIStreamListener and whose
+ * OnDataAvailable callback may be retargeted for delivery off the main thread.
+ */
+[uuid(fb2304b8-f82f-4433-af68-d874a2ebbdc1)]
+interface nsIThreadRetargetableStreamListener : nsISupports
+{
+  /**
+   * Checks this listener and any next listeners it may have to verify that
+   * they can receive OnDataAvailable off the main thread. It is the
+   * responsibility of the implementing class to decide on the criteria to
+   * determine if retargeted delivery of these methods is possible, but it must
+   * check any and all nsIStreamListener objects that might be called in the
+   * listener chain.
+   *
+   * An exception should be thrown if a listener in the chain does not
+   * support retargeted delivery, i.e. if the next listener does not implement
+   * nsIThreadRetargetableStreamListener, or a call to its checkListenerChain()
+   * fails.
+   */
+  void checkListenerChain();
+};
+
--- a/netwerk/base/public/nsStreamListenerWrapper.h
+++ b/netwerk/base/public/nsStreamListenerWrapper.h
@@ -3,32 +3,35 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef nsStreamListenerWrapper_h__
 #define nsStreamListenerWrapper_h__
 
 #include "nsCOMPtr.h"
 #include "nsIStreamListener.h"
 #include "nsIRequestObserver.h"
+#include "nsIThreadRetargetableStreamListener.h"
 #include "mozilla/Attributes.h"
 
 // Wrapper class to make replacement of nsHttpChannel's listener
 // from JavaScript possible. It is workaround for bug 433711 and 682305.
 class nsStreamListenerWrapper MOZ_FINAL : public nsIStreamListener
+                                        , public nsIThreadRetargetableStreamListener
 {
 public:
   nsStreamListenerWrapper(nsIStreamListener *listener)
     : mListener(listener)
   {
     NS_ASSERTION(mListener, "no stream listener specified");
   }
 
   NS_DECL_ISUPPORTS
   NS_FORWARD_NSIREQUESTOBSERVER(mListener->)
   NS_FORWARD_NSISTREAMLISTENER(mListener->)
+  NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
 
 private:
   ~nsStreamListenerWrapper() {}
   nsCOMPtr<nsIStreamListener> mListener;
 };
 
 #endif // nsStreamListenerWrapper_h__
 
--- a/netwerk/base/src/nsInputStreamPump.cpp
+++ b/netwerk/base/src/nsInputStreamPump.cpp
@@ -1,25 +1,29 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
 /* vim:set ts=4 sts=4 sw=4 et cin: */
 /* 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 "mozilla/DebugOnly.h"
 #include "nsIOService.h"
 #include "nsInputStreamPump.h"
 #include "nsIServiceManager.h"
 #include "nsIStreamTransportService.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsISeekableStream.h"
 #include "nsITransport.h"
+#include "nsIThreadRetargetableStreamListener.h"
+#include "nsStreamUtils.h"
 #include "nsNetUtil.h"
 #include "nsThreadUtils.h"
 #include "nsCOMPtr.h"
 #include "prlog.h"
+#include "nsPrintfCString.h"
 #include "GeckoProfiler.h"
 #include <algorithm>
 
 static NS_DEFINE_CID(kStreamTransportServiceCID, NS_STREAMTRANSPORTSERVICE_CID);
 
 #if defined(PR_LOGGING)
 //
 // NSPR_LOG_MODULES=nsStreamPump:5
@@ -36,16 +40,17 @@ nsInputStreamPump::nsInputStreamPump()
     : mState(STATE_IDLE)
     , mStreamOffset(0)
     , mStreamLength(UINT64_MAX)
     , mStatus(NS_OK)
     , mSuspendCount(0)
     , mLoadFlags(LOAD_NORMAL)
     , mWaiting(false)
     , mCloseWhenDone(false)
+    , mRetargeting(false)
 {
 #if defined(PR_LOGGING)
     if (!gStreamPumpLog)
         gStreamPumpLog = PR_NewLogModule("nsStreamPump");
 #endif
 }
 
 nsInputStreamPump::~nsInputStreamPump()
@@ -114,38 +119,50 @@ nsInputStreamPump::PeekStream(PeekSegmen
                                     nsIOService::gDefaultSegmentSize,
                                     &dummy);
 }
 
 nsresult
 nsInputStreamPump::EnsureWaiting()
 {
     // no need to worry about multiple threads... an input stream pump lives
-    // on only one thread.
-
+    // on only one thread at a time.
+    MOZ_ASSERT(mAsyncStream);
     if (!mWaiting) {
+        // Ensure OnStateStop is called on the main thread.
+        if (mState == STATE_STOP) {
+            nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
+            if (mTargetThread != mainThread) {
+                mTargetThread = do_QueryInterface(mainThread);
+            }
+        }
+        MOZ_ASSERT(mTargetThread);
         nsresult rv = mAsyncStream->AsyncWait(this, 0, 0, mTargetThread);
         if (NS_FAILED(rv)) {
             NS_ERROR("AsyncWait failed");
             return rv;
         }
+        // Any retargeting during STATE_START or START_TRANSFER is complete
+        // after the call to AsyncWait; next callback wil be on mTargetThread.
+        mRetargeting = false;
         mWaiting = true;
     }
     return NS_OK;
 }
 
 //-----------------------------------------------------------------------------
 // nsInputStreamPump::nsISupports
 //-----------------------------------------------------------------------------
 
 // although this class can only be accessed from one thread at a time, we do
 // allow its ownership to move from thread to thread, assuming the consumer
 // understands the limitations of this.
-NS_IMPL_THREADSAFE_ISUPPORTS3(nsInputStreamPump,
+NS_IMPL_THREADSAFE_ISUPPORTS4(nsInputStreamPump,
                               nsIRequest,
+                              nsIThreadRetargetableRequest,
                               nsIInputStreamCallback,
                               nsIInputStreamPump)
 
 //-----------------------------------------------------------------------------
 // nsInputStreamPump::nsIRequest
 //-----------------------------------------------------------------------------
 
 NS_IMETHODIMP
@@ -268,16 +285,18 @@ nsInputStreamPump::Init(nsIInputStream *
     return NS_OK;
 }
 
 NS_IMETHODIMP
 nsInputStreamPump::AsyncRead(nsIStreamListener *listener, nsISupports *ctxt)
 {
     NS_ENSURE_TRUE(mState == STATE_IDLE, NS_ERROR_IN_PROGRESS);
     NS_ENSURE_ARG_POINTER(listener);
+    MOZ_ASSERT(NS_IsMainThread(), "nsInputStreamPump should be read from the "
+                                  "main thread only.");
 
     //
     // OK, we need to use the stream transport service if
     //
     // (1) the stream is blocking
     // (2) the stream does not support nsIAsyncInputStream
     //
 
@@ -367,33 +386,55 @@ nsInputStreamPump::OnInputStreamReady(ns
         switch (mState) {
         case STATE_START:
             nextState = OnStateStart();
             break;
         case STATE_TRANSFER:
             nextState = OnStateTransfer();
             break;
         case STATE_STOP:
+            mRetargeting = false;
             nextState = OnStateStop();
             break;
         default:
             nextState = 0;
             NS_NOTREACHED("Unknown enum value.");
             return NS_ERROR_UNEXPECTED;
         }
 
-        if (mState == nextState && !mSuspendCount) {
-            NS_ASSERTION(mState == STATE_TRANSFER, "unexpected state");
-            NS_ASSERTION(NS_SUCCEEDED(mStatus), "unexpected status");
+        bool stillTransferring = (mState == STATE_TRANSFER &&
+                                  nextState == STATE_TRANSFER);
+        if (stillTransferring) {
+            NS_ASSERTION(NS_SUCCEEDED(mStatus),
+                         "Should not have failed status for ongoing transfer");
+        } else {
+            NS_ASSERTION(mState != nextState,
+                         "Only OnStateTransfer can be called more than once.");
+        }
+        if (mRetargeting) {
+            NS_ASSERTION(mState != STATE_STOP,
+                         "Retargeting should not happen during OnStateStop.");
+        }
 
+        // Set mRetargeting so EnsureWaiting will be called. It ensures that
+        // OnStateStop is called on the main thread. 
+        if (nextState == STATE_STOP && !NS_IsMainThread()) {
+            mRetargeting = true;
+        }
+
+        // Wait asynchronously if there is still data to transfer, or we're
+        // switching event delivery to another thread.
+        if (!mSuspendCount && (stillTransferring || mRetargeting)) {
+            mState = nextState;
             mWaiting = false;
             mStatus = EnsureWaiting();
             if (NS_SUCCEEDED(mStatus))
                 break;
             
+            // Failure to start asynchronous wait: stop transfer.
             nextState = STATE_STOP;
         }
 
         mState = nextState;
     }
     return NS_OK;
 }
 
@@ -528,36 +569,103 @@ nsInputStreamPump::OnStateTransfer()
                 return STATE_TRANSFER;
             if (rv != NS_BASE_STREAM_CLOSED)
                 mStatus = rv;
         }
     }
     return STATE_STOP;
 }
 
+nsresult
+nsInputStreamPump::OnStateStopForFailure()
+{
+    MOZ_ASSERT(NS_FAILED(mStatus), "OnStateStopForFailure should be called "
+                                   "in a failed state");
+    MOZ_ASSERT(NS_IsMainThread(), "OnStateStopForFailure should be on the "
+                                  "main thread");
+
+    mState = OnStateStop();
+    return NS_OK;
+}
+
 uint32_t
 nsInputStreamPump::OnStateStop()
 {
+    if (NS_FAILED(mStatus)) {
+        // If EnsureWaiting has failed, it's possible that we could be off main
+        // thread. We may have to dispatch OnStateStop to the main thread
+        // directly. Note: this would result in OnStateStop being called
+        // outside the context of OnInputStreamReady.
+        if (!NS_IsMainThread()) {
+            nsresult rv = NS_DispatchToMainThread(
+                NS_NewRunnableMethod(this, &nsInputStreamPump::OnStateStopForFailure));
+            NS_ENSURE_SUCCESS(rv, STATE_IDLE);
+            return STATE_IDLE;
+        }
+    } else {
+        MOZ_ASSERT(NS_IsMainThread(), "In a success state, OnStateStop should "
+                                      "be on the main thread");
+    }
+
     PROFILER_LABEL("Input", "nsInputStreamPump::OnStateTransfer");
     LOG(("  OnStateStop [this=%p status=%x]\n", this, mStatus));
 
     // if an error occurred, we must be sure to pass the error onto the async
     // stream.  in some cases, this is redundant, but since close is idempotent,
     // this is OK.  otherwise, be sure to honor the "close-when-done" option.
 
     if (NS_FAILED(mStatus))
         mAsyncStream->CloseWithStatus(mStatus);
     else if (mCloseWhenDone)
         mAsyncStream->Close();
 
     mAsyncStream = 0;
     mTargetThread = 0;
     mIsPending = false;
-
     mListener->OnStopRequest(this, mListenerContext, mStatus);
     mListener = 0;
     mListenerContext = 0;
 
     if (mLoadGroup)
         mLoadGroup->RemoveRequest(this, nullptr, mStatus);
 
     return STATE_IDLE;
 }
+
+//-----------------------------------------------------------------------------
+// nsIThreadRetargetableRequest
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsInputStreamPump::RetargetDeliveryTo(nsIEventTarget* aNewTarget)
+{
+    NS_ENSURE_ARG(aNewTarget);
+    NS_ENSURE_TRUE(mState == STATE_START || mState == STATE_TRANSFER,
+                   NS_ERROR_UNEXPECTED);
+
+    // If canceled, do not retarget. Return with canceled status.
+    if (NS_FAILED(mStatus)) {
+        return mStatus;
+    }
+
+    if (aNewTarget == mTargetThread) {
+        NS_WARNING("Retargeting delivery to same thread");
+        return NS_OK;
+    }
+
+    // Ensure that |mListener| and any subsequent listeners can be retargeted
+    // to another thread.
+    nsresult rv = NS_OK;
+    nsCOMPtr<nsIThreadRetargetableStreamListener> retargetableListener =
+        do_QueryInterface(mListener, &rv);
+    if (NS_SUCCEEDED(rv) && retargetableListener) {
+        rv = retargetableListener->CheckListenerChain();
+        if (NS_SUCCEEDED(rv)) {
+            mTargetThread = aNewTarget;
+            mRetargeting = true;
+        }
+    }
+    LOG(("nsInputStreamPump::RetargetDeliveryTo [this=%x aNewTarget=%p] "
+         "%s listener [%p] rv[%x]",
+         this, aNewTarget, (mTargetThread == aNewTarget ? "success" : "failure"),
+         (nsIStreamListener*)mListener, rv));
+    return rv;
+}
--- a/netwerk/base/src/nsInputStreamPump.h
+++ b/netwerk/base/src/nsInputStreamPump.h
@@ -10,27 +10,30 @@
 #include "nsIInputStream.h"
 #include "nsIURI.h"
 #include "nsILoadGroup.h"
 #include "nsIStreamListener.h"
 #include "nsIInterfaceRequestor.h"
 #include "nsIProgressEventSink.h"
 #include "nsIAsyncInputStream.h"
 #include "nsIThread.h"
+#include "nsIThreadRetargetableRequest.h"
 #include "nsCOMPtr.h"
 #include "mozilla/Attributes.h"
 
 class nsInputStreamPump MOZ_FINAL : public nsIInputStreamPump
                                   , public nsIInputStreamCallback
+                                  , public nsIThreadRetargetableRequest
 {
 public:
     NS_DECL_ISUPPORTS
     NS_DECL_NSIREQUEST
     NS_DECL_NSIINPUTSTREAMPUMP
     NS_DECL_NSIINPUTSTREAMCALLBACK
+    NS_DECL_NSITHREADRETARGETABLEREQUEST
 
     nsInputStreamPump(); 
     ~nsInputStreamPump();
 
     static NS_HIDDEN_(nsresult)
                       Create(nsInputStreamPump  **result,
                              nsIInputStream      *stream,
                              int64_t              streamPos = -1,
@@ -48,16 +51,30 @@ public:
      *
      * The data from the stream will not be consumed, i.e. the pump's listener
      * can still read all the data.
      *
      * Do not call before asyncRead. Do not call after onStopRequest.
      */
     NS_HIDDEN_(nsresult) PeekStream(PeekSegmentFun callback, void *closure);
 
+    /**
+     * Called on the main thread to clean up member variables. Called directly
+     * from OnStateStop if on the main thread, or dispatching to the main
+     * thread if not. Must be called on the main thread to serialize member
+     * variable deletion with calls to Cancel.
+     */
+    void OnStateStopCleanup();
+
+    /**
+     * Called on the main thread if EnsureWaiting fails, so that we always call
+     * OnStopRequest on main thread.
+     */
+    nsresult OnStateStopForFailure();
+
 protected:
 
     enum {
         STATE_IDLE,
         STATE_START,
         STATE_TRANSFER,
         STATE_STOP
     };
@@ -66,24 +83,25 @@ protected:
     uint32_t OnStateStart();
     uint32_t OnStateTransfer();
     uint32_t OnStateStop();
 
     uint32_t                      mState;
     nsCOMPtr<nsILoadGroup>        mLoadGroup;
     nsCOMPtr<nsIStreamListener>   mListener;
     nsCOMPtr<nsISupports>         mListenerContext;
-    nsCOMPtr<nsIThread>           mTargetThread;
+    nsCOMPtr<nsIEventTarget>      mTargetThread;
     nsCOMPtr<nsIInputStream>      mStream;
     nsCOMPtr<nsIAsyncInputStream> mAsyncStream;
     uint64_t                      mStreamOffset;
     uint64_t                      mStreamLength;
     uint32_t                      mSegSize;
     uint32_t                      mSegCount;
     nsresult                      mStatus;
     uint32_t                      mSuspendCount;
     uint32_t                      mLoadFlags;
     bool                          mIsPending;
     bool                          mWaiting; // true if waiting on async source
     bool                          mCloseWhenDone;
+    bool                          mRetargeting;
 };
 
 #endif // !nsInputStreamChannel_h__
--- a/netwerk/base/src/nsStreamListenerTee.cpp
+++ b/netwerk/base/src/nsStreamListenerTee.cpp
@@ -1,19 +1,20 @@
 /* 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 "nsStreamListenerTee.h"
 #include "nsProxyRelease.h"
 
-NS_IMPL_ISUPPORTS3(nsStreamListenerTee,
-                   nsIStreamListener,
-                   nsIRequestObserver,
-                   nsIStreamListenerTee)
+NS_IMPL_THREADSAFE_ISUPPORTS4(nsStreamListenerTee,
+                              nsIStreamListener,
+                              nsIRequestObserver,
+                              nsIStreamListenerTee,
+                              nsIThreadRetargetableStreamListener)
 
 NS_IMETHODIMP
 nsStreamListenerTee::OnStartRequest(nsIRequest *request,
                                     nsISupports *context)
 {
     NS_ENSURE_TRUE(mListener, NS_ERROR_NOT_INITIALIZED);
     nsresult rv1 = mListener->OnStartRequest(request, context);
     nsresult rv2 = NS_OK;
@@ -88,16 +89,29 @@ nsStreamListenerTee::OnDataAvailable(nsI
         tee = do_QueryInterface(mInputTee, &rv);
         if (NS_FAILED(rv)) return rv;
     }
 
     return mListener->OnDataAvailable(request, context, tee, offset, count);
 }
 
 NS_IMETHODIMP
+nsStreamListenerTee::CheckListenerChain()
+{
+    NS_ASSERTION(NS_IsMainThread(), "Should be on main thread!");
+    nsresult rv = NS_OK;
+    nsCOMPtr<nsIThreadRetargetableStreamListener> retargetableListener =
+        do_QueryInterface(mListener, &rv);
+    if (retargetableListener) {
+        rv = retargetableListener->CheckListenerChain();
+    }
+    return rv;
+}
+
+NS_IMETHODIMP
 nsStreamListenerTee::Init(nsIStreamListener *listener,
                           nsIOutputStream *sink,
                           nsIRequestObserver *requestObserver)
 {
     NS_ENSURE_ARG_POINTER(listener);
     NS_ENSURE_ARG_POINTER(sink);
     mListener = listener;
     mSink = sink;
--- a/netwerk/base/src/nsStreamListenerTee.h
+++ b/netwerk/base/src/nsStreamListenerTee.h
@@ -1,27 +1,30 @@
 /* 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 nsStreamListenerTee_h__
 #define nsStreamListenerTee_h__
 
 #include "nsIStreamListenerTee.h"
+#include "nsIThreadRetargetableStreamListener.h"
 #include "nsIInputStreamTee.h"
 #include "nsIOutputStream.h"
 #include "nsCOMPtr.h"
 #include "nsIEventTarget.h"
 
 class nsStreamListenerTee : public nsIStreamListenerTee
+                          , public nsIThreadRetargetableStreamListener
 {
 public:
     NS_DECL_ISUPPORTS
     NS_DECL_NSIREQUESTOBSERVER
     NS_DECL_NSISTREAMLISTENER
+    NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
     NS_DECL_NSISTREAMLISTENERTEE
 
     nsStreamListenerTee() { }
     virtual ~nsStreamListenerTee() { }
 
 private:
     nsCOMPtr<nsIInputStreamTee>  mInputTee;
     nsCOMPtr<nsIOutputStream>    mSink;
--- a/netwerk/base/src/nsStreamListenerWrapper.cpp
+++ b/netwerk/base/src/nsStreamListenerWrapper.cpp
@@ -1,10 +1,24 @@
 /* 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 "nsStreamListenerWrapper.h"
+#include "nsThreadUtils.h"
 
-NS_IMPL_ISUPPORTS2(nsStreamListenerWrapper,
+NS_IMPL_ISUPPORTS3(nsStreamListenerWrapper,
                    nsIStreamListener,
-                   nsIRequestObserver)
+                   nsIRequestObserver,
+                   nsIThreadRetargetableStreamListener)
 
+NS_IMETHODIMP
+nsStreamListenerWrapper::CheckListenerChain()
+{
+    NS_ASSERTION(NS_IsMainThread(), "Should be on main thread!");
+    nsresult rv = NS_OK;
+    nsCOMPtr<nsIThreadRetargetableStreamListener> retargetableListener =
+        do_QueryInterface(mListener, &rv);
+    if (retargetableListener) {
+        rv = retargetableListener->CheckListenerChain();
+    }
+    return rv;
+}
--- a/netwerk/protocol/http/HttpBaseChannel.cpp
+++ b/netwerk/protocol/http/HttpBaseChannel.cpp
@@ -197,16 +197,18 @@ HttpBaseChannel::GetLoadGroup(nsILoadGro
   *aLoadGroup = mLoadGroup;
   NS_IF_ADDREF(*aLoadGroup);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 HttpBaseChannel::SetLoadGroup(nsILoadGroup *aLoadGroup)
 {
+  MOZ_ASSERT(NS_IsMainThread(), "Should only be called on the main thread.");
+  
   if (!CanSetLoadGroup(aLoadGroup)) {
     return NS_ERROR_FAILURE;
   }
 
   mLoadGroup = aLoadGroup;
   mProgressSink = nullptr;
   mPrivateBrowsing = NS_UsePrivateBrowsing(this);
   return NS_OK;
@@ -281,16 +283,18 @@ HttpBaseChannel::GetNotificationCallback
   *aCallbacks = mCallbacks;
   NS_IF_ADDREF(*aCallbacks);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 HttpBaseChannel::SetNotificationCallbacks(nsIInterfaceRequestor *aCallbacks)
 {
+  MOZ_ASSERT(NS_IsMainThread(), "Should only be called on the main thread.");
+  
   if (!CanSetCallbacks(aCallbacks)) {
     return NS_ERROR_FAILURE;
   }
 
   mCallbacks = aCallbacks;
   mProgressSink = nullptr;
 
   mPrivateBrowsing = NS_UsePrivateBrowsing(this);
@@ -1489,16 +1493,18 @@ HttpBaseChannel::SetNewListener(nsIStrea
 
 //-----------------------------------------------------------------------------
 // HttpBaseChannel helpers
 //-----------------------------------------------------------------------------
 
 void
 HttpBaseChannel::ReleaseListeners()
 {
+  MOZ_ASSERT(NS_IsMainThread(), "Should only be called on the main thread.");
+  
   mListener = nullptr;
   mListenerContext = nullptr;
   mCallbacks = nullptr;
   mProgressSink = nullptr;
 }
 
 void
 HttpBaseChannel::DoNotifyListener()
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -28,16 +28,17 @@
 #include "nsStreamUtils.h"
 #include "nsIOService.h"
 #include "nsICacheService.h"
 #include "nsDNSPrefetch.h"
 #include "nsChannelClassifier.h"
 #include "nsIRedirectResultListener.h"
 #include "mozilla/TimeStamp.h"
 #include "nsError.h"
+#include "nsPrintfCString.h"
 #include "nsAlgorithm.h"
 #include "GeckoProfiler.h"
 #include "nsIConsoleService.h"
 #include "base/compiler_specific.h"
 #include "NullHttpTransaction.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/VisualEventTracer.h"
 #include "nsISSLSocketControl.h"
@@ -4388,16 +4389,18 @@ NS_INTERFACE_MAP_BEGIN(nsHttpChannel)
     NS_INTERFACE_MAP_ENTRY(nsISupportsPriority)
     NS_INTERFACE_MAP_ENTRY(nsIProtocolProxyCallback)
     NS_INTERFACE_MAP_ENTRY(nsIProxiedChannel)
     NS_INTERFACE_MAP_ENTRY(nsIHttpAuthenticableChannel)
     NS_INTERFACE_MAP_ENTRY(nsIApplicationCacheContainer)
     NS_INTERFACE_MAP_ENTRY(nsIApplicationCacheChannel)
     NS_INTERFACE_MAP_ENTRY(nsIAsyncVerifyRedirectCallback)
     NS_INTERFACE_MAP_ENTRY(nsITimedChannel)
+    NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableRequest)
+    NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableStreamListener)
 NS_INTERFACE_MAP_END_INHERITING(HttpBaseChannel)
 
 //-----------------------------------------------------------------------------
 // nsHttpChannel::nsIRequest
 //-----------------------------------------------------------------------------
 
 NS_IMETHODIMP
 nsHttpChannel::Cancel(nsresult status)
@@ -5223,16 +5226,47 @@ nsHttpChannel::OnStopRequest(nsIRequest 
 
     return NS_OK;
 }
 
 //-----------------------------------------------------------------------------
 // nsHttpChannel::nsIStreamListener
 //-----------------------------------------------------------------------------
 
+class OnTransportStatusAsyncEvent : public nsRunnable
+{
+public:
+    OnTransportStatusAsyncEvent(nsITransportEventSink* aEventSink,
+                                nsresult aTransportStatus,
+                                uint64_t aProgress,
+                                uint64_t aProgressMax)
+    : mEventSink(aEventSink)
+    , mTransportStatus(aTransportStatus)
+    , mProgress(aProgress)
+    , mProgressMax(aProgressMax)
+    {
+        MOZ_ASSERT(!NS_IsMainThread(), "Shouldn't be created on main thread");
+    }
+
+    NS_IMETHOD Run()
+    {
+        MOZ_ASSERT(NS_IsMainThread(), "Should run on main thread");
+        if (mEventSink) {
+            mEventSink->OnTransportStatus(nullptr, mTransportStatus,
+                                          mProgress, mProgressMax);
+        }
+        return NS_OK;
+    }
+private:
+    nsCOMPtr<nsITransportEventSink> mEventSink;
+    nsresult mTransportStatus;
+    uint64_t mProgress;
+    uint64_t mProgressMax;
+};
+
 NS_IMETHODIMP
 nsHttpChannel::OnDataAvailable(nsIRequest *request, nsISupports *ctxt,
                                nsIInputStream *input,
                                uint64_t offset, uint32_t count)
 {
     PROFILER_LABEL("network", "nsHttpChannel::OnDataAvailable");
     LOG(("nsHttpChannel::OnDataAvailable [this=%p request=%p offset=%llu count=%u]\n",
         this, request, offset, count));
@@ -5267,17 +5301,24 @@ nsHttpChannel::OnDataAvailable(nsIReques
         // holds our best estimate of the total content length.  Even in the case
         // of a byte range request, the content length stored in the cached
         // response headers is what we want to use here.
 
         uint64_t progressMax(uint64_t(mResponseHead->ContentLength()));
         uint64_t progress = mLogicalOffset + uint64_t(count);
         MOZ_ASSERT(progress <= progressMax, "unexpected progress values");
 
-        OnTransportStatus(nullptr, transportStatus, progress, progressMax);
+        if (NS_IsMainThread()) {
+            OnTransportStatus(nullptr, transportStatus, progress, progressMax);
+        } else {
+            nsresult rv = NS_DispatchToMainThread(
+                new OnTransportStatusAsyncEvent(this, transportStatus,
+                                                progress, progressMax));
+            NS_ENSURE_SUCCESS(rv, rv);
+        }
 
         //
         // we have to manually keep the logical offset of the stream up-to-date.
         // we cannot depend solely on the offset provided, since we may have
         // already streamed some data from another source (see, for example,
         // OnDoneReadingPartialCacheEntry).
         //
         if (!mLogicalOffset)
@@ -5292,23 +5333,85 @@ nsHttpChannel::OnDataAvailable(nsIReques
             mLogicalOffset = progress;
         return rv;
     }
 
     return NS_ERROR_ABORT;
 }
 
 //-----------------------------------------------------------------------------
+// nsHttpChannel::nsIThreadRetargetableRequest
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsHttpChannel::RetargetDeliveryTo(nsIEventTarget* aNewTarget)
+{
+    MOZ_ASSERT(NS_IsMainThread(), "Should be called on main thread only");
+
+    NS_ENSURE_ARG(aNewTarget);
+    if (aNewTarget == NS_GetCurrentThread()) {
+        NS_WARNING("Retargeting delivery to same thread");
+        return NS_OK;
+    }
+    NS_ENSURE_TRUE(mTransactionPump || mCachePump, NS_ERROR_NOT_AVAILABLE);
+
+    nsresult rv = NS_OK;
+    // If both cache pump and transaction pump exist, we're probably dealing
+    // with partially cached content. So, we must be able to retarget both.
+    nsCOMPtr<nsIThreadRetargetableRequest> retargetableCachePump;
+    nsCOMPtr<nsIThreadRetargetableRequest> retargetableTransactionPump;
+    if (mCachePump) {
+        retargetableCachePump = do_QueryObject(mCachePump);
+        // nsInputStreamPump should implement this interface.
+        MOZ_ASSERT(retargetableCachePump);
+        rv = retargetableCachePump->RetargetDeliveryTo(aNewTarget);
+    }
+    if (NS_SUCCEEDED(rv) && mTransactionPump) {
+        retargetableTransactionPump = do_QueryObject(mTransactionPump);
+        // nsInputStreamPump should implement this interface.
+        MOZ_ASSERT(retargetableTransactionPump);
+        rv = retargetableTransactionPump->RetargetDeliveryTo(aNewTarget);
+
+        // If retarget fails for transaction pump, we must restore mCachePump.
+        if (NS_FAILED(rv) && retargetableCachePump) {
+            nsCOMPtr<nsIThread> mainThread;
+            rv = NS_GetMainThread(getter_AddRefs(mainThread));
+            NS_ENSURE_SUCCESS(rv, rv);
+            rv = retargetableCachePump->RetargetDeliveryTo(mainThread);
+        }
+    }
+    return rv;
+}
+
+//-----------------------------------------------------------------------------
+// nsHttpChannel::nsThreadRetargetableStreamListener
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsHttpChannel::CheckListenerChain()
+{
+    NS_ASSERTION(NS_IsMainThread(), "Should be on main thread!");
+    nsresult rv = NS_OK;
+    nsCOMPtr<nsIThreadRetargetableStreamListener> retargetableListener =
+        do_QueryInterface(mListener, &rv);
+    if (retargetableListener) {
+        rv = retargetableListener->CheckListenerChain();
+    }
+    return rv;
+}
+
+//-----------------------------------------------------------------------------
 // nsHttpChannel::nsITransportEventSink
 //-----------------------------------------------------------------------------
 
 NS_IMETHODIMP
 nsHttpChannel::OnTransportStatus(nsITransport *trans, nsresult status,
                                  uint64_t progress, uint64_t progressMax)
 {
+    MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread only");
     // cache the progress sink so we don't have to query for it each time.
     if (!mProgressSink)
         GetCallback(mProgressSink);
 
     if (status == NS_NET_STATUS_CONNECTED_TO ||
         status == NS_NET_STATUS_WAITING_FOR) {
         nsCOMPtr<nsISocketTransport> socketTransport =
             do_QueryInterface(trans);
@@ -5325,17 +5428,23 @@ nsHttpChannel::OnTransportStatus(nsITran
 
         nsAutoCString host;
         mURI->GetHost(host);
         mProgressSink->OnStatus(this, nullptr, status,
                                 NS_ConvertUTF8toUTF16(host).get());
 
         if (progress > 0) {
             MOZ_ASSERT(progress <= progressMax, "unexpected progress values");
-            mProgressSink->OnProgress(this, nullptr, progress, progressMax);
+            // Try to get mProgressSink if it was nulled out during OnStatus.
+            if (!mProgressSink) {
+                GetCallback(mProgressSink);
+            }
+            if (mProgressSink) {
+                mProgressSink->OnProgress(this, nullptr, progress, progressMax);
+            }
         }
     }
 #ifdef DEBUG
     else
         LOG(("skipping status notification [this=%p sink=%p pending=%u background=%x]\n",
             this, mProgressSink.get(), mIsPending, (mLoadFlags & LOAD_BACKGROUND)));
 #endif
 
--- a/netwerk/protocol/http/nsHttpChannel.h
+++ b/netwerk/protocol/http/nsHttpChannel.h
@@ -23,16 +23,18 @@
 #include "nsIResumableChannel.h"
 #include "nsIProtocolProxyCallback.h"
 #include "nsICancelable.h"
 #include "nsIHttpAuthenticableChannel.h"
 #include "nsIHttpChannelAuthProvider.h"
 #include "nsIAsyncVerifyRedirectCallback.h"
 #include "nsITimedChannel.h"
 #include "nsIFile.h"
+#include "nsIThreadRetargetableRequest.h"
+#include "nsIThreadRetargetableStreamListener.h"
 #include "nsDNSPrefetch.h"
 #include "TimingStruct.h"
 #include "AutoClose.h"
 #include "mozilla/Telemetry.h"
 
 class nsAHttpConnection;
 class nsIPrincipal;
 
@@ -50,31 +52,35 @@ class nsHttpChannel : public HttpBaseCha
                     , public nsICachingChannel
                     , public nsICacheListener
                     , public nsITransportEventSink
                     , public nsIProtocolProxyCallback
                     , public nsIHttpAuthenticableChannel
                     , public nsIApplicationCacheChannel
                     , public nsIAsyncVerifyRedirectCallback
                     , public nsITimedChannel
+                    , public nsIThreadRetargetableRequest
+                    , public nsIThreadRetargetableStreamListener
 {
 public:
     NS_DECL_ISUPPORTS_INHERITED
     NS_DECL_NSIREQUESTOBSERVER
     NS_DECL_NSISTREAMLISTENER
+    NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
     NS_DECL_NSICACHEINFOCHANNEL
     NS_DECL_NSICACHINGCHANNEL
     NS_DECL_NSICACHELISTENER
     NS_DECL_NSITRANSPORTEVENTSINK
     NS_DECL_NSIPROTOCOLPROXYCALLBACK
     NS_DECL_NSIPROXIEDCHANNEL
     NS_DECL_NSIAPPLICATIONCACHECONTAINER
     NS_DECL_NSIAPPLICATIONCACHECHANNEL
     NS_DECL_NSIASYNCVERIFYREDIRECTCALLBACK
     NS_DECL_NSITIMEDCHANNEL
+    NS_DECL_NSITHREADRETARGETABLEREQUEST
 
     // nsIHttpAuthenticableChannel. We can't use
     // NS_DECL_NSIHTTPAUTHENTICABLECHANNEL because it duplicates cancel() and
     // others.
     NS_IMETHOD GetIsSSL(bool *aIsSSL);
     NS_IMETHOD GetProxyMethodIsConnect(bool *aProxyMethodIsConnect);
     NS_IMETHOD GetServerResponseHeader(nsACString & aServerResponseHeader);
     NS_IMETHOD GetProxyChallenges(nsACString & aChallenges);
new file mode 100644
--- /dev/null
+++ b/netwerk/test/mochitests/Makefile.in
@@ -0,0 +1,21 @@
+# -*- Mode: Makefile; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+# vim: set ts=8 sts=2 et sw=2 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/.
+
+DEPTH		= @DEPTH@
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+relativesrcdir	= @relativesrcdir@
+FAIL_ON_WARNINGS := 1
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_FILES = \
+  partial_content.sjs \
+  test_partially_cached_content.html \
+  $(NULL)
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/netwerk/test/mochitests/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+MODULE = 'test_necko'
new file mode 100644
--- /dev/null
+++ b/netwerk/test/mochitests/partial_content.sjs
@@ -0,0 +1,151 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 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/. */
+
+/* Debug and Error wrapper functions for dump().
+ */
+function ERR(response, responseCode, responseCodeStr, msg)
+{
+  // Reset state var.
+  setState("expectedRequestType", "");
+  // Dump to console log and send to client in response.
+  dump("SERVER ERROR: " + msg + "\n");
+  response.write("HTTP/1.1" + responseCode + responseCodeStr + "\r\n");
+  response.write("Content-Type: text/html; charset=UTF-8\r\n");
+  response.write("Content-Length: " + msg.length + "\r\n");
+  response.write("\r\n");
+  response.write(msg);
+}
+
+function DBG(msg)
+{
+  // Dump to console only.
+  dump("SERVER DEBUG: " + msg + "\n");
+}
+
+/* Delivers content in parts to test partially cached content: requires two
+ * requests for the same resource.
+ *
+ * First call will respond with partial content, but a 200 header and
+ * Content-Length equal to the full content length. No Range or If-Range
+ * headers are allowed in the request.
+ *
+ * Second call will require Range and If-Range in the request headers, and
+ * will respond with the range requested.
+ */
+function handleRequest(request, response)
+{
+  DBG("Trying to seize power");
+  response.seizePower();
+
+  DBG("About to check state vars");
+  // Get state var to determine if this is the first or second request.
+  var expectedRequestType;
+  var lastModified;
+  if (getState("expectedRequestType") === "") {
+    DBG("First call: Should be requesting full content.");
+    expectedRequestType = "fullRequest";
+    // Set state var for second request.
+    setState("expectedRequestType", "partialRequest");
+    // Create lastModified variable for responses.
+    lastModified = (new Date()).toUTCString();
+    setState("lastModified", lastModified);
+  } else if (getState("expectedRequestType") === "partialRequest") {
+    DBG("Second call: Should be requesting undelivered content.");
+    expectedRequestType = "partialRequest";
+    // Reset state var for first request.
+    setState("expectedRequestType", "");
+    // Get last modified date and reset state var.
+    lastModified = getState("lastModified");
+  } else {
+    ERR(response, 500, "Internal Server Error",
+        "Invalid expectedRequestType \"" + expectedRequestType + "\"in " +
+        "server state db.");
+    return;
+  }
+
+  // Look for Range and If-Range
+  var range = request.hasHeader("Range") ? request.getHeader("Range") : "";
+  var ifRange = request.hasHeader("If-Range") ? request.getHeader("If-Range") : "";
+
+  if (expectedRequestType === "fullRequest") {
+    // Should not have Range or If-Range in first request.
+    if (range && range.length > 0) {
+      ERR(response, 400, "Bad Request",
+          "Should not receive \"Range: " + range + "\" for first, full request.");
+      return;
+    }
+    if (ifRange && ifRange.length > 0) {
+      ERR(response, 400, "Bad Request",
+          "Should not receive \"Range: " + range + "\" for first, full request.");
+      return;
+    }
+  } else if (expectedRequestType === "partialRequest") {
+    // Range AND If-Range should both be present in second request.
+    if (!range) {
+      ERR(response, 400, "Bad Request",
+          "Should receive \"Range: \" for second, partial request.");
+      return;
+    }
+    if (!ifRange) {
+      ERR(response, 400, "Bad Request",
+          "Should receive \"If-Range: \" for second, partial request.");
+      return;
+    }
+  } else {
+    // Somewhat redundant, but a check for errors in this test code.
+    ERR(response, 500, "Internal Server Error",
+        "expectedRequestType not set correctly: \"" + expectedRequestType + "\"");
+    return;
+  }
+
+  // Prepare content in two parts for responses.
+  var partialContent = "<html><head></head><body><p id=\"firstResponse\">" +
+                       "First response</p>";
+  var remainderContent = "<p id=\"secondResponse\">Second response</p>" +
+                         "</body></html>";
+  var totalLength = partialContent.length + remainderContent.length;
+
+  DBG("totalLength: " + totalLength);
+
+  // Prepare common headers for the two responses.
+  date = new Date();
+  DBG("Date: " + date.toUTCString() + ", Last-Modified: " + lastModified);
+  var commonHeaders = "Date: " + date.toUTCString() + "\r\n" +
+                      "Last-Modified: " + lastModified + "\r\n" +
+                      "Content-Type: text/html; charset=UTF-8\r\n" +
+                      "ETag: abcd0123\r\n" +
+                      "Accept-Ranges: bytes\r\n";
+
+
+  // Prepare specific headers and content for first and second responses.
+  if (expectedRequestType === "fullRequest") {
+    DBG("First response: Sending partial content with a full header");
+    response.write("HTTP/1.1 200 OK\r\n");
+    response.write(commonHeaders);
+    // Set Content-Length to full length of resource.
+    response.write("Content-Length: " + totalLength + "\r\n");
+    response.write("\r\n");
+    response.write(partialContent);
+  } else if (expectedRequestType === "partialRequest") {
+    DBG("Second response: Sending remaining content with a range header");
+    response.write("HTTP/1.1 206 Partial Content\r\n");
+    response.write(commonHeaders);
+    // Set Content-Length to length of bytes transmitted.
+    response.write("Content-Length: " + remainderContent.length + "\r\n");
+    response.write("Content-Range: bytes " + partialContent.length + "-" +
+                   (totalLength - 1) + "/" + totalLength + "\r\n");
+    response.write("\r\n");
+    response.write(remainderContent);
+  } else {
+    // Somewhat redundant, but a check for errors in this test code.
+    ERR(response, 500, "Internal Server Error",
+       "Something very bad happened here: expectedRequestType is invalid " +
+       "towards the end of handleRequest! - \"" + expectedRequestType + "\"");
+    return;
+  }
+
+  response.finish();
+}
new file mode 100644
--- /dev/null
+++ b/netwerk/test/mochitests/test_partially_cached_content.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+  https://bugzilla.mozilla.org/show_bug.cgi?id=497003
+
+  This test verifies that partially cached content is read from the cache first
+  and then from the network. It is written in the mochitest framework to take
+  thread retargeting into consideration of nsIStreamListener callbacks (inc.
+  nsIRequestObserver). E.g. HTML5 Stream Parser requesting retargeting of
+  nsIStreamListener callbacks to the parser thread.
+-->
+<head>
+  <meta charset="UTF-8">
+  <title>Test for Bug 497003: support sending OnDataAvailable() to other threads</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <p><a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=497003">Mozilla Bug 497003: support sending OnDataAvailable() to other threads</a></p>
+  <p><iframe id="contentFrame" src="partial_content.sjs"></iframe></p>
+
+<pre id="test">
+<script>
+
+
+
+/* Check that the iframe has initial content only after the first load.
+ */
+function expectInitialContent(e) {
+  info("expectInitialContent",
+       "First response received: should have partial content");
+  var frameWindow = document.getElementById('contentFrame').contentWindow;
+
+  // Expect "First response" in received HTML.
+  var firstResponse = frameWindow.document.getElementById('firstResponse');
+  ok(firstResponse, "First response should exist");
+  if (firstResponse) {
+    is(firstResponse.innerHTML, "First response",
+       "First response should be correct");
+  }
+
+  // Expect NOT to get any second response element.
+  var secondResponse = frameWindow.document.getElementById('secondResponse');
+  ok(!secondResponse, "Should not get text for second response in first.");
+
+  // Set up listener for second load.
+  e.target.removeEventListener("load", expectInitialContent, false);
+  e.target.addEventListener("load", expectFullContent, false);
+
+  // Reload.
+  e.target.src="partial_content.sjs";
+}
+
+/* Check that the iframe has all the content after the second load.
+ */
+function expectFullContent(e)
+{
+  info("expectFullContent",
+       "Second response received: should complete content from first load");
+  var frameWindow = document.getElementById('contentFrame').contentWindow;
+
+  // Expect "First response" to still be there
+  var firstResponse = frameWindow.document.getElementById('firstResponse');
+  ok(firstResponse, "First response should exist");
+  if (firstResponse) {
+    is(firstResponse.innerHTML, "First response",
+       "First response should be correct");
+  }
+
+  // Expect "Second response" to be there also.
+  var secondResponse = frameWindow.document.getElementById('secondResponse');
+  ok(secondResponse, "Second response should exist");
+  if (secondResponse) {
+    is(secondResponse.innerHTML, "Second response",
+       "Second response should be correct");
+  }
+
+  SimpleTest.finish();
+}
+
+// Set listener for first load to expect partial content.
+document.getElementById('contentFrame')
+  .addEventListener("load", expectInitialContent, false);
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
--- a/netwerk/test/moz.build
+++ b/netwerk/test/moz.build
@@ -1,15 +1,15 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
-TEST_DIRS += ['httpserver', 'browser']
+TEST_DIRS += ['httpserver', 'browser', 'mochitests']
 
 MODULE = 'test_necko'
 
 XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
 
 # FIXME/bug 575918: out-of-process xpcshell is broken on OS X
 if CONFIG['OS_ARCH'] != 'Darwin':
     XPCSHELL_TESTS_MANIFESTS += ['unit_ipc/xpcshell.ini']