Bug 1264177 - Implement FetchEvent.resultingClientId r=edenchuang,mrbkap
☠☠ backed out by 8bb9edd7ca13 ☠ ☠
authorPerry Jiang <proxy@perryjiang.com>
Mon, 12 Nov 2018 20:10:41 +0000
changeset 445916 6ad8b10cc0d6951618ca814d87a28ac651f1f333
parent 445915 8e4063938d4ba12e1c2c888b902c159534a83202
child 445917 7566a8c23a4a96a2698b4c5af9fd776b5457f4e8
push id35029
push usercsabou@mozilla.com
push dateTue, 13 Nov 2018 04:22:06 +0000
treeherdermozilla-central@d056f0ff51c0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersedenchuang, mrbkap
bugs1264177
milestone65.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 1264177 - Implement FetchEvent.resultingClientId r=edenchuang,mrbkap - Expose FetchEvent.resultingClientId on non-subresource, non-"report"-destination requests. - Delay Clients.get(FetchEvent.resultingClientId) resolution until the resulting client is execution ready. - Add WPTs to test for existence of resultingClientId and Clients.get promise resolution values. Differential Revision: https://phabricator.services.mozilla.com/D5333
dom/clients/manager/ClientManagerService.cpp
dom/clients/manager/ClientSourceParent.cpp
dom/clients/manager/ClientSourceParent.h
dom/serviceworkers/ServiceWorkerEvents.cpp
dom/serviceworkers/ServiceWorkerEvents.h
dom/serviceworkers/ServiceWorkerManager.cpp
dom/serviceworkers/ServiceWorkerPrivate.cpp
dom/serviceworkers/ServiceWorkerPrivate.h
dom/webidl/FetchEvent.webidl
testing/web-platform/tests/service-workers/service-worker/clients-get.https.html
testing/web-platform/tests/service-workers/service-worker/fetch-event.https.html
testing/web-platform/tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js
testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-test-worker.js
--- a/dom/clients/manager/ClientManagerService.cpp
+++ b/dom/clients/manager/ClientManagerService.cpp
@@ -13,17 +13,19 @@
 #include "ClientPrincipalUtils.h"
 #include "ClientSourceParent.h"
 #include "mozilla/dom/ContentParent.h"
 #include "mozilla/dom/ServiceWorkerManager.h"
 #include "mozilla/dom/ServiceWorkerUtils.h"
 #include "mozilla/ipc/BackgroundParent.h"
 #include "mozilla/ipc/PBackgroundSharedTypes.h"
 #include "mozilla/ClearOnShutdown.h"
+#include "mozilla/MozPromise.h"
 #include "mozilla/SystemGroup.h"
+#include "jsfriendapi.h"
 #include "nsIAsyncShutdown.h"
 #include "nsIXULRuntime.h"
 #include "nsProxyRelease.h"
 
 namespace mozilla {
 namespace dom {
 
 using mozilla::ipc::AssertIsOnBackgroundThread;
@@ -538,21 +540,44 @@ ClientManagerService::Claim(const Client
   promiseList->MaybeFinish();
 
   return promiseList->GetResultPromise();
 }
 
 RefPtr<ClientOpPromise>
 ClientManagerService::GetInfoAndState(const ClientGetInfoAndStateArgs& aArgs)
 {
-  RefPtr<ClientOpPromise> ref;
+  ClientSourceParent* source = FindSource(aArgs.id(), aArgs.principalInfo());
+
+  if (!source) {
+    RefPtr<ClientOpPromise> ref =
+      ClientOpPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+    return ref.forget();
+  }
+
+  if (!source->ExecutionReady()) {
+    RefPtr<ClientManagerService> self = this;
 
-  ClientSourceParent* source = FindSource(aArgs.id(), aArgs.principalInfo());
-  if (!source || !source->ExecutionReady()) {
-    ref = ClientOpPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+    // rejection ultimately converted to `undefined` in Clients::Get
+    RefPtr<ClientOpPromise> ref =
+      source->ExecutionReadyPromise()
+            ->Then(GetCurrentThreadSerialEventTarget(), __func__,
+                   [self, aArgs] () -> RefPtr<ClientOpPromise> {
+                      ClientSourceParent* source = self->FindSource(aArgs.id(),
+                                                                    aArgs.principalInfo());
+
+                      if (!source) {
+                        RefPtr<ClientOpPromise> ref =
+                          ClientOpPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+                        return ref.forget();
+                      }
+
+                      return source->StartOp(aArgs);
+                   });
+
     return ref.forget();
   }
 
   return source->StartOp(aArgs);
 }
 
 namespace {
 
--- a/dom/clients/manager/ClientSourceParent.cpp
+++ b/dom/clients/manager/ClientSourceParent.cpp
@@ -113,16 +113,18 @@ ClientSourceParent::RecvExecutionReady(c
   mClientInfo.SetURL(aArgs.url());
   mClientInfo.SetFrameType(aArgs.frameType());
   mExecutionReady = true;
 
   for (ClientHandleParent* handle : mHandleList) {
     Unused << handle->SendExecutionReady(mClientInfo.ToIPC());
   }
 
+  mExecutionReadyPromise.ResolveIfExists(true, __func__);
+
   return IPC_OK();
 };
 
 IPCResult
 ClientSourceParent::RecvFreeze()
 {
   MOZ_DIAGNOSTIC_ASSERT(!mFrozen);
   mFrozen = true;
@@ -223,16 +225,18 @@ ClientSourceParent::ClientSourceParent(c
   , mExecutionReady(false)
   , mFrozen(false)
 {
 }
 
 ClientSourceParent::~ClientSourceParent()
 {
   MOZ_DIAGNOSTIC_ASSERT(mHandleList.IsEmpty());
+
+  mExecutionReadyPromise.RejectIfExists(NS_ERROR_FAILURE, __func__);
 }
 
 void
 ClientSourceParent::Init()
 {
   // Ensure the principal is reasonable before adding ourself to the service.
   // Since we validate the principal on the child side as well, any failure
   // here is treated as fatal.
@@ -263,16 +267,25 @@ ClientSourceParent::IsFrozen() const
 }
 
 bool
 ClientSourceParent::ExecutionReady() const
 {
   return mExecutionReady;
 }
 
+RefPtr<GenericPromise>
+ClientSourceParent::ExecutionReadyPromise()
+{
+  // Only call if ClientSourceParent::ExecutionReady() is false; otherwise,
+  // the promise will never resolve
+  MOZ_ASSERT(!mExecutionReady);
+  return mExecutionReadyPromise.Ensure(__func__);
+}
+
 const Maybe<ServiceWorkerDescriptor>&
 ClientSourceParent::GetController() const
 {
   return mController;
 }
 
 void
 ClientSourceParent::ClearController()
--- a/dom/clients/manager/ClientSourceParent.h
+++ b/dom/clients/manager/ClientSourceParent.h
@@ -5,29 +5,31 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 #ifndef _mozilla_dom_ClientSourceParent_h
 #define _mozilla_dom_ClientSourceParent_h
 
 #include "ClientInfo.h"
 #include "ClientOpPromise.h"
 #include "mozilla/dom/PClientSourceParent.h"
 #include "mozilla/dom/ServiceWorkerDescriptor.h"
+#include "mozilla/MozPromise.h"
 
 namespace mozilla {
 namespace dom {
 
 class ClientHandleParent;
 class ClientManagerService;
 
 class ClientSourceParent final : public PClientSourceParent
 {
   ClientInfo mClientInfo;
   Maybe<ServiceWorkerDescriptor> mController;
   RefPtr<ClientManagerService> mService;
   nsTArray<ClientHandleParent*> mHandleList;
+  MozPromiseHolder<GenericPromise> mExecutionReadyPromise;
   bool mExecutionReady;
   bool mFrozen;
 
   void
   KillInvalidChild();
 
   // PClientSourceParent
   mozilla::ipc::IPCResult
@@ -71,16 +73,19 @@ public:
   Info() const;
 
   bool
   IsFrozen() const;
 
   bool
   ExecutionReady() const;
 
+  RefPtr<GenericPromise>
+  ExecutionReadyPromise();
+
   const Maybe<ServiceWorkerDescriptor>&
   GetController() const;
 
   void
   ClearController();
 
   void
   AttachHandle(ClientHandleParent* aClientSource);
--- a/dom/serviceworkers/ServiceWorkerEvents.cpp
+++ b/dom/serviceworkers/ServiceWorkerEvents.cpp
@@ -156,16 +156,17 @@ FetchEvent::Constructor(const GlobalObje
   MOZ_ASSERT(owner);
   RefPtr<FetchEvent> e = new FetchEvent(owner);
   bool trusted = e->Init(owner);
   e->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable);
   e->SetTrusted(trusted);
   e->SetComposed(aOptions.mComposed);
   e->mRequest = aOptions.mRequest;
   e->mClientId = aOptions.mClientId;
+  e->mResultingClientId = aOptions.mResultingClientId;
   e->mIsReload = aOptions.mIsReload;
   return e.forget();
 }
 
 namespace {
 
 struct RespondWithClosure
 {
--- a/dom/serviceworkers/ServiceWorkerEvents.h
+++ b/dom/serviceworkers/ServiceWorkerEvents.h
@@ -118,16 +118,17 @@ public:
 class FetchEvent final : public ExtendableEvent
 {
   nsMainThreadPtrHandle<nsIInterceptedChannel> mChannel;
   nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration;
   RefPtr<Request> mRequest;
   nsCString mScriptSpec;
   nsCString mPreventDefaultScriptSpec;
   nsString mClientId;
+  nsString mResultingClientId;
   uint32_t mPreventDefaultLineNumber;
   uint32_t mPreventDefaultColumnNumber;
   bool mIsReload;
   bool mWaitToRespond;
 protected:
   explicit FetchEvent(EventTarget* aOwner);
   ~FetchEvent();
 
@@ -164,16 +165,22 @@ public:
   }
 
   void
   GetClientId(nsAString& aClientId) const
   {
     aClientId = mClientId;
   }
 
+  void
+  GetResultingClientId(nsAString& aResultingClientId) const
+  {
+    aResultingClientId = mResultingClientId;
+  }
+
   bool
   IsReload() const
   {
     return mIsReload;
   }
 
   void
   RespondWith(JSContext* aCx, Promise& aArg, ErrorResult& aRv);
--- a/dom/serviceworkers/ServiceWorkerManager.cpp
+++ b/dom/serviceworkers/ServiceWorkerManager.cpp
@@ -2013,31 +2013,53 @@ public:
     nsresult status;
     rv = channel->GetStatus(&status);
     if (NS_WARN_IF(NS_FAILED(rv) || NS_FAILED(status))) {
       HandleError();
       return NS_OK;
     }
 
     nsString clientId;
+    nsString resultingClientId;
     nsCOMPtr<nsILoadInfo> loadInfo = channel->GetLoadInfo();
     if (loadInfo) {
+      char buf[NSID_LENGTH];
       Maybe<ClientInfo> clientInfo = loadInfo->GetClientInfo();
       if (clientInfo.isSome()) {
-        char buf[NSID_LENGTH];
         clientInfo.ref().Id().ToProvidedString(buf);
         NS_ConvertASCIItoUTF16 uuid(buf);
 
         // Remove {} and the null terminator
         clientId.Assign(Substring(uuid, 1, NSID_LENGTH - 3));
       }
+
+      // Having an initial or reserved client are mutually exclusive events:
+      // either an initial client is used upon navigating an about:blank
+      // iframe, or a new, reserved environment/client is created (e.g.
+      // upon a top-level navigation). See step 4 of
+      // https://html.spec.whatwg.org/#process-a-navigate-fetch as well as
+      // https://github.com/w3c/ServiceWorker/issues/1228#issuecomment-345132444
+      Maybe<ClientInfo> resulting = loadInfo->GetInitialClientInfo();
+
+      if (resulting.isNothing()) {
+        resulting = loadInfo->GetReservedClientInfo();
+      } else {
+        MOZ_ASSERT(loadInfo->GetReservedClientInfo().isNothing());
+      }
+
+      if (resulting.isSome()) {
+        resulting.ref().Id().ToProvidedString(buf);
+        NS_ConvertASCIItoUTF16 uuid(buf);
+
+        resultingClientId.Assign(Substring(uuid, 1, NSID_LENGTH - 3));
+      }
     }
 
     rv = mServiceWorkerPrivate->SendFetchEvent(mChannel, mLoadGroup, clientId,
-                                               mIsReload);
+                                               resultingClientId, mIsReload);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       HandleError();
     }
 
     return NS_OK;
   }
 };
 
--- a/dom/serviceworkers/ServiceWorkerPrivate.cpp
+++ b/dom/serviceworkers/ServiceWorkerPrivate.cpp
@@ -1304,16 +1304,17 @@ class FetchEventRunnable : public Extend
   nsMainThreadPtrHandle<nsIInterceptedChannel> mInterceptedChannel;
   const nsCString mScriptSpec;
   nsTArray<nsCString> mHeaderNames;
   nsTArray<nsCString> mHeaderValues;
   nsCString mSpec;
   nsCString mFragment;
   nsCString mMethod;
   nsString mClientId;
+  nsString mResultingClientId;
   bool mIsReload;
   bool mMarkLaunchServiceWorkerEnd;
   RequestCache mCacheMode;
   RequestMode mRequestMode;
   RequestRedirect mRequestRedirect;
   RequestCredentials mRequestCredentials;
   nsContentPolicyType mContentPolicyType;
   nsCOMPtr<nsIInputStream> mUploadStream;
@@ -1325,23 +1326,25 @@ public:
   FetchEventRunnable(WorkerPrivate* aWorkerPrivate,
                      KeepAliveToken* aKeepAliveToken,
                      nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel,
                      // CSP checks might require the worker script spec
                      // later on.
                      const nsACString& aScriptSpec,
                      nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration,
                      const nsAString& aClientId,
+                     const nsAString& aResultingClientId,
                      bool aIsReload,
                      bool aMarkLaunchServiceWorkerEnd)
     : ExtendableFunctionalEventWorkerRunnable(
         aWorkerPrivate, aKeepAliveToken, aRegistration)
     , mInterceptedChannel(aChannel)
     , mScriptSpec(aScriptSpec)
     , mClientId(aClientId)
+    , mResultingClientId(aResultingClientId)
     , mIsReload(aIsReload)
     , mMarkLaunchServiceWorkerEnd(aMarkLaunchServiceWorkerEnd)
     , mCacheMode(RequestCache::Default)
     , mRequestMode(RequestMode::No_cors)
     , mRequestRedirect(RequestRedirect::Follow)
     // By default we set it to same-origin since normal HTTP fetches always
     // send credentials to same-origin websites unless explicitly forbidden.
     , mRequestCredentials(RequestCredentials::Same_origin)
@@ -1619,22 +1622,36 @@ private:
     MOZ_ASSERT_IF(internalReq->IsNavigationRequest(),
                   request->Redirect() == RequestRedirect::Manual);
 
     RootedDictionary<FetchEventInit> init(aCx);
     init.mRequest = request;
     init.mBubbles = false;
     init.mCancelable = true;
     // Only expose the FetchEvent.clientId on subresource requests for now.
-    // Once we implement .resultingClientId and .targetClientId we can then
-    // start exposing .clientId on non-subresource requests as well.  See
-    // bug 1264177.
+    // Once we implement .targetClientId we can then start exposing .clientId
+    // on non-subresource requests as well.  See bug 1487534.
     if (!mClientId.IsEmpty() && !internalReq->IsNavigationRequest()) {
       init.mClientId = mClientId;
     }
+
+    /*
+     * https://w3c.github.io/ServiceWorker/#on-fetch-request-algorithm
+     *
+     * "If request is a non-subresource request and request’s
+     * destination is not "report", initialize e’s resultingClientId attribute
+     * to reservedClient’s [resultingClient's] id, and to the empty string
+     * otherwise." (Step 18.8)
+     */
+    if (!mResultingClientId.IsEmpty() &&
+        nsContentUtils::IsNonSubresourceRequest(channel) &&
+        internalReq->Destination() != RequestDestination::Report) {
+      init.mResultingClientId = mResultingClientId;
+    }
+
     init.mIsReload = mIsReload;
     RefPtr<FetchEvent> event =
       FetchEvent::Constructor(globalObj, NS_LITERAL_STRING("fetch"), init, result);
     if (NS_WARN_IF(result.Failed())) {
       result.SuppressException();
       return false;
     }
 
@@ -1668,17 +1685,19 @@ private:
 
 NS_IMPL_ISUPPORTS_INHERITED(FetchEventRunnable, WorkerRunnable, nsIHttpHeaderVisitor)
 
 } // anonymous namespace
 
 nsresult
 ServiceWorkerPrivate::SendFetchEvent(nsIInterceptedChannel* aChannel,
                                      nsILoadGroup* aLoadGroup,
-                                     const nsAString& aClientId, bool aIsReload)
+                                     const nsAString& aClientId,
+                                     const nsAString& aResultingClientId,
+                                     bool aIsReload)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
   if (NS_WARN_IF(!mInfo || !swm)) {
     return NS_ERROR_FAILURE;
   }
 
@@ -1737,17 +1756,18 @@ ServiceWorkerPrivate::SendFetchEvent(nsI
       "ServiceWorkerRegistrationInfoProxy", registration, false));
 
   RefPtr<KeepAliveToken> token = CreateEventKeepAliveToken();
 
 
   RefPtr<FetchEventRunnable> r =
     new FetchEventRunnable(mWorkerPrivate, token, handle,
                            mInfo->ScriptSpec(), regInfo,
-                           aClientId, aIsReload, newWorkerCreated);
+                           aClientId, aResultingClientId,
+                           aIsReload, newWorkerCreated);
   rv = r->Init();
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   if (mInfo->State() == ServiceWorkerState::Activating) {
     mPendingFunctionalEvents.AppendElement(r.forget());
     return NS_OK;
--- a/dom/serviceworkers/ServiceWorkerPrivate.h
+++ b/dom/serviceworkers/ServiceWorkerPrivate.h
@@ -120,18 +120,21 @@ public:
                         const nsAString& aBody,
                         const nsAString& aTag,
                         const nsAString& aIcon,
                         const nsAString& aData,
                         const nsAString& aBehavior,
                         const nsAString& aScope);
 
   nsresult
-  SendFetchEvent(nsIInterceptedChannel* aChannel, nsILoadGroup* aLoadGroup,
-                 const nsAString& aClientId, bool aIsReload);
+  SendFetchEvent(nsIInterceptedChannel* aChannel,
+                 nsILoadGroup* aLoadGroup,
+                 const nsAString& aClientId,
+                 const nsAString& aResultingClientId,
+                 bool aIsReload);
 
   bool
   MaybeStoreISupports(nsISupports* aSupports);
 
   void
   RemoveISupports(nsISupports* aSupports);
 
   // This will terminate the current running worker thread and drop the
--- a/dom/webidl/FetchEvent.webidl
+++ b/dom/webidl/FetchEvent.webidl
@@ -8,19 +8,21 @@
  */
 
 [Constructor(DOMString type, FetchEventInit eventInitDict),
  Func="ServiceWorkerVisible",
  Exposed=(ServiceWorker)]
 interface FetchEvent : ExtendableEvent {
   [SameObject] readonly attribute Request request;
   readonly attribute DOMString clientId;
+  readonly attribute DOMString resultingClientId;
   readonly attribute boolean isReload;
 
   [Throws]
   void respondWith(Promise<Response> r);
 };
 
 dictionary FetchEventInit : EventInit {
   required Request request;
   DOMString clientId = "";
+  DOMString resultingClientId = "";
   boolean isReload = false;
 };
--- a/testing/web-platform/tests/service-workers/service-worker/clients-get.https.html
+++ b/testing/web-platform/tests/service-workers/service-worker/clients-get.https.html
@@ -44,16 +44,172 @@ promise_test(function(t) {
       .then(function(e) {
           assert_equals(e.data.length, 3);
           assert_array_equals(e.data[0], expected[0]);
           assert_array_equals(e.data[1], expected[1]);
           assert_equals(e.data[2], expected[2]);
         });
   }, 'Test Clients.get()');
 
+promise_test((t) => {
+  let frame = null;
+  const scope = 'resources/simple.html';
+  const outerSwContainer = navigator.serviceWorker;
+  let innerSwReg = null;
+  let innerSw = null;
+
+  return service_worker_unregister_and_register(
+    t, 'resources/clients-get-resultingClientId-worker.js', scope)
+    .then((registration) => {
+        innerSwReg = registration;
+        add_completion_callback(function() { registration.unregister(); });
+        return wait_for_state(t, registration.installing, 'activated');
+    })
+    .then(() => {
+        // load frame and get resulting client id
+      let channel = new MessageChannel();
+      innerSw = innerSwReg.active;
+
+      let p = new Promise(resolve => {
+        function getResultingClientId(e) {
+          if (e.data.msg == 'getResultingClientId') {
+            const { resultingClientId } = e.data;
+
+            channel.port1.removeEventListener('message', getResultingClientId);
+
+            resolve({ resultingClientId, port: channel.port1 });
+          }
+        }
+
+        channel.port1.onmessage = getResultingClientId;
+      });
+
+
+      return with_iframe(scope).then((iframe) => {
+        innerSw.postMessage(
+          { port: channel.port2, msg: 'getResultingClientId' },
+          [channel.port2],
+        );
+
+        frame = iframe;
+        frame.focus();
+        add_completion_callback(() => iframe.remove());
+
+        return p;
+      });
+    })
+    .then(({ resultingClientId, port }) => {
+      // query service worker for clients.get(resultingClientId)
+      let channel = new MessageChannel();
+
+      let p = new Promise(resolve => {
+        function getIsResultingClientUndefined(e) {
+          if (e.data.msg == 'getIsResultingClientUndefined') {
+            let { isResultingClientUndefined } = e.data;
+
+            port.removeEventListener('message', getIsResultingClientUndefined);
+
+            resolve(isResultingClientUndefined);
+          }
+        }
+
+        port.onmessage = getIsResultingClientUndefined;
+      });
+
+      innerSw.postMessage(
+        { port: channel.port2, msg: 'getIsResultingClientUndefined', resultingClientId },
+        [channel.port2],
+      );
+
+      return p;
+    })
+    .then((isResultingClientUndefined) => {
+      assert_false(isResultingClientUndefined, 'Clients.get(FetchEvent.resultingClientId) resolved with a Client');
+    });
+}, 'Test successful Clients.get(FetchEvent.resultingClientId)');
+
+promise_test((t) => {
+  const scope = 'resources/simple.html?fail';
+  const outerSwContainer = navigator.serviceWorker;
+  let innerSwReg = null;
+  let innerSw = null;
+
+  return service_worker_unregister_and_register(
+    t, 'resources/clients-get-resultingClientId-worker.js', scope)
+    .then((registration) => {
+        innerSwReg = registration;
+        add_completion_callback(function() { registration.unregister(); });
+        return wait_for_state(t, registration.installing, 'activated');
+    })
+    .then(() => {
+        // load frame, destroying it while loading, and get resulting client id
+        innerSw = innerSwReg.active;
+
+        let iframe = document.createElement('iframe');
+        iframe.className = 'test-iframe';
+        iframe.src = scope;
+
+        function destroyIframe(e) {
+          if (e.data.msg == 'destroyResultingClient') {
+            iframe.remove();
+            iframe = null;
+
+            innerSw.postMessage({ msg: 'resultingClientDestroyed' });
+          }
+        }
+
+        outerSwContainer.addEventListener('message', destroyIframe);
+
+        let p = new Promise(resolve => {
+          function resultingClientDestroyedAck(e) {
+            if (e.data.msg == 'resultingClientDestroyedAck') {
+              let { resultingDestroyedClientId } = e.data;
+
+              outerSwContainer.removeEventListener('message', resultingClientDestroyedAck);
+              resolve(resultingDestroyedClientId);
+            }
+          }
+
+          outerSwContainer.addEventListener('message', resultingClientDestroyedAck);
+        });
+
+        document.body.appendChild(iframe);
+
+        return p;
+    })
+    .then((resultingDestroyedClientId) => {
+        // query service worker for clients.get(resultingDestroyedClientId)
+        let channel = new MessageChannel();
+
+        let p = new Promise((resolve, reject) => {
+          function getIsResultingClientUndefined(e) {
+            if (e.data.msg == 'getIsResultingClientUndefined') {
+              let { isResultingClientUndefined } = e.data;
+
+              channel.port1.removeEventListener('message', getIsResultingClientUndefined);
+
+              resolve(isResultingClientUndefined);
+            }
+          }
+
+          channel.port1.onmessage = getIsResultingClientUndefined;
+        });
+
+        innerSw.postMessage(
+          { port: channel.port2, msg: 'getIsResultingClientUndefined', resultingClientId: resultingDestroyedClientId },
+          [channel.port2],
+        );
+
+        return p;
+    })
+    .then((isResultingClientUndefined) => {
+      assert_true(isResultingClientUndefined, 'Clients.get(FetchEvent.resultingClientId) resolved with `undefined`');
+    });
+}, 'Test unsuccessful Clients.get(FetchEvent.resultingClientId)');
+
 function wait_for_clientId() {
   return new Promise(function(resolve, reject) {
       function get_client_id(e) {
         window.removeEventListener('message', get_client_id);
         resolve(e.data.clientId);
       }
       window.addEventListener('message', get_client_id, false);
     });
--- a/testing/web-platform/tests/service-workers/service-worker/fetch-event.https.html
+++ b/testing/web-platform/tests/service-workers/service-worker/fetch-event.https.html
@@ -111,25 +111,46 @@ promise_test(t => {
           assert_equals(
             frame.contentDocument.body.textContent,
             'Client ID Not Found',
             'Service Worker should respond to fetch with a client id');
           return frame.contentWindow.fetch('resources/other.html?clientId');
         })
       .then(function(response) { return response.text(); })
       .then(function(response_text) {
-          var new_client_id = response_text.substr(17);
           assert_equals(
             response_text.substr(0, 15),
             'Client ID Found',
             'Service Worker should respond to fetch with an existing client id');
         });
   }, 'Service Worker responds to fetch event with an existing client id');
 
 promise_test(t => {
+    const page_url = 'resources/simple.html?resultingClientId';
+    const expected_found = 'Resulting Client ID Found';
+    const expected_not_found = 'Resulting Client ID Not Found';
+    return with_iframe(page_url)
+      .then(function(frame) {
+          t.add_cleanup(() => { frame.remove(); });
+          assert_equals(
+            frame.contentDocument.body.textContent.substr(0, expected_found.length),
+            expected_found,
+            'Service Worker should respond with an existing resulting client id for non-subresource requests');
+          return frame.contentWindow.fetch('resources/other.html?resultingClientId');
+        })
+      .then(function(response) { return response.text(); })
+      .then(function(response_text) {
+          assert_equals(
+            response_text.substr(0),
+            expected_not_found,
+            'Service Worker should respond with an empty resulting client id for subresource requests');
+        });
+  }, 'Service Worker responds to fetch event with the correct resulting client id');
+
+promise_test(t => {
     const page_url = 'resources/simple.html?ignore';
     return with_iframe(page_url)
       .then(function(frame) {
           t.add_cleanup(() => { frame.remove(); });
           assert_equals(frame.contentDocument.body.textContent,
                         'Here\'s a simple html file.\n',
                         'Response should come from fallback to native fetch');
         });
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js
@@ -0,0 +1,64 @@
+let savedPort = null;
+let savedResultingClientId = null;
+
+async function destroyResultingClient(e) {
+  const outer = await self.clients.matchAll({ type: 'window', includeUncontrolled: true })
+    .then((clientList) => {
+    for (let c of clientList) {
+      if (c.url.endsWith('clients-get.https.html')) {
+        c.focus();
+        return c;
+      }
+    }
+  });
+
+  const p = new Promise(resolve => {
+    function resultingClientDestroyed(evt) {
+      if (evt.data.msg == 'resultingClientDestroyed') {
+        self.removeEventListener('message', resultingClientDestroyed);
+        resolve(outer);
+      }
+    }
+
+    self.addEventListener('message', resultingClientDestroyed);
+  });
+
+  outer.postMessage({ msg: 'destroyResultingClient' });
+
+  return await p;
+}
+
+self.addEventListener('fetch', async (e) => {
+  let { resultingClientId } = e;
+  savedResultingClientId = resultingClientId;
+
+  if (e.request.url.endsWith('simple.html?fail')) {
+    e.waitUntil(new Promise(async (resolve) => {
+        let outer = await destroyResultingClient(e);
+
+        outer.postMessage({ msg: 'resultingClientDestroyedAck',
+                            resultingDestroyedClientId: savedResultingClientId });
+        resolve();
+    }));
+  } else {
+    e.respondWith(fetch(e.request));
+  }
+});
+
+self.addEventListener('message', (e) => {
+  let { msg, port, resultingClientId } = e.data;
+  savedPort = savedPort || port;
+
+  if (msg == 'getIsResultingClientUndefined') {
+    self.clients.get(resultingClientId).then((client) => {
+      let isUndefined = typeof client == 'undefined';
+      savedPort.postMessage({ msg: 'getIsResultingClientUndefined',
+        isResultingClientUndefined: isUndefined });
+    });
+  }
+
+  if (msg == 'getResultingClientId') {
+    savedPort.postMessage({ msg: 'getResultingClientId',
+      resultingClientId: savedResultingClientId });
+  }
+});
--- a/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-test-worker.js
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/fetch-event-test-worker.js
@@ -32,16 +32,26 @@ function handleClientId(event) {
   if (event.clientId !== "") {
     body = 'Client ID Found: ' + event.clientId;
   } else {
     body = 'Client ID Not Found';
   }
   event.respondWith(new Response(body));
 }
 
+function handleResultingClientId(event) {
+  var body;
+  if (event.resultingClientId !== "") {
+    body = 'Resulting Client ID Found: ' + event.resultingClientId;
+  } else {
+    body = 'Resulting Client ID Not Found';
+  }
+  event.respondWith(new Response(body));
+}
+
 function handleNullBody(event) {
   event.respondWith(new Response());
 }
 
 function handleFetch(event) {
   event.respondWith(fetch('other.html'));
 }
 
@@ -150,16 +160,17 @@ self.addEventListener('fetch', function(
     var handlers = [
       { pattern: '?headers', fn: handleHeaders },
       { pattern: '?string', fn: handleString },
       { pattern: '?blob', fn: handleBlob },
       { pattern: '?referrerFull', fn: handleReferrerFull },
       { pattern: '?referrerPolicy', fn: handleReferrerPolicy },
       { pattern: '?referrer', fn: handleReferrer },
       { pattern: '?clientId', fn: handleClientId },
+      { pattern: '?resultingClientId', fn: handleResultingClientId },
       { pattern: '?ignore', fn: function() {} },
       { pattern: '?null', fn: handleNullBody },
       { pattern: '?fetch', fn: handleFetch },
       { pattern: '?form-post', fn: handleFormPost },
       { pattern: '?multiple-respond-with', fn: handleMultipleRespondWith },
       { pattern: '?used-check', fn: handleUsedCheck },
       { pattern: '?fragment-check', fn: handleFragmentCheck },
       { pattern: '?cache', fn: handleCache },