Bug 1407384 - P3: Handle the case if the channel is suspended or canceled by "http-on-examine-merged-response" observer r=mayhemer
authorKershaw Chang <kershaw@mozilla.com>
Tue, 15 Jan 2019 16:17:48 +0000
changeset 453950 f5b0ec66117d2fe16c7729cee9362831edb1a1ca
parent 453949 18abdcc812a606114c3b761c5134699bd385c703
child 453951 ac94b0e3484b102481f2c8cf52d012cbc46b05cf
push id35380
push userdluca@mozilla.com
push dateTue, 15 Jan 2019 22:13:12 +0000
treeherdermozilla-central@a51d26029042 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmayhemer
bugs1407384
milestone66.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 1407384 - P3: Handle the case if the channel is suspended or canceled by "http-on-examine-merged-response" observer r=mayhemer 1. This patch somehow sets a "breakpoint" in ProcessPartialContent() and ProcessNotModified() to really stop doing things after ProcessPartialContent() and ProcessNotModified(), when the channel is suspended. 2. Add a test for this. Differential Revision: https://phabricator.services.mozilla.com/D13277
netwerk/protocol/http/nsHttpChannel.cpp
netwerk/protocol/http/nsHttpChannel.h
netwerk/test/unit/test_suspend_channel_on_examine_merged_response.js
netwerk/test/unit/xpcshell.ini
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -2455,19 +2455,16 @@ nsresult nsHttpChannel::ContinueProcessR
     // code that called this function.
     return NS_OK;
   }
 
   rv = NS_OK;
 
   uint32_t httpStatus = mResponseHead->Status();
 
-  bool successfulReval = false;
-  bool partialContentUsed = false;
-
   // handle different server response categories.  Note that we handle
   // caching or not caching of error pages in
   // nsHttpResponseHead::MustValidate; if you change this switch, update that
   // one
   switch (httpStatus) {
     case 200:
     case 203:
       // Per RFC 2616, 14.35.2, "A server MAY ignore the Range header".
@@ -2481,20 +2478,26 @@ nsresult nsHttpChannel::ContinueProcessR
         break;
       }
       // these can normally be cached
       rv = ProcessNormal();
       MaybeInvalidateCacheEntryForSubsequentGet();
       break;
     case 206:
       if (mCachedContentIsPartial) {  // an internal byte range request...
-        rv = ProcessPartialContent();
-        if (NS_SUCCEEDED(rv)) {
-          partialContentUsed = true;
+        auto func = [](auto *self, nsresult aRv) {
+          return self->ContinueProcessResponseAfterPartialContent(aRv);
+        };
+        rv = ProcessPartialContent(func);
+        // Directly call ContinueProcessResponseAfterPartialContent if channel
+        // is not suspended or ProcessPartialContent throws.
+        if (!mSuspendCount || NS_FAILED(rv)) {
+          return ContinueProcessResponseAfterPartialContent(rv);
         }
+        return NS_OK;
       } else {
         mCacheInputStream.CloseAndRelease();
         rv = ProcessNormal();
       }
       break;
     case 300:
     case 301:
     case 302:
@@ -2519,39 +2522,26 @@ nsresult nsHttpChannel::ContinueProcessR
           DoNotifyListener();
         } else {
           rv = ContinueProcessResponse3(rv);
         }
       }
       break;
     case 304:
       if (!ShouldBypassProcessNotModified()) {
-        rv = ProcessNotModified();
-        if (NS_SUCCEEDED(rv)) {
-          successfulReval = true;
-          break;
+        auto func = [](auto *self, nsresult aRv) {
+          return self->ContinueProcessResponseAfterNotModified(aRv);
+        };
+        rv = ProcessNotModified(func);
+        // Directly call ContinueProcessResponseAfterNotModified if channel
+        // is not suspended or ProcessNotModified throws.
+        if (!mSuspendCount || NS_FAILED(rv)) {
+          return ContinueProcessResponseAfterNotModified(rv);
         }
-
-        LOG(("ProcessNotModified failed [rv=%" PRIx32 "]\n",
-             static_cast<uint32_t>(rv)));
-
-        // We cannot read from the cache entry, it might be in an
-        // incosistent state.  Doom it and redirect the channel
-        // to the same URI to reload from the network.
-        mCacheInputStream.CloseAndRelease();
-        if (mCacheEntry) {
-          mCacheEntry->AsyncDoom(nullptr);
-          mCacheEntry = nullptr;
-        }
-
-        rv = StartRedirectChannelToURI(mURI,
-                                       nsIChannelEventSink::REDIRECT_INTERNAL);
-        if (NS_SUCCEEDED(rv)) {
-          return NS_OK;
-        }
+        return NS_OK;
       }
 
       // Don't cache uninformative 304
       if (mCustomConditionalRequest) {
         CloseCacheEntry(false);
       }
 
       if (ShouldBypassProcessNotModified() || NS_FAILED(rv)) {
@@ -2612,32 +2602,92 @@ nsresult nsHttpChannel::ContinueProcessR
       CloseCacheEntry(false);
       MOZ_FALLTHROUGH;  // process normally
     default:
       rv = ProcessNormal();
       MaybeInvalidateCacheEntryForSubsequentGet();
       break;
   }
 
+  UpdateCacheDisposition(false, false);
+  return rv;
+}
+
+nsresult nsHttpChannel::ContinueProcessResponseAfterPartialContent(
+    nsresult aRv) {
+  LOG(
+      ("nsHttpChannel::ContinueProcessResponseAfterPartialContent "
+       "[this=%p, rv=%" PRIx32 "]",
+       this, static_cast<uint32_t>(aRv)));
+
+  UpdateCacheDisposition(false, NS_SUCCEEDED(aRv));
+  return aRv;
+}
+
+nsresult nsHttpChannel::ContinueProcessResponseAfterNotModified(nsresult aRv) {
+  LOG(
+      ("nsHttpChannel::ContinueProcessResponseAfterNotModified "
+       "[this=%p, rv=%" PRIx32 "]",
+       this, static_cast<uint32_t>(aRv)));
+
+  if (NS_SUCCEEDED(aRv)) {
+    mTransactionReplaced = true;
+    UpdateCacheDisposition(true, false);
+    return NS_OK;
+  }
+
+  LOG(("ProcessNotModified failed [rv=%" PRIx32 "]\n",
+       static_cast<uint32_t>(aRv)));
+
+  // We cannot read from the cache entry, it might be in an
+  // incosistent state.  Doom it and redirect the channel
+  // to the same URI to reload from the network.
+  mCacheInputStream.CloseAndRelease();
+  if (mCacheEntry) {
+    mCacheEntry->AsyncDoom(nullptr);
+    mCacheEntry = nullptr;
+  }
+
+  nsresult rv =
+      StartRedirectChannelToURI(mURI, nsIChannelEventSink::REDIRECT_INTERNAL);
+  if (NS_SUCCEEDED(rv)) {
+    return NS_OK;
+  }
+
+  // Don't cache uninformative 304
+  if (mCustomConditionalRequest) {
+    CloseCacheEntry(false);
+  }
+
+  if (ShouldBypassProcessNotModified() || NS_FAILED(rv)) {
+    rv = ProcessNormal();
+  }
+
+  UpdateCacheDisposition(false, false);
+  return rv;
+}
+
+void nsHttpChannel::UpdateCacheDisposition(bool aSuccessfulReval,
+                                           bool aPartialContentUsed) {
   if (mRaceDelay && !mRaceCacheWithNetwork &&
       (mCachedContentIsPartial || mDidReval)) {
-    if (successfulReval || partialContentUsed) {
+    if (aSuccessfulReval || aPartialContentUsed) {
       AccumulateCategorical(
           Telemetry::LABELS_NETWORK_RACE_CACHE_VALIDATION::CachedContentUsed);
     } else {
       AccumulateCategorical(Telemetry::LABELS_NETWORK_RACE_CACHE_VALIDATION::
                                 CachedContentNotUsed);
     }
   }
 
   if (Telemetry::CanRecordPrereleaseData()) {
     CacheDisposition cacheDisposition;
     if (!mDidReval) {
       cacheDisposition = kCacheMissed;
-    } else if (successfulReval) {
+    } else if (aSuccessfulReval) {
       cacheDisposition = kCacheHitViaReval;
     } else {
       cacheDisposition = kCacheMissedViaReval;
     }
     AccumulateCacheHitTelemetry(cacheDisposition);
     mCacheDisposition = cacheDisposition;
 
     Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_VERSION,
@@ -2651,17 +2701,16 @@ nsresult nsHttpChannel::ContinueProcessR
         v09Info += 1;
       }
       if (mConnectionInfo->OriginPort() != mConnectionInfo->DefaultPort()) {
         v09Info += 2;
       }
       Telemetry::Accumulate(Telemetry::HTTP_09_INFO, v09Info);
     }
   }
-  return rv;
 }
 
 nsresult nsHttpChannel::ContinueProcessResponse3(nsresult rv) {
   bool doNotRender = DoNotRender3xxBody(rv);
 
   if (rv == NS_ERROR_DOM_BAD_URI && mRedirectURI) {
     bool isHTTP = false;
     if (NS_FAILED(mRedirectURI->SchemeIs("http", &isHTTP))) isHTTP = false;
@@ -3262,17 +3311,19 @@ nsresult nsHttpChannel::SetupByteRangeRe
 void nsHttpChannel::UntieByteRangeRequest() {
   DebugOnly<nsresult> rv;
   rv = mRequestHead.ClearHeader(nsHttp::Range);
   MOZ_ASSERT(NS_SUCCEEDED(rv));
   rv = mRequestHead.ClearHeader(nsHttp::If_Range);
   MOZ_ASSERT(NS_SUCCEEDED(rv));
 }
 
-nsresult nsHttpChannel::ProcessPartialContent() {
+nsresult nsHttpChannel::ProcessPartialContent(
+    const std::function<nsresult(nsHttpChannel *, nsresult)>
+        &aContinueProcessResponseFunc) {
   // ok, we've just received a 206
   //
   // we need to stream whatever data is in the cache out first, and then
   // pick up whatever data is on the wire, writing it into the cache.
 
   LOG(("nsHttpChannel::ProcessPartialContent [this=%p]\n", this));
 
   NS_ENSURE_TRUE(mCachedResponseHead, NS_ERROR_NOT_INITIALIZED);
@@ -3362,25 +3413,26 @@ nsresult nsHttpChannel::ProcessPartialCo
   gHttpHandler->OnExamineMergedResponse(this);
 
   if (mConcurrentCacheAccess) {
     mCachedContentIsPartial = false;
     // Leave the mConcurrentCacheAccess flag set, we want to use it
     // to prevent duplicate OnStartRequest call on the target listener
     // in case this channel is canceled before it gets its OnStartRequest
     // from the http transaction.
-
-    // Now we continue reading the network response.
-  } else {
-    // the cached content is valid, although incomplete.
-    mCachedContentIsValid = true;
-    rv = ReadFromCache(false);
-  }
-
-  return rv;
+    return rv;
+  }
+
+  // Now we continue reading the network response.
+  // the cached content is valid, although incomplete.
+  mCachedContentIsValid = true;
+  return CallOrWaitForResume([aContinueProcessResponseFunc](auto *self) {
+    nsresult rv = self->ReadFromCache(false);
+    return aContinueProcessResponseFunc(self, rv);
+  });
 }
 
 nsresult nsHttpChannel::OnDoneReadingPartialCacheEntry(bool *streamDone) {
   nsresult rv;
 
   LOG(("nsHttpChannel::OnDoneReadingPartialCacheEntry [this=%p]", this));
 
   // by default, assume we would have streamed all data or failed...
@@ -3436,17 +3488,19 @@ bool nsHttpChannel::ShouldBypassProcessN
         ("Server returned a 304 response even though we did not send a "
          "conditional request"));
     return true;
   }
 
   return false;
 }
 
-nsresult nsHttpChannel::ProcessNotModified() {
+nsresult nsHttpChannel::ProcessNotModified(
+    const std::function<nsresult(nsHttpChannel *, nsresult)>
+        &aContinueProcessResponseFunc) {
   nsresult rv;
 
   LOG(("nsHttpChannel::ProcessNotModified [this=%p]\n", this));
 
   // Assert ShouldBypassProcessNotModified() has been checked before call to
   // ProcessNotModified().
   MOZ_ASSERT(!ShouldBypassProcessNotModified());
 
@@ -3506,21 +3560,20 @@ nsresult nsHttpChannel::ProcessNotModifi
   gHttpHandler->OnExamineMergedResponse(this);
 
   mCachedContentIsValid = true;
 
   // Tell other consumers the entry is OK to use
   rv = mCacheEntry->SetValid();
   if (NS_FAILED(rv)) return rv;
 
-  rv = ReadFromCache(false);
-  if (NS_FAILED(rv)) return rv;
-
-  mTransactionReplaced = true;
-  return NS_OK;
+  return CallOrWaitForResume([aContinueProcessResponseFunc](auto *self) {
+    nsresult rv = self->ReadFromCache(false);
+    return aContinueProcessResponseFunc(self, rv);
+  });
 }
 
 nsresult nsHttpChannel::ProcessFallback(bool *waitingForRedirectCallback) {
   LOG(("nsHttpChannel::ProcessFallback [this=%p]\n", this));
   nsresult rv;
 
   *waitingForRedirectCallback = false;
   mFallingBack = false;
--- a/netwerk/protocol/http/nsHttpChannel.h
+++ b/netwerk/protocol/http/nsHttpChannel.h
@@ -332,22 +332,27 @@ class nsHttpChannel final : public HttpB
   MOZ_MUST_USE nsresult Connect();
   void SpeculativeConnect();
   MOZ_MUST_USE nsresult SetupTransaction();
   MOZ_MUST_USE nsresult CallOnStartRequest();
   MOZ_MUST_USE nsresult ProcessResponse();
   void AsyncContinueProcessResponse();
   MOZ_MUST_USE nsresult ContinueProcessResponse1();
   MOZ_MUST_USE nsresult ContinueProcessResponse2(nsresult);
+  void UpdateCacheDisposition(bool aSuccessfulReval, bool aPartialContentUsed);
   MOZ_MUST_USE nsresult ContinueProcessResponse3(nsresult);
   MOZ_MUST_USE nsresult ProcessNormal();
   MOZ_MUST_USE nsresult ContinueProcessNormal(nsresult);
   void ProcessAltService();
   bool ShouldBypassProcessNotModified();
-  MOZ_MUST_USE nsresult ProcessNotModified();
+  MOZ_MUST_USE nsresult ProcessNotModified(
+      const std::function<nsresult(nsHttpChannel *, nsresult)>
+          &aContinueProcessResponseFunc);
+  MOZ_MUST_USE nsresult ContinueProcessResponseAfterNotModified(nsresult aRv);
+
   MOZ_MUST_USE nsresult AsyncProcessRedirection(uint32_t httpStatus);
   MOZ_MUST_USE nsresult ContinueProcessRedirection(nsresult);
   MOZ_MUST_USE nsresult ContinueProcessRedirectionAfterFallback(nsresult);
   MOZ_MUST_USE nsresult ProcessFailedProxyConnect(uint32_t httpStatus);
   MOZ_MUST_USE nsresult ProcessFallback(bool *waitingForRedirectCallback);
   MOZ_MUST_USE nsresult ContinueProcessFallback(nsresult);
   void HandleAsyncAbort();
   MOZ_MUST_USE nsresult EnsureAssocReq();
@@ -409,17 +414,21 @@ class nsHttpChannel final : public HttpB
   MOZ_MUST_USE nsresult InstallOfflineCacheListener(int64_t offset = 0);
   void MaybeInvalidateCacheEntryForSubsequentGet();
   void AsyncOnExamineCachedResponse();
 
   // Handle the bogus Content-Encoding Apache sometimes sends
   void ClearBogusContentEncodingIfNeeded();
 
   // byte range request specific methods
-  MOZ_MUST_USE nsresult ProcessPartialContent();
+  MOZ_MUST_USE nsresult ProcessPartialContent(
+      const std::function<nsresult(nsHttpChannel *, nsresult)>
+          &aContinueProcessResponseFunc);
+  MOZ_MUST_USE nsresult
+  ContinueProcessResponseAfterPartialContent(nsresult aRv);
   MOZ_MUST_USE nsresult OnDoneReadingPartialCacheEntry(bool *streamDone);
 
   MOZ_MUST_USE nsresult
   DoAuthRetry(nsAHttpConnection *,
               const std::function<nsresult(nsHttpChannel *, nsresult)> &aOuter);
   MOZ_MUST_USE nsresult ContinueDoAuthRetry(
       nsAHttpConnection *aConn,
       const std::function<nsresult(nsHttpChannel *, nsresult)> &aOuter);
new file mode 100644
--- /dev/null
+++ b/netwerk/test/unit/test_suspend_channel_on_examine_merged_response.js
@@ -0,0 +1,193 @@
+/* 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/. */
+
+// This file tests async handling of a channel suspended in
+// notifying http-on-examine-merged-response observers.
+// Note that this test is developed based on test_bug482601.js.
+
+
+ChromeUtils.import("resource://testing-common/httpd.js");
+ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+var httpserv = null;
+var test_nr = 0;
+var buffer = "";
+var observerCalled = false;
+var channelResumed = false;
+
+var observer = {
+  QueryInterface: function (aIID) {
+    if (aIID.equals(Ci.nsISupports) ||
+        aIID.equals(Ci.nsIObserver))
+      return this;
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+
+  observe: function(subject, topic, data) {
+    if (topic === "http-on-examine-merged-response" &&
+        subject instanceof Ci.nsIHttpChannel) {
+      var chan = subject.QueryInterface(Ci.nsIHttpChannel);
+      executeSoon(() => {
+        Assert.equal(channelResumed,false);
+        channelResumed = true;
+        chan.resume();
+      });
+      Assert.equal(observerCalled,false);
+      observerCalled = true;
+      chan.suspend();
+    }
+  }
+};
+
+var listener = {
+  onStartRequest: function (request, ctx) {
+    buffer = "";
+  },
+
+  onDataAvailable: function (request, ctx, stream, offset, count) {
+    buffer = buffer.concat(read_stream(stream, count));
+  },
+
+  onStopRequest: function (request, ctx, status) {
+    Assert.equal(status, Cr.NS_OK);
+    Assert.equal(buffer, "0123456789");
+    Assert.equal(channelResumed, true);
+    Assert.equal(observerCalled, true);
+    test_nr++;
+    do_timeout(0, do_test);
+  }
+};
+
+function run_test() {
+  httpserv = new HttpServer();
+  httpserv.registerPathHandler("/path/partial", path_partial);
+  httpserv.registerPathHandler("/path/cached", path_cached);
+  httpserv.start(-1);
+
+  var obs = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
+  obs.addObserver(observer, "http-on-examine-merged-response");
+
+  do_timeout(0, do_test);
+  do_test_pending();
+}
+
+function do_test() {
+  if (test_nr < tests.length) {
+    tests[test_nr]();
+  }
+  else {
+    var obs = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
+    obs.removeObserver(observer, "http-on-examine-merged-response");
+    httpserv.stop(do_test_finished);
+  }
+}
+
+var tests = [test_partial,
+             test_cached];
+
+function makeChan(url) {
+  return NetUtil.newChannel({uri: url, loadUsingSystemPrincipal: true})
+                .QueryInterface(Ci.nsIHttpChannel);
+}
+
+function storeCache(aCacheEntry, aResponseHeads, aContent) {
+  aCacheEntry.setMetaDataElement("request-method", "GET");
+  aCacheEntry.setMetaDataElement("response-head", aResponseHeads);
+  aCacheEntry.setMetaDataElement("charset", "ISO-8859-1");
+
+  var oStream = aCacheEntry.openOutputStream(0, aContent.length);
+  var written = oStream.write(aContent, aContent.length);
+  if (written != aContent.length) {
+    do_throw("oStream.write has not written all data!\n" +
+             "  Expected: " + written  + "\n" +
+             "  Actual: " + aContent.length + "\n");
+  }
+  oStream.close();
+  aCacheEntry.close();
+}
+
+function test_partial() {
+  observerCalled = false;
+  channelResumed = false;
+  asyncOpenCacheEntry("http://localhost:" + httpserv.identity.primaryPort +
+                      "/path/partial",
+                      "disk", Ci.nsICacheStorage.OPEN_NORMALLY, null,
+                      test_partial2);
+}
+
+function test_partial2(status, entry) {
+  Assert.equal(status, Cr.NS_OK);
+  storeCache(entry,
+             "HTTP/1.1 200 OK\r\n" +
+             "Date: Thu, 1 Jan 2009 00:00:00 GMT\r\n" +
+             "Server: httpd.js\r\n" +
+             "Last-Modified: Thu, 1 Jan 2009 00:00:00 GMT\r\n" +
+             "Accept-Ranges: bytes\r\n" +
+             "Content-Length: 10\r\n" +
+             "Content-Type: text/plain\r\n",
+             "0123");
+
+  observers_called = "";
+
+  var chan = makeChan("http://localhost:" + httpserv.identity.primaryPort +
+                      "/path/partial");
+  chan.asyncOpen2(listener);
+}
+
+function test_cached() {
+  observerCalled = false;
+  channelResumed = false;
+  asyncOpenCacheEntry("http://localhost:" + httpserv.identity.primaryPort +
+                      "/path/cached",
+                      "disk", Ci.nsICacheStorage.OPEN_NORMALLY, null,
+                      test_cached2);
+}
+
+function test_cached2(status, entry) {
+  Assert.equal(status, Cr.NS_OK);
+  storeCache(entry,
+             "HTTP/1.1 200 OK\r\n" +
+             "Date: Thu, 1 Jan 2009 00:00:00 GMT\r\n" +
+             "Server: httpd.js\r\n" +
+             "Last-Modified: Thu, 1 Jan 2009 00:00:00 GMT\r\n" +
+             "Accept-Ranges: bytes\r\n" +
+             "Content-Length: 10\r\n" +
+             "Content-Type: text/plain\r\n",
+             "0123456789");
+
+  observers_called = "";
+
+  var chan = makeChan("http://localhost:" + httpserv.identity.primaryPort +
+                      "/path/cached");
+  chan.loadFlags = Ci.nsIRequest.VALIDATE_ALWAYS;
+  chan.asyncOpen2(listener);
+}
+
+// PATHS
+
+// /path/partial
+function path_partial(metadata, response) {
+  Assert.ok(metadata.hasHeader("If-Range"));
+  Assert.equal(metadata.getHeader("If-Range"),
+               "Thu, 1 Jan 2009 00:00:00 GMT");
+  Assert.ok(metadata.hasHeader("Range"));
+  Assert.equal(metadata.getHeader("Range"), "bytes=4-");
+
+  response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
+  response.setHeader("Content-Range", "bytes 4-9/10", false);
+  response.setHeader("Content-Type", "text/plain", false);
+  response.setHeader("Last-Modified", "Thu, 1 Jan 2009 00:00:00 GMT");
+
+  var body = "456789";
+  response.bodyOutputStream.write(body, body.length);
+}
+
+// /path/cached
+function path_cached(metadata, response) {
+  Assert.ok(metadata.hasHeader("If-Modified-Since"));
+  Assert.equal(metadata.getHeader("If-Modified-Since"),
+               "Thu, 1 Jan 2009 00:00:00 GMT");
+
+  response.setStatusLine(metadata.httpVersion, 304, "Not Modified");
+}
--- a/netwerk/test/unit/xpcshell.ini
+++ b/netwerk/test/unit/xpcshell.ini
@@ -421,8 +421,9 @@ skip-if = os == "android"
 skip-if = os == "android" # CP service is disabled on Android
 run-sequentially = node server exceptions dont replay well
 [test_esni_dns_fetch.js]
 # http2-using tests require node available
 skip-if = os == "android"
 [test_network_connectivity_service.js]
 skip-if = os == "android" # DNSv6 issues on android
 [test_suspend_channel_on_authRetry.js]
+[test_suspend_channel_on_examine_merged_response.js]