Bug 1141332 - Disable content decoding and use decoded length on intercepted channels. r=mcmanus
authorNikhil Marathe <nsm.nikhil@gmail.com>
Tue, 17 Mar 2015 08:48:32 -0700
changeset 263846 675bb5164535a474815e095920a116d2407d42cc
parent 263845 76b1809d22ebe04df848f2061cd26a282db1ff13
child 263847 84dae60ceecfba156cdca3f488045447a4dcf4f2
push id4718
push userraliiev@mozilla.com
push dateMon, 11 May 2015 18:39:53 +0000
treeherdermozilla-beta@c20c4ef55f08 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmcmanus
bugs1141332
milestone39.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 1141332 - Disable content decoding and use decoded length on intercepted channels. r=mcmanus
dom/workers/ServiceWorkerEvents.cpp
dom/workers/test/serviceworkers/fetch/deliver-gzip.sjs
dom/workers/test/serviceworkers/fetch/fetch_tests.js
dom/workers/test/serviceworkers/fetch_event_worker.js
dom/workers/test/serviceworkers/mochitest.ini
netwerk/protocol/http/HttpChannelChild.cpp
netwerk/protocol/http/HttpChannelChild.h
netwerk/protocol/http/InterceptedChannel.cpp
netwerk/protocol/http/InterceptedChannel.h
netwerk/protocol/http/nsHttpChannel.cpp
--- a/dom/workers/ServiceWorkerEvents.cpp
+++ b/dom/workers/ServiceWorkerEvents.cpp
@@ -239,16 +239,19 @@ RespondWithHandler::ResolvedCallback(JSC
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return;
     }
 
     nsCOMPtr<nsIEventTarget> stsThread = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
     if (NS_WARN_IF(!stsThread)) {
       return;
     }
+
+    // XXXnsm, Fix for Bug 1141332 means that if we decide to make this
+    // streaming at some point, we'll need a different solution to that bug.
     rv = NS_AsyncCopy(body, responseBody, stsThread, NS_ASYNCCOPY_VIA_READSEGMENTS, 4096,
                       RespondWithCopyComplete, closure.forget());
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return;
     }
   } else {
     RespondWithCopyComplete(closure.forget(), NS_OK);
   }
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/serviceworkers/fetch/deliver-gzip.sjs
@@ -0,0 +1,17 @@
+function handleRequest(request, response) {
+  // The string "hello" repeated 10 times followed by newline. Compressed using gzip.
+  var bytes = [0x1f, 0x8b, 0x08, 0x08, 0x4d, 0xe2, 0xf9, 0x54, 0x00, 0x03, 0x68,
+               0x65, 0x6c, 0x6c, 0x6f, 0x00, 0xcb, 0x48, 0xcd, 0xc9, 0xc9, 0xcf,
+               0x20, 0x85, 0xe0, 0x02, 0x00, 0xf5, 0x4b, 0x38, 0xcf, 0x33, 0x00,
+               0x00, 0x00];
+
+  response.setHeader("Content-Encoding", "gzip", false);
+  response.setHeader("Content-Length", "" + bytes.length, false);
+  response.setHeader("Content-Type", "text/plain", false);
+
+  var bos = Components.classes["@mozilla.org/binaryoutputstream;1"]
+      .createInstance(Components.interfaces.nsIBinaryOutputStream);
+  bos.setOutputStream(response.bodyOutputStream);
+
+  bos.writeByteArray(bytes, bytes.length);
+}
--- a/dom/workers/test/serviceworkers/fetch/fetch_tests.js
+++ b/dom/workers/test/serviceworkers/fetch/fetch_tests.js
@@ -66,8 +66,40 @@ fetch('nonresponse2.txt', null, function
   finish();
 });
 
 fetch('headers.txt', function(xhr) {
   my_ok(xhr.status == 200, "load should be successful");
   my_ok(xhr.responseText == "1", "request header checks should have passed");
   finish();
 }, null, [["X-Test1", "header1"], ["X-Test2", "header2"]]);
+
+var expectedUncompressedResponse = "";
+for (var i = 0; i < 10; ++i) {
+  expectedUncompressedResponse += "hello";
+}
+expectedUncompressedResponse += "\n";
+
+// ServiceWorker does not intercept, at which point the network request should
+// be correctly decoded.
+fetch('deliver-gzip.sjs', function(xhr) {
+  my_ok(xhr.status == 200, "network gzip load should be successful");
+  my_ok(xhr.responseText == expectedUncompressedResponse, "network gzip load should have synthesized response.");
+  my_ok(xhr.getResponseHeader("Content-Encoding") == "gzip", "network Content-Encoding should be gzip.");
+  my_ok(xhr.getResponseHeader("Content-Length") == "35", "network Content-Length should be of original gzipped file.");
+  finish();
+});
+
+fetch('hello.gz', function(xhr) {
+  my_ok(xhr.status == 200, "gzip load should be successful");
+  my_ok(xhr.responseText == expectedUncompressedResponse, "gzip load should have synthesized response.");
+  my_ok(xhr.getResponseHeader("Content-Encoding") == "gzip", "Content-Encoding should be gzip.");
+  my_ok(xhr.getResponseHeader("Content-Length") == "35", "Content-Length should be of original gzipped file.");
+  finish();
+});
+
+fetch('hello-after-extracting.gz', function(xhr) {
+  my_ok(xhr.status == 200, "gzip load should be successful");
+  my_ok(xhr.responseText == expectedUncompressedResponse, "gzip load should have synthesized response.");
+  my_ok(xhr.getResponseHeader("Content-Encoding") == "gzip", "Content-Encoding should be gzip.");
+  my_ok(xhr.getResponseHeader("Content-Length") == "35", "Content-Length should be of original gzipped file.");
+  finish();
+});
--- a/dom/workers/test/serviceworkers/fetch_event_worker.js
+++ b/dom/workers/test/serviceworkers/fetch_event_worker.js
@@ -91,9 +91,28 @@ onfetch = function(ev) {
     ));
   }
 
   else if (ev.request.url.contains("nonexistent_imported_script.js")) {
     ev.respondWith(Promise.resolve(
       new Response("check_intercepted_script();", {})
     ));
   }
+
+  else if (ev.request.url.contains("deliver-gzip")) {
+    // Don't handle the request, this will make Necko perform a network request, at
+    // which point SetApplyConversion must be re-enabled, otherwise the request
+    // will fail.
+    return;
+  }
+
+  else if (ev.request.url.contains("hello.gz")) {
+    ev.respondWith(fetch("fetch/deliver-gzip.sjs"));
+  }
+
+  else if (ev.request.url.contains("hello-after-extracting.gz")) {
+    ev.respondWith(fetch("fetch/deliver-gzip.sjs").then(function(res) {
+      return res.text().then(function(body) {
+        return new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers });
+      });
+    }));
+  }
 }
--- a/dom/workers/test/serviceworkers/mochitest.ini
+++ b/dom/workers/test/serviceworkers/mochitest.ini
@@ -20,16 +20,17 @@ support-files =
   match_all_worker.js
   match_all_advanced_worker.js
   worker_unregister.js
   worker_update.js
   message_posting_worker.js
   fetch/index.html
   fetch/fetch_worker_script.js
   fetch/fetch_tests.js
+  fetch/deliver-gzip.sjs
   fetch/https/index.html
   fetch/https/register.html
   fetch/https/unregister.html
   fetch/https/https_test.js
   fetch/https/clonedresponse/index.html
   fetch/https/clonedresponse/register.html
   fetch/https/clonedresponse/unregister.html
   fetch/https/clonedresponse/https_test.js
--- a/netwerk/protocol/http/HttpChannelChild.cpp
+++ b/netwerk/protocol/http/HttpChannelChild.cpp
@@ -131,17 +131,17 @@ InterceptStreamListener::OnDataAvailable
     mOwner->GetURI(getter_AddRefs(uri));
 
     nsAutoCString host;
     uri->GetHost(host);
 
     OnStatus(mOwner, aContext, NS_NET_STATUS_READING, NS_ConvertUTF8toUTF16(host).get());
 
     int64_t progress = aOffset + aCount;
-    OnProgress(mOwner, aContext, progress, mOwner->GetResponseHead()->ContentLength());
+    OnProgress(mOwner, aContext, progress, mOwner->mSynthesizedStreamLength);
   }
 
   mOwner->DoOnDataAvailable(mOwner, mContext, aInputStream, aOffset, aCount);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 InterceptStreamListener::OnStopRequest(nsIRequest* aRequest, nsISupports* aContext, nsresult aStatusCode)
@@ -162,16 +162,17 @@ InterceptStreamListener::Cleanup()
 }
 
 //-----------------------------------------------------------------------------
 // HttpChannelChild
 //-----------------------------------------------------------------------------
 
 HttpChannelChild::HttpChannelChild()
   : HttpAsyncAborter<HttpChannelChild>(this)
+  , mSynthesizedStreamLength(0)
   , mIsFromCache(false)
   , mCacheEntryAvailable(false)
   , mCacheExpirationTime(nsICacheEntry::NO_EXPIRATION_TIME)
   , mSendResumeAt(false)
   , mIPCOpen(false)
   , mKeptAlive(false)
   , mDivertingToParent(false)
   , mFlushedForDiversion(false)
@@ -2078,20 +2079,22 @@ HttpChannelChild::ResetInterception()
 
   // Continue with the original cross-process request
   nsresult rv = ContinueAsyncOpen();
   NS_ENSURE_SUCCESS_VOID(rv);
 }
 
 void
 HttpChannelChild::OverrideWithSynthesizedResponse(nsAutoPtr<nsHttpResponseHead>& aResponseHead,
-                                                  nsInputStreamPump* aPump)
+                                                  nsInputStreamPump* aPump,
+                                                  int64_t aStreamLength)
 {
   mSynthesizedResponsePump = aPump;
   mResponseHead = aResponseHead;
+  mSynthesizedStreamLength = aStreamLength;
 
   // if this channel has been suspended previously, the pump needs to be
   // correspondingly suspended now that it exists.
   for (uint32_t i = 0; i < mSuspendCount; i++) {
     nsresult rv = mSynthesizedResponsePump->Suspend();
     NS_ENSURE_SUCCESS_VOID(rv);
   }
 
--- a/netwerk/protocol/http/HttpChannelChild.h
+++ b/netwerk/protocol/http/HttpChannelChild.h
@@ -154,22 +154,23 @@ private:
   void DoPreOnStopRequest(nsresult aStatus);
   void DoOnStopRequest(nsIRequest* aRequest, nsISupports* aContext);
 
   // Discard the prior interception and continue with the original network request.
   void ResetInterception();
 
   // Override this channel's pending response with a synthesized one. The content will be
   // asynchronously read from the pump.
-  void OverrideWithSynthesizedResponse(nsAutoPtr<nsHttpResponseHead>& aResponseHead, nsInputStreamPump* aPump);
+  void OverrideWithSynthesizedResponse(nsAutoPtr<nsHttpResponseHead>& aResponseHead, nsInputStreamPump* aPump, int64_t aStreamLength);
 
   RequestHeaderTuples mClientSetRequestHeaders;
   nsCOMPtr<nsIChildChannel> mRedirectChannelChild;
   nsRefPtr<InterceptStreamListener> mInterceptListener;
   nsRefPtr<nsInputStreamPump> mSynthesizedResponsePump;
+  int64_t mSynthesizedStreamLength;
 
   bool mIsFromCache;
   bool mCacheEntryAvailable;
   uint32_t     mCacheExpirationTime;
   nsCString    mCachedCharset;
 
   // If ResumeAt is called before AsyncOpen, we need to send extra data upstream
   bool mSendResumeAt;
--- a/netwerk/protocol/http/InterceptedChannel.cpp
+++ b/netwerk/protocol/http/InterceptedChannel.cpp
@@ -97,23 +97,30 @@ InterceptedChannelBase::DoSynthesizeHead
 
 InterceptedChannelChrome::InterceptedChannelChrome(nsHttpChannel* aChannel,
                                                    nsINetworkInterceptController* aController,
                                                    nsICacheEntry* aEntry)
 : InterceptedChannelBase(aController, aChannel->IsNavigation())
 , mChannel(aChannel)
 , mSynthesizedCacheEntry(aEntry)
 {
+  nsresult rv = mChannel->GetApplyConversion(&mOldApplyConversion);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    mOldApplyConversion = false;
+  }
 }
 
 void
 InterceptedChannelChrome::NotifyController()
 {
   nsCOMPtr<nsIOutputStream> out;
 
+  // Intercepted responses should already be decoded.
+  mChannel->SetApplyConversion(false);
+
   nsresult rv = mSynthesizedCacheEntry->OpenOutputStream(0, getter_AddRefs(mResponseBody));
   NS_ENSURE_SUCCESS_VOID(rv);
 
   DoNotifyController();
 }
 
 NS_IMETHODIMP
 InterceptedChannelChrome::GetChannel(nsIChannel** aChannel)
@@ -127,16 +134,18 @@ InterceptedChannelChrome::ResetIntercept
 {
   if (!mChannel) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   mSynthesizedCacheEntry->AsyncDoom(nullptr);
   mSynthesizedCacheEntry = nullptr;
 
+  mChannel->SetApplyConversion(mOldApplyConversion);
+
   nsCOMPtr<nsIURI> uri;
   mChannel->GetURI(getter_AddRefs(uri));
 
   nsresult rv = mChannel->StartRedirectChannelToURI(uri, nsIChannelEventSink::REDIRECT_INTERNAL);
   NS_ENSURE_SUCCESS(rv, rv);
 
   mChannel = nullptr;
   return NS_OK;
@@ -304,17 +313,32 @@ InterceptedChannelContent::FinishSynthes
     return rv;
   }
 
   mResponseBody = nullptr;
 
   rv = mStoragePump->AsyncRead(mStreamListener, nullptr);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  mChannel->OverrideWithSynthesizedResponse(mSynthesizedResponseHead.ref(), mStoragePump);
+  // Intercepted responses should already be decoded.
+  mChannel->SetApplyConversion(false);
+
+  // In our current implementation, the FetchEvent handler will copy the
+  // response stream completely into the pipe backing the input stream so we
+  // can treat the available as the length of the stream.
+  int64_t streamLength;
+  uint64_t available;
+  rv = mSynthesizedInput->Available(&available);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    streamLength = -1;
+  } else {
+    streamLength = int64_t(available);
+  }
+
+  mChannel->OverrideWithSynthesizedResponse(mSynthesizedResponseHead.ref(), mStoragePump, streamLength);
 
   mChannel = nullptr;
   mStreamListener = nullptr;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 InterceptedChannelContent::Cancel()
--- a/netwerk/protocol/http/InterceptedChannel.h
+++ b/netwerk/protocol/http/InterceptedChannel.h
@@ -61,16 +61,22 @@ public:
 
 class InterceptedChannelChrome : public InterceptedChannelBase
 {
   // The actual channel being intercepted.
   nsRefPtr<nsHttpChannel> mChannel;
 
   // Writeable cache entry for use when synthesizing a response in a parent process
   nsCOMPtr<nsICacheEntry> mSynthesizedCacheEntry;
+
+  // When a channel is intercepted, content decoding is disabled since the
+  // ServiceWorker will have already extracted the decoded data. For parent
+  // process channels we need to preserve the earlier value in case
+  // ResetInterception is called.
+  bool mOldApplyConversion;
 public:
   InterceptedChannelChrome(nsHttpChannel* aChannel,
                            nsINetworkInterceptController* aController,
                            nsICacheEntry* aEntry);
 
   NS_IMETHOD ResetInterception() override;
   NS_IMETHOD FinishSynthesizedResponse() override;
   NS_IMETHOD GetChannel(nsIChannel** aChannel) override;
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -2992,17 +2992,17 @@ nsHttpChannel::OnCacheEntryCheck(nsICach
             }
 
             if (size == 0 && mCacheOnlyMetadata) {
                 // Don't break cache entry load when the entry's data size
                 // is 0 and mCacheOnlyMetadata flag is set. In that case we
                 // want to proceed since the LOAD_ONLY_IF_MODIFIED flag is
                 // also set.
                 MOZ_ASSERT(mLoadFlags & LOAD_ONLY_IF_MODIFIED);
-            } else {
+            } else if (mInterceptCache != INTERCEPTED) {
                 return rv;
             }
         }
     }
 
     bool isHttps = false;
     rv = mURI->SchemeIs("https", &isHttps);
     NS_ENSURE_SUCCESS(rv,rv);