Bug 1261585 - Make nsITraceableChannel listener work with content encoding. r=mayhemer
☠☠ backed out by f930d9fbb403 ☠ ☠
authorDragana Damjanovic <dd.mozilla@gmail.com>
Tue, 17 Jan 2017 08:27:00 -0500
changeset 374704 f6d5e6243d01ad740d5ec00659b5c9e5820a2f50
parent 374703 c84ced896f8340813d7321536cc4de86052f1c89
child 374705 3b5321a426ad97d0ac43bdc4f3a05d10fb5b58e4
push id6996
push userjlorenzo@mozilla.com
push dateMon, 06 Mar 2017 20:48:21 +0000
treeherdermozilla-beta@d89512dab048 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmayhemer
bugs1261585
milestone53.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 1261585 - Make nsITraceableChannel listener work with content encoding. r=mayhemer
netwerk/protocol/http/HttpBaseChannel.cpp
netwerk/protocol/http/HttpBaseChannel.h
netwerk/protocol/http/HttpChannelChild.cpp
netwerk/protocol/http/HttpChannelChild.h
netwerk/protocol/http/HttpChannelParent.cpp
netwerk/protocol/http/HttpChannelParent.h
netwerk/protocol/http/PHttpChannel.ipdl
netwerk/protocol/http/nsHttpChannel.cpp
netwerk/protocol/http/nsHttpChannel.h
netwerk/test/unit_ipc/child_tracable_listener.js
netwerk/test/unit_ipc/test_traceable_channel_decoded_data.js
netwerk/test/unit_ipc/test_traceable_channel_modify_response.js
netwerk/test/unit_ipc/xpcshell.ini
--- a/netwerk/protocol/http/HttpBaseChannel.cpp
+++ b/netwerk/protocol/http/HttpBaseChannel.cpp
@@ -91,16 +91,17 @@ HttpBaseChannel::HttpBaseChannel()
   , mAllowAltSvc(true)
   , mBeConservative(false)
   , mResponseTimeoutEnabled(true)
   , mAllRedirectsSameOrigin(true)
   , mAllRedirectsPassTimingAllowCheck(true)
   , mResponseCouldBeSynthesized(false)
   , mBlockAuthPrompt(false)
   , mAllowStaleCacheContent(false)
+  , mHasListenerForTraceableChannel(false)
   , mSuspendCount(0)
   , mInitialRwin(0)
   , mProxyResolveFlags(0)
   , mProxyURI(nullptr)
   , mContentDispositionHint(UINT32_MAX)
   , mHttpHandler(gHttpHandler)
   , mReferrerPolicy(NS_GetDefaultReferrerPolicy())
   , mRedirectCount(0)
@@ -2771,16 +2772,17 @@ HttpBaseChannel::SetNewListener(nsIStrea
 
   NS_ENSURE_STATE(mListener);
   NS_ENSURE_ARG_POINTER(aListener);
 
   nsCOMPtr<nsIStreamListener> wrapper = new nsStreamListenerWrapper(mListener);
 
   wrapper.forget(_retval);
   mListener = aListener;
+  mHasListenerForTraceableChannel = true;
   return NS_OK;
 }
 
 //-----------------------------------------------------------------------------
 // HttpBaseChannel helpers
 //-----------------------------------------------------------------------------
 
 void
--- a/netwerk/protocol/http/HttpBaseChannel.h
+++ b/netwerk/protocol/http/HttpBaseChannel.h
@@ -336,16 +336,18 @@ public: /* Necko internal use only... */
     // mListenerContext.
     nsresult DoApplyContentConversions(nsIStreamListener *aNextListener,
                                        nsIStreamListener **aNewNextListener);
 
     // Callback on main thread when NS_AsyncCopy() is finished populating
     // the new mUploadStream.
     void EnsureUploadStreamIsCloneableComplete(nsresult aStatus);
 
+    bool HasListenerForTraceableChannel() { return mHasListenerForTraceableChannel; }
+
     void SetIsTrackingResource()
     {
       mIsTrackingResource = true;
     }
 
 protected:
   nsCOMArray<nsISecurityConsoleMessage> mSecurityConsoleMessages;
 
@@ -490,16 +492,18 @@ protected:
   uint32_t                          mResponseCouldBeSynthesized : 1;
 
   uint32_t                          mBlockAuthPrompt : 1;
 
   // If true, we behave as if the LOAD_FROM_CACHE flag has been set.
   // Used to enforce that flag's behavior but not expose it externally.
   uint32_t                          mAllowStaleCacheContent : 1;
 
+  uint32_t                          mHasListenerForTraceableChannel : 1;
+
   // Current suspension depth for this channel object
   uint32_t                          mSuspendCount;
 
   // Per channel transport window override (0 means no override)
   uint32_t                          mInitialRwin;
 
   nsCOMPtr<nsIURI>                  mAPIRedirectToURI;
   nsAutoPtr<nsTArray<nsCString> >   mRedirectedCachekeys;
--- a/netwerk/protocol/http/HttpChannelChild.cpp
+++ b/netwerk/protocol/http/HttpChannelChild.cpp
@@ -175,16 +175,17 @@ HttpChannelChild::HttpChannelChild()
   , mSuspendSent(false)
   , mSynthesizedResponse(false)
   , mShouldInterceptSubsequentRedirect(false)
   , mRedirectingForSubsequentSynthesizedResponse(false)
   , mPostRedirectChannelShouldIntercept(false)
   , mPostRedirectChannelShouldUpgrade(false)
   , mShouldParentIntercept(false)
   , mSuspendParentAfterSynthesizeResponse(false)
+  , mContentDecodingWillBeCalledOnParent(false)
 {
   LOG(("Creating HttpChannelChild @%x\n", this));
 
   mChannelCreationTime = PR_Now();
   mChannelCreationTimestamp = TimeStamp::Now();
   mAsyncOpenTime = TimeStamp::Now();
   mEventQ = new ChannelEventQueue(static_cast<nsIHttpChannel*>(this));
 }
@@ -377,29 +378,32 @@ HttpChannelChild::RecvOnStartRequest(con
                                      const bool& cacheEntryAvailable,
                                      const uint32_t& cacheExpirationTime,
                                      const nsCString& cachedCharset,
                                      const nsCString& securityInfoSerialization,
                                      const NetAddr& selfAddr,
                                      const NetAddr& peerAddr,
                                      const int16_t& redirectCount,
                                      const uint32_t& cacheKey,
-                                     const nsCString& altDataType)
+                                     const nsCString& altDataType,
+                                     const bool& contentDecodingWillBeCalledOnParent)
 {
   LOG(("HttpChannelChild::RecvOnStartRequest [this=%p]\n", this));
   // mFlushedForDiversion and mDivertingToParent should NEVER be set at this
   // stage, as they are set in the listener's OnStartRequest.
   MOZ_RELEASE_ASSERT(!mFlushedForDiversion,
     "mFlushedForDiversion should be unset before OnStartRequest!");
   MOZ_RELEASE_ASSERT(!mDivertingToParent,
     "mDivertingToParent should be unset before OnStartRequest!");
 
 
   mRedirectCount = redirectCount;
 
+  mContentDecodingWillBeCalledOnParent = contentDecodingWillBeCalledOnParent;
+
   mEventQ->RunOrEnqueue(new StartRequestEvent(this, channelStatus, responseHead,
                                               useResponseHead, requestHeaders,
                                               isFromCache, cacheEntryAvailable,
                                               cacheExpirationTime,
                                               cachedCharset,
                                               securityInfoSerialization,
                                               selfAddr, peerAddr, cacheKey,
                                               altDataType));
@@ -550,16 +554,36 @@ HttpChannelChild::DoOnStartRequest(nsIRe
     return;
   }
   nsresult rv = mListener->OnStartRequest(aRequest, aContext);
   if (NS_FAILED(rv)) {
     Cancel(rv);
     return;
   }
 
+  if (mContentDecodingWillBeCalledOnParent) {
+    // Whether we need to do a data conversion or not is decided during
+    // OnStartRequest call. nsURILoader and nsExternalHelperAppService are
+    // making this decision on the child.
+    // If the parent channel has a listener for the TracableChannel interface,
+    // we need to convert the data before giving it to the listent, e.g. we
+    // need to convert the data on the parent.
+    //
+    // After calling OnStartRequest check if we need to do a data conversion or
+    // not and inform the parent about the decision.
+    bool doContentConversion;
+    GetApplyConversion(&doContentConversion);
+    SetApplyConversion(false);
+    SendApplyConversion(doContentConversion);
+    mContentDecodingWillBeCalledOnParent = false;
+    LOG(("HttpChannelChild::DoOnStartRequest [this=%p] doing content decoding"
+         " on the parent; doContentEncoding=%d \n",
+         this, doContentConversion));
+  }
+
   if (mDivertingToParent) {
     mListener = nullptr;
     mListenerContext = nullptr;
     mCompressListener = nullptr;
     if (mLoadGroup) {
       mLoadGroup->RemoveRequest(this, nullptr, mStatus);
     }
 
@@ -1824,16 +1848,22 @@ HttpChannelChild::OnRedirectVerifyCallba
 //-----------------------------------------------------------------------------
 
 NS_IMETHODIMP
 HttpChannelChild::Cancel(nsresult status)
 {
   LOG(("HttpChannelChild::Cancel [this=%p]\n", this));
   MOZ_ASSERT(NS_IsMainThread());
 
+  if (mContentDecodingWillBeCalledOnParent) {
+    LOG(("HttpChannelChild::Cancel call SendApplyConversion [this=%p]\n", this));
+    SendApplyConversion(false);
+    mContentDecodingWillBeCalledOnParent = false;
+  }
+
   if (!mCanceled) {
     // If this cancel occurs before nsHttpChannel has been set up, AsyncOpen
     // is responsible for cleaning up.
     mCanceled = true;
     mStatus = status;
     if (RemoteChannelExists())
       SendCancel(status);
     if (mSynthesizedResponsePump) {
@@ -2761,16 +2791,22 @@ HttpChannelChild::DivertToParent(Channel
   HttpChannelDiverterArgs args;
   args.mChannelChild() = this;
   args.mApplyConversion() = mApplyConversion;
 
   PChannelDiverterChild* diverter =
     gNeckoChild->SendPChannelDiverterConstructor(args);
   MOZ_RELEASE_ASSERT(diverter);
 
+  // When a request is diverted to the parent, OnStartRequest of all listeners
+  // are called on the parent (and of course the end decision whether data
+  // should be converted or not is made on the parent), therefore just clear
+  // mContentDecodingWillBeCalledOnParent and do not send SendApplyConversion.
+  mContentDecodingWillBeCalledOnParent = false;
+
   *aChild = static_cast<ChannelDiverterChild*>(diverter);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 HttpChannelChild::UnknownDecoderInvolvedKeepData()
 {
--- a/netwerk/protocol/http/HttpChannelChild.h
+++ b/netwerk/protocol/http/HttpChannelChild.h
@@ -118,17 +118,18 @@ protected:
                                              const bool& cacheEntryAvailable,
                                              const uint32_t& cacheExpirationTime,
                                              const nsCString& cachedCharset,
                                              const nsCString& securityInfoSerialization,
                                              const NetAddr& selfAddr,
                                              const NetAddr& peerAddr,
                                              const int16_t& redirectCount,
                                              const uint32_t& cacheKey,
-                                             const nsCString& altDataType) override;
+                                             const nsCString& altDataType,
+                                             const bool& contentDecodingWillBeCalledOnParent) override;
   mozilla::ipc::IPCResult RecvOnTransportAndData(const nsresult& channelStatus,
                                                  const nsresult& status,
                                                  const uint64_t& progress,
                                                  const uint64_t& progressMax,
                                                  const uint64_t& offset,
                                                  const uint32_t& count,
                                                  const nsCString& data) override;
   mozilla::ipc::IPCResult RecvOnStopRequest(const nsresult& statusCode, const ResourceTimingStruct& timing) override;
@@ -270,16 +271,18 @@ private:
   // Set if the corresponding parent channel should force an interception to occur
   // before the network transaction is initiated.
   bool mShouldParentIntercept;
 
   // Set if the corresponding parent channel should suspend after a response
   // is synthesized.
   bool mSuspendParentAfterSynthesizeResponse;
 
+  bool mContentDecodingWillBeCalledOnParent;
+
   // Needed to call AsyncOpen in FinishInterceptedRedirect
   nsCOMPtr<nsIStreamListener> mInterceptedRedirectListener;
   nsCOMPtr<nsISupports> mInterceptedRedirectContext;
   // Needed to call CleanupRedirectingChannel in FinishInterceptedRedirect
   RefPtr<HttpChannelChild> mInterceptingChannel;
   // Used to call OverrideWithSynthesizedResponse in FinishInterceptedRedirect
   RefPtr<OverrideRunnable> mOverrideRunnable;
 
--- a/netwerk/protocol/http/HttpChannelParent.cpp
+++ b/netwerk/protocol/http/HttpChannelParent.cpp
@@ -62,16 +62,17 @@ HttpChannelParent::HttpChannelParent(con
   , mLoadContext(aLoadContext)
   , mStatus(NS_OK)
   , mPendingDiversion(false)
   , mDivertingFromChild(false)
   , mDivertedOnStartRequest(false)
   , mSuspendedForDiversion(false)
   , mSuspendAfterSynthesizeResponse(false)
   , mWillSynthesizeResponse(false)
+  , mWaitingForApplyConversionResponse(false)
   , mNestedFrameId(0)
 {
   LOG(("Creating HttpChannelParent [this=%p]\n", this));
 
   // Ensure gHttpHandler is initialized: we need the atom table up and running.
   nsCOMPtr<nsIHttpProtocolHandler> dummyInitializer =
     do_GetService(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "http");
 
@@ -811,16 +812,32 @@ HttpChannelParent::RecvMarkOfflineCacheE
   if (mOfflineForeignMarker) {
     mOfflineForeignMarker->MarkAsForeign();
     mOfflineForeignMarker = 0;
   }
 
   return IPC_OK();
 }
 
+mozilla::ipc::IPCResult
+HttpChannelParent::RecvApplyConversion(const bool& aApply)
+{
+  LOG(("HttpChannelParent::RecvApplyConversion [this=%p, aApply=%d, "
+       "mWaitingForApplyConversionResponse=%d]",
+       this, aApply, mWaitingForApplyConversionResponse));
+  MOZ_ASSERT(mChannel->HasListenerForTraceableChannel());
+  if (mWaitingForApplyConversionResponse) {
+    mWaitingForApplyConversionResponse = false;
+    mChannel->SetApplyConversion(aApply);
+    mChannel->ApplyContentConversions();
+    mChannel->Resume();
+  }
+  return IPC_OK();
+}
+
 class DivertDataAvailableEvent : public ChannelEvent
 {
 public:
   DivertDataAvailableEvent(HttpChannelParent* aParent,
                            const nsCString& data,
                            const uint64_t& offset,
                            const uint32_t& count)
   : mParent(aParent)
@@ -1133,20 +1150,16 @@ HttpChannelParent::OnStartRequest(nsIReq
     appCache->GetClientID(appCacheClientId);
     if (mIPCClosed ||
         !SendAssociateApplicationCache(appCacheGroupId, appCacheClientId))
     {
       return NS_ERROR_UNEXPECTED;
     }
   }
 
-  nsCOMPtr<nsIEncodedChannel> encodedChannel = do_QueryInterface(aRequest);
-  if (encodedChannel)
-    encodedChannel->SetApplyConversion(false);
-
   // Keep the cache entry for future use in RecvSetCacheTokenCachedCharset().
   // It could be already released by nsHttpChannel at that time.
   nsCOMPtr<nsISupports> cacheEntry;
   chan->GetCacheToken(getter_AddRefs(cacheEntry));
   mCacheEntry = do_QueryInterface(cacheEntry);
 
   nsresult channelStatus = NS_OK;
   chan->GetStatus(&channelStatus);
@@ -1170,32 +1183,54 @@ HttpChannelParent::OnStartRequest(nsIReq
     if (NS_FAILED(rv)) {
       return rv;
     }
   }
 
   nsAutoCString altDataType;
   chan->GetAlternativeDataType(altDataType);
 
+  nsCOMPtr<nsIEncodedChannel> encodedChannel = do_QueryInterface(aRequest);
+  if (encodedChannel) {
+    encodedChannel->SetApplyConversion(false);
+    if (mChannel->HasListenerForTraceableChannel()) {
+      LOG(("HttpChannelParent::OnStartRequest - suspend channel until we know "
+           "whether we need to apply conversions [this=%p]\n", this));
+      // We have a traceableChannel listener so we need to do a data conversion
+      // on the parent.
+      // The correct decision whether we need to convert the data is made on
+      // the child process during OnStartRequest call, therefore we will
+      // suspend the channel here and wait for the decision result from the
+      // child (we are waiting for RecvApplyConversion).
+      mWaitingForApplyConversionResponse = NS_SUCCEEDED(mChannel->Suspend()) ?
+                                           true : false;
+    }
+  }
+
   // !!! We need to lock headers and please don't forget to unlock them !!!
   requestHead->Enter();
   nsresult rv = NS_OK;
   if (mIPCClosed ||
       !SendOnStartRequest(channelStatus,
                           responseHead ? *responseHead : nsHttpResponseHead(),
                           !!responseHead,
                           requestHead->Headers(),
                           isFromCache,
                           mCacheEntry ? true : false,
                           expirationTime, cachedCharset, secInfoSerialization,
                           chan->GetSelfAddr(), chan->GetPeerAddr(),
                           redirectCount,
                           cacheKeyValue,
-                          altDataType))
+                          altDataType,
+                          mWaitingForApplyConversionResponse))
   {
+    if (mWaitingForApplyConversionResponse) {
+      mChannel->Resume();
+      mWaitingForApplyConversionResponse = false;
+    }
     rv = NS_ERROR_UNEXPECTED;
   }
   requestHead->Exit();
   return rv;
 }
 
 NS_IMETHODIMP
 HttpChannelParent::OnStopRequest(nsIRequest *aRequest,
@@ -1623,24 +1658,35 @@ HttpChannelParent::StartDiversion()
         mChannel->Cancel(rv);
       }
       mStatus = rv;
     }
   }
   mDivertedOnStartRequest = true;
 
   // After OnStartRequest has been called, setup content decoders if needed.
-  //
-  // Create a content conversion chain based on mDivertListener and update
-  // mDivertListener.
-  nsCOMPtr<nsIStreamListener> converterListener;
-  mChannel->DoApplyContentConversions(mDivertListener,
-                                      getter_AddRefs(converterListener));
-  if (converterListener) {
-    mDivertListener = converterListener.forget();
+  if (mWaitingForApplyConversionResponse) {
+    // If we are mWaitingForApplyConversionResponse, means that mChannel is
+    // suspended right after OnStartRequest, therefore we can apply content
+    // conversion directly on the top of mChannel and Tracable Listeners will
+    // get decoded data.
+    if (NS_SUCCEEDED(mStatus)) {
+      mChannel->ApplyContentConversions();
+    }
+    mWaitingForApplyConversionResponse = false;
+    mChannel->Resume();
+  } else {
+    // Create a content conversion chain based on mDivertListener and update
+    // mDivertListener.
+    nsCOMPtr<nsIStreamListener> converterListener;
+    mChannel->DoApplyContentConversions(mDivertListener,
+                                        getter_AddRefs(converterListener));
+    if (converterListener) {
+      mDivertListener = converterListener.forget();
+    }
   }
 
   // Now mParentListener can be diverted to mDivertListener.
   DebugOnly<nsresult> rvdbg = mParentListener->DivertTo(mDivertListener);
   MOZ_ASSERT(NS_SUCCEEDED(rvdbg));
   mDivertListener = nullptr;
 
   if (NS_WARN_IF(mIPCClosed || !SendFlushedForDiversion())) {
@@ -1701,16 +1747,21 @@ HttpChannelParent::NotifyDiversionFailed
        this, aErrorCode));
   MOZ_RELEASE_ASSERT(NS_FAILED(aErrorCode));
   MOZ_RELEASE_ASSERT(mDivertingFromChild);
   MOZ_RELEASE_ASSERT(mParentListener);
   MOZ_RELEASE_ASSERT(mChannel);
 
   mChannel->Cancel(aErrorCode);
 
+  if (mWaitingForApplyConversionResponse) {
+    mWaitingForApplyConversionResponse = false;
+    mChannel->Resume();
+  }
+
   mChannel->ForcePending(false);
 
   bool isPending = false;
   nsresult rv = mChannel->IsPending(&isPending);
   MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
 
   // Resume only if we suspended earlier.
   if (mSuspendedForDiversion) {
--- a/netwerk/protocol/http/HttpChannelParent.h
+++ b/netwerk/protocol/http/HttpChannelParent.h
@@ -170,16 +170,17 @@ protected:
   virtual mozilla::ipc::IPCResult RecvMarkOfflineCacheEntryAsForeign() override;
   virtual mozilla::ipc::IPCResult RecvDivertOnDataAvailable(const nsCString& data,
                                          const uint64_t& offset,
                                          const uint32_t& count) override;
   virtual mozilla::ipc::IPCResult RecvDivertOnStopRequest(const nsresult& statusCode) override;
   virtual mozilla::ipc::IPCResult RecvDivertComplete() override;
   virtual mozilla::ipc::IPCResult RecvRemoveCorsPreflightCacheEntry(const URIParams& uri,
                                                                     const mozilla::ipc::PrincipalInfo& requestingPrincipal) override;
+  virtual mozilla::ipc::IPCResult RecvApplyConversion(const bool& applyConversion)  override;
   virtual void ActorDestroy(ActorDestroyReason why) override;
 
   // Supporting function for ADivertableParentChannel.
   nsresult ResumeForDiversion();
 
   // Asynchronously calls NotifyDiversionFailed.
   void FailDiversion(nsresult aErrorCode, bool aSkipResume = true);
 
@@ -255,16 +256,36 @@ private:
 
   bool mSuspendedForDiversion;
 
   // Set if this channel should be suspended after synthesizing a response.
   bool mSuspendAfterSynthesizeResponse;
   // Set if this channel will synthesize its response.
   bool mWillSynthesizeResponse;
 
+  // Whether we need to apply a data conversion or not is decided during
+  // a OnStartRequest call on the last channel listener. This is perform on the
+  // child and conversion is perform on the child process as well.
+  // If the http channel has a TracableChannel listener we need to perform
+  // the data conversion, if needed, on the parent, but still the decision
+  // whether to perform the conversion or not is made during OnStartRequest on
+  // the child process. Using a parameter in SendOnStartRequest we will request
+  // from the child to send us the final decision (Send/RecvApplyConversion
+  // with parameter aApply true or false). The mChannel will be suspended until
+  // the responce is received.
+  // If the channel gets canceled we will just set the ApplyConversion to
+  // false and resume mChannel.
+  //
+  // If DivertToParent is performed, the OnStartRequest call on the last
+  // channel listener is performed on the parent and the child process does not
+  // need to send Send/RecvApplyConversion. ApplyContentConversions will be
+  // called after the OnStartRequest has been called (This is in the
+  // StartDiversion function on the parent).
+  bool mWaitingForApplyConversionResponse;
+
   dom::TabId mNestedFrameId;
 
   RefPtr<ChannelEventQueue> mEventQ;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(HttpChannelParent,
                               HTTP_CHANNEL_PARENT_IID)
 
--- a/netwerk/protocol/http/PHttpChannel.ipdl
+++ b/netwerk/protocol/http/PHttpChannel.ipdl
@@ -86,33 +86,36 @@ parent:
   // to remove any matching entry from the CORS preflight cache.
   async RemoveCorsPreflightCacheEntry(URIParams uri,
                                       PrincipalInfo requestingPrincipal);
 
   // After receiving this message, the parent calls SendDeleteSelf, and makes
   // sure not to send any more messages after that.
   async DeletingChannel();
 
+  async ApplyConversion(bool applyConversion);
+
   async __delete__();
 
 child:
   async OnStartRequest(nsresult            channelStatus,
                        nsHttpResponseHead  responseHead,
                        bool                useResponseHead,
                        nsHttpHeaderArray   requestHeaders,
                        bool                isFromCache,
                        bool                cacheEntryAvailable,
                        uint32_t            cacheExpirationTime,
                        nsCString           cachedCharset,
                        nsCString           securityInfoSerialization,
                        NetAddr             selfAddr,
                        NetAddr             peerAddr,
                        int16_t             redirectCount,
                        uint32_t            cacheKey,
-                       nsCString           altDataType);
+                       nsCString           altDataType,
+                       bool                contentDecodingWillBeCalledOnParent);
 
   // Combines a single OnDataAvailable and its associated OnProgress &
   // OnStatus calls into one IPDL message
   async OnTransportAndData(nsresult  channelStatus,
                            nsresult  transportStatus,
                            uint64_t  progress,
                            uint64_t  progressMax,
                            uint64_t  offset,
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -1210,16 +1210,33 @@ EnsureMIMEOfScript(nsIURI* aURI, nsHttpR
         return NS_OK;
     }
 
     // script load has unknown type
     Telemetry::Accumulate(Telemetry::SCRIPT_BLOCK_INCORRECT_MIME, 0);
     return NS_OK;
 }
 
+void
+nsHttpChannel::ApplyContentConversions()
+{
+    nsCOMPtr<nsIStreamListener> listener;
+    nsISupports *ctxt = mListenerContext;
+    nsresult rv = DoApplyContentConversions(mListener,
+                                            getter_AddRefs(listener),
+                                            ctxt);
+    if (NS_FAILED(rv)) {
+        AsyncAbort(rv);
+    }
+
+    if (listener) {
+        mListener = listener;
+        mCompressListener = listener;
+    }
+}
 
 nsresult
 nsHttpChannel::CallOnStartRequest()
 {
     MOZ_RELEASE_ASSERT(!(mRequireCORSPreflight &&
                          mInterceptCache != INTERCEPTED) ||
                        mIsCorsPreflightDone,
                        "CORS preflight must have been finished by the time we "
--- a/netwerk/protocol/http/nsHttpChannel.h
+++ b/netwerk/protocol/http/nsHttpChannel.h
@@ -173,16 +173,18 @@ public:
     NS_IMETHOD OnPreflightFailed(nsresult aError) override;
 
     nsresult AddSecurityMessage(const nsAString& aMessageTag,
                                 const nsAString& aMessageCategory) override;
 
     void SetWarningReporter(HttpChannelSecurityWarningReporter* aReporter)
       { mWarningReporter = aReporter; }
 
+    void ApplyContentConversions();
+
 public: /* internal necko use only */
 
     void InternalSetUploadStream(nsIInputStream *uploadStream)
       { mUploadStream = uploadStream; }
     void SetUploadStreamHasHeaders(bool hasHeaders)
       { mUploadStreamHasHeaders = hasHeaders; }
 
     nsresult SetReferrerWithPolicyInternal(nsIURI *referrer,
new file mode 100644
--- /dev/null
+++ b/netwerk/test/unit_ipc/child_tracable_listener.js
@@ -0,0 +1,36 @@
+/**
+ * Send HTTP requests and check if the received content is correct.
+ */
+
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+let shouldQuit = false;
+
+function run_test() {
+  // keep the event loop busy and the test alive until a "finish" command
+  // is issued by parent
+  do_timeout(100, function keepAlive() {
+    if (!shouldQuit) {
+      do_timeout(100, keepAlive);
+    }
+  });
+}
+
+var expectedResponse;
+
+function makeRequest(uri, response) {
+  let requestChannel = NetUtil.newChannel({uri, loadUsingSystemPrincipal: true});
+
+  expectedResponse = response;
+  requestChannel.asyncOpen2(new ChannelListener(checkResponse, null, CL_EXPECT_GZIP));
+}
+
+function checkResponse(request, buffer) {
+  do_check_eq(expectedResponse, buffer);
+
+  do_send_remote_message(`response`);
+}
+
+function finish() {
+  shouldQuit = true;
+}
new file mode 100644
--- /dev/null
+++ b/netwerk/test/unit_ipc/test_traceable_channel_decoded_data.js
@@ -0,0 +1,172 @@
+// Test nsITraceableChannel interface.
+//
+// The test checks that content-encoded responses are received decoded by a
+// tracing listener.
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+const DEBUG = false;
+
+function debug(msg) {
+  if (DEBUG) {
+    dump(msg);
+  }
+}
+
+var httpserver = new HttpServer();
+httpserver.start(-1);
+const PORT = httpserver.identity.primaryPort;
+
+var originalBody = "original http response body";
+
+var body = [
+  0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcb, 0x2f, 0xca, 0x4c, 0xcf, 0xcc,
+  0x4b, 0xcc, 0x51, 0xc8, 0x28, 0x29, 0x29, 0x50, 0x28, 0x4a, 0x2d, 0x2e, 0xc8, 0xcf, 0x2b, 0x4e,
+  0x55, 0x48, 0xca, 0x4f, 0xa9, 0x04, 0x00, 0x94, 0xde, 0x94, 0x9c, 0x1b, 0x00, 0x00, 0x00];
+
+var observer;
+
+function TracingListener() {}
+
+TracingListener.prototype = {
+  gotOnStartRequest: false,
+  pipe: null,
+  streamSink: null,
+  listener: null,
+
+  // Replace received response body.
+  onDataAvailable: function(request, context, inputStream,
+                           offset, count) {
+    debug("*** tracing listener onDataAvailable\n");
+  },
+
+  onStartRequest: function(request, context) {
+    debug("*** tracing listener onStartRequest\n");
+
+    this.gotOnStartRequest = true;
+
+    request.QueryInterface(Components.interfaces.nsIHttpChannelInternal);
+
+    do_check_eq(request.localAddress, "127.0.0.1");
+    do_check_eq(request.localPort > 0, true);
+    do_check_neq(request.localPort, PORT);
+    do_check_eq(request.remoteAddress, "127.0.0.1");
+    do_check_eq(request.remotePort, PORT);
+  },
+
+  onStopRequest: function(request, context, statusCode) {
+    debug("*** tracing listener onStopRequest\n");
+
+    var sin = Components.classes["@mozilla.org/scriptableinputstream;1"].
+                createInstance(Ci.nsIScriptableInputStream);
+
+    this.streamSink.close();
+    var input = this.pipe.inputStream;
+    sin.init(input);
+    do_check_eq(sin.available(), originalBody.length);
+
+    var result = sin.read(originalBody.length);
+    do_check_eq(result, originalBody);
+
+    input.close();
+
+    do_check_eq(this.gotOnStartRequest, true);
+
+    let message = `response`;
+    do_await_remote_message(message).then(() => {
+      sendCommand("finish();", stopServer);
+    });
+  },
+
+  QueryInterface: function(iid) {
+    if (iid.equals(Components.interfaces.nsIStreamListener) ||
+        iid.equals(Components.interfaces.nsIRequestObserver) ||
+        iid.equals(Components.interfaces.nsISupports)
+        )
+      return this;
+    throw Components.results.NS_NOINTERFACE;
+  }
+}
+
+function HttpResponseExaminer() {}
+
+HttpResponseExaminer.prototype = {
+  register: function() {
+    Cc["@mozilla.org/observer-service;1"].
+      getService(Components.interfaces.nsIObserverService).
+      addObserver(this, "http-on-examine-response", true);
+    debug("Did HttpResponseExaminer.register\n");
+  },
+
+  unregister: function() {
+    Cc["@mozilla.org/observer-service;1"].
+      getService(Components.interfaces.nsIObserverService).
+      removeObserver(this, "http-on-examine-response", true);
+    debug("Did HttpResponseExaminer.unregister\n");
+  },
+
+  // Replace channel's listener.
+  observe: function(subject, topic, data) {
+    debug("In HttpResponseExaminer.observe\n");
+
+    try {
+      subject.QueryInterface(Components.interfaces.nsITraceableChannel);
+
+      var tee = Cc["@mozilla.org/network/stream-listener-tee;1"].
+          createInstance(Ci.nsIStreamListenerTee);
+      var newListener = new TracingListener();
+      newListener.pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+      newListener.pipe.init(false, false, 0, 0xffffffff, null);
+      newListener.streamSink = newListener.pipe.outputStream;
+
+      var originalListener = subject.setNewListener(tee);
+      tee.init(originalListener, newListener.streamSink, newListener);
+    } catch(e) {
+      do_throw("can't replace listener " + e);
+    }
+
+    debug("Did HttpResponseExaminer.observe\n");
+  },
+
+  QueryInterface: function(iid) {
+    if (iid.equals(Components.interfaces.nsIObserver) ||
+        iid.equals(Components.interfaces.nsISupportsWeakReference) ||
+        iid.equals(Components.interfaces.nsISupports))
+      return this;
+    throw Components.results.NS_NOINTERFACE;
+  }
+}
+
+function test_handlerContentEncoded(metadata, response) {
+  response.setStatusLine(metadata.httpVersion, 200, "OK");
+  response.setHeader("Content-Type", "text/plain", false);
+  response.setHeader("Content-Encoding", "gzip", false);
+  response.setHeader("Content-Length", "" + body.length, false);
+
+  var bos = Components.classes["@mozilla.org/binaryoutputstream;1"]
+    .createInstance(Components.interfaces.nsIBinaryOutputStream);
+  bos.setOutputStream(response.bodyOutputStream);
+
+  response.processAsync();
+  bos.writeByteArray(body, body.length);
+  response.finish();
+}
+
+function test_contentEncoded() {
+  sendCommand(`makeRequest("http://localhost:${PORT}/testContentEncoded", "${originalBody}");`);
+}
+
+function stopServer() {
+  observer.unregister();
+  httpserver.stop(do_test_finished)
+}
+
+function run_test() {
+
+  observer = new HttpResponseExaminer();
+  observer.register();
+
+  httpserver.registerPathHandler("/testContentEncoded", test_handlerContentEncoded);
+  run_test_in_child("child_tracable_listener.js", test_contentEncoded);
+}
new file mode 100644
--- /dev/null
+++ b/netwerk/test/unit_ipc/test_traceable_channel_modify_response.js
@@ -0,0 +1,186 @@
+// Test nsITraceableChannel interface.
+//
+// The test checks that a tracing listener can modifies the body of a HTTP
+// response.
+// This test also check that it is not possible to set a tracableLisener after
+// onStartRequest is called.
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+const DEBUG = false;
+
+function debug(msg) {
+  if (DEBUG) {
+    dump(msg);
+  }
+}
+
+var httpserver = new HttpServer();
+httpserver.start(-1);
+const PORT = httpserver.identity.primaryPort;
+
+var originalBody = "original http response body";
+var replacedBody = "replaced http response body";
+
+var observer;
+
+function TracingListener() {}
+
+TracingListener.prototype = {
+  gotOnStartRequest: false,
+  pipe: null,
+  streamSink: null,
+  listener: null,
+  onDataAvailableCalled: false,
+
+  // Replace received response body.
+  onDataAvailable: function(request, context, inputStream,
+                           offset, count) {
+    debug("*** tracing listener onDataAvailable\n");
+
+    var binaryInputStream = Cc["@mozilla.org/binaryinputstream;1"].
+      createInstance(Components.interfaces.nsIBinaryInputStream);
+    binaryInputStream.setInputStream(inputStream);
+
+    var data = binaryInputStream.readBytes(count);
+    var origBody = originalBody.substr(offset, count);
+
+    do_check_eq(origBody, data);
+
+    if (!this.onDataAvailableCalled) {
+      this.onDataAvailableCalled = true;
+      var storageStream = Cc["@mozilla.org/storagestream;1"].
+        createInstance(Components.interfaces.nsIStorageStream);
+      var binaryOutputStream = Cc["@mozilla.org/binaryoutputstream;1"].
+        createInstance(Components.interfaces.nsIBinaryOutputStream);
+
+      storageStream.init(8192, 100, null);
+      binaryOutputStream.setOutputStream(storageStream.getOutputStream(0));
+
+      var newBody = replacedBody.substr(offset, count);
+      binaryOutputStream.writeBytes(newBody, newBody.length);
+
+      this.listener.onDataAvailable(request, context,
+                                    storageStream.newInputStream(0), 0,
+                                    replacedBody.length);
+    }
+  },
+
+  onStartRequest: function(request, context) {
+    debug("*** tracing listener onStartRequest\n");
+
+    this.listener.onStartRequest(request, context);
+
+    this.gotOnStartRequest = true;
+
+    request.QueryInterface(Components.interfaces.nsIHttpChannelInternal);
+
+    do_check_eq(request.localAddress, "127.0.0.1");
+    do_check_eq(request.localPort > 0, true);
+    do_check_neq(request.localPort, PORT);
+    do_check_eq(request.remoteAddress, "127.0.0.1");
+    do_check_eq(request.remotePort, PORT);
+
+    // Make sure listener can't be replaced after OnStartRequest was called.
+    request.QueryInterface(Components.interfaces.nsITraceableChannel);
+    try {
+      var newListener = new TracingListener();
+      newListener.listener = request.setNewListener(newListener);
+    } catch(e) {
+      do_check_true(true, "TracingListener.onStartRequest swallowing exception: " + e + "\n");
+      return; // OK
+    }
+    do_throw("replaced channel's listener during onStartRequest.");
+  },
+
+  onStopRequest: function(request, context, statusCode) {
+    debug("*** tracing listener onStopRequest\n");
+
+
+    this.listener.onStopRequest(request, context, statusCode)
+
+    do_check_eq(this.gotOnStartRequest, true);
+
+    let message = `response`;
+    do_await_remote_message(message).then(() => {
+      sendCommand("finish();", stopServer);
+    });
+  },
+
+  QueryInterface: function(iid) {
+    if (iid.equals(Components.interfaces.nsIStreamListener) ||
+        iid.equals(Components.interfaces.nsIRequestObserver) ||
+        iid.equals(Components.interfaces.nsISupports)
+        )
+      return this;
+    throw Components.results.NS_NOINTERFACE;
+  }
+}
+
+function HttpResponseExaminer() {}
+
+HttpResponseExaminer.prototype = {
+  register: function() {
+    Cc["@mozilla.org/observer-service;1"].
+      getService(Components.interfaces.nsIObserverService).
+      addObserver(this, "http-on-examine-response", true);
+    debug("Did HttpResponseExaminer.register\n");
+  },
+
+  unregister: function() {
+    Cc["@mozilla.org/observer-service;1"].
+      getService(Components.interfaces.nsIObserverService).
+      removeObserver(this, "http-on-examine-response", true);
+    debug("Did HttpResponseExaminer.unregister\n");
+  },
+
+  // Replace channel's listener.
+  observe: function(subject, topic, data) {
+    debug("In HttpResponseExaminer.observe\n");
+
+    try {
+      subject.QueryInterface(Components.interfaces.nsITraceableChannel);
+      var newListener = new TracingListener();
+      newListener.listener = subject.setNewListener(newListener);
+    } catch(e) {
+      do_throw("can't replace listener " + e);
+    }
+
+    debug("Did HttpResponseExaminer.observe\n");
+  },
+
+  QueryInterface: function(iid) {
+    if (iid.equals(Components.interfaces.nsIObserver) ||
+        iid.equals(Components.interfaces.nsISupportsWeakReference) ||
+        iid.equals(Components.interfaces.nsISupports))
+      return this;
+    throw Components.results.NS_NOINTERFACE;
+  }
+}
+
+function test_handlerSimple(metadata, response) {
+  response.setHeader("Content-Type", "text/html", false);
+  response.setStatusLine(metadata.httpVersion, 200, "OK");
+  response.bodyOutputStream.write(originalBody, originalBody.length);
+}
+
+function test_modify() { 
+  sendCommand(`makeRequest("http://localhost:${PORT}/testSimple", "${replacedBody}");`);
+}
+
+
+function stopServer() {
+  observer.unregister();
+  httpserver.stop(do_test_finished)
+}
+
+function run_test() {
+
+  observer = new HttpResponseExaminer();
+  observer.register();
+
+  httpserver.registerPathHandler("/testSimple", test_handlerSimple);
+
+  run_test_in_child("child_tracable_listener.js", test_modify);
+}
--- a/netwerk/test/unit_ipc/xpcshell.ini
+++ b/netwerk/test/unit_ipc/xpcshell.ini
@@ -1,14 +1,15 @@
 [DEFAULT]
 head = head_channels_clone.js head_cc.js
 tail =
 skip-if = toolkit == 'android'
 support-files =
   child_channel_id.js
+  child_tracable_listener.js
   !/netwerk/test/unit/test_XHR_redirects.js
   !/netwerk/test/unit/test_bug248970_cookie.js
   !/netwerk/test/unit/test_bug528292.js
   !/netwerk/test/unit/test_cache_jar.js
   !/netwerk/test/unit/test_cacheflags.js
   !/netwerk/test/unit/test_channel_close.js
   !/netwerk/test/unit/test_cookie_header.js
   !/netwerk/test/unit/test_cookiejars.js
@@ -93,10 +94,12 @@ skip-if = true
 [test_XHR_redirects.js]
 [test_redirect_history_wrap.js]
 [test_reply_without_content_type_wrap.js]
 [test_getHost_wrap.js]
 [test_alt-data_simple_wrap.js]
 [test_alt-data_stream_wrap.js]
 [test_original_sent_received_head_wrap.js]
 [test_channel_id.js]
+[test_traceable_channel_modify_response.js]
+[test_traceable_channel_decoded_data.js]
 [test_trackingProtection_annotateChannels_wrap1.js]
 [test_trackingProtection_annotateChannels_wrap2.js]