Bug 1399446 - implement ServiceWorkerGlobalScope and ServiceWorkerContainer onmessageerror event handlers r=edenchuang,baku
authorPerry Jiang <perry@mozilla.com>
Mon, 22 Oct 2018 21:22:25 +0000
changeset 490844 988f40d0fd2c05a3dc4262d22209f6607c8fefd5
parent 490843 859e7d50fc031e6e19199f6c093e031566cf875a
child 490845 bebe2e4eab255b73ca22ba3b55684fcc0b222d16
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersedenchuang, baku
bugs1399446
milestone65.0a1
Bug 1399446 - implement ServiceWorkerGlobalScope and ServiceWorkerContainer onmessageerror event handlers r=edenchuang,baku Differential Revision: https://phabricator.services.mozilla.com/D5213
dom/serviceworkers/ServiceWorkerContainer.cpp
dom/serviceworkers/ServiceWorkerContainer.h
dom/serviceworkers/ServiceWorkerPrivate.cpp
dom/serviceworkers/test/mochitest.ini
dom/serviceworkers/test/onmessageerror_worker.js
dom/serviceworkers/test/test_onmessageerror.html
dom/webidl/ServiceWorkerContainer.webidl
dom/webidl/ServiceWorkerGlobalScope.webidl
dom/workers/WorkerScope.h
testing/web-platform/meta/service-workers/service-worker/interfaces-sw.https.html.ini
--- a/dom/serviceworkers/ServiceWorkerContainer.cpp
+++ b/dom/serviceworkers/ServiceWorkerContainer.cpp
@@ -713,27 +713,39 @@ ServiceWorkerContainer::DispatchMessage(
   MOZ_ASSERT(NS_IsMainThread());
 
   // When dispatching a message, either DOMContentLoaded has already
   // been fired, or someone called startMessages() or set onmessage.
   // Either way, a global object is supposed to be present. If it's
   // not, we'd fail to initialize the JS API and exit.
   RunWithJSContext([this, message = std::move(aMessage)](JSContext* const aCx,
                                                          nsIGlobalObject* const aGlobal) {
+    ErrorResult result;
+    bool deserializationFailed = false;
     RootedDictionary<MessageEventInit> init(aCx);
-    if (!FillInMessageEventInit(aCx, aGlobal, *message, init)) {
-      // TODO: The spec requires us to fire a messageerror event here.
-      return;
+    if (!FillInMessageEventInit(aCx, aGlobal, *message, init, result)) {
+      deserializationFailed = result.ErrorCodeIs(NS_ERROR_DOM_DATA_CLONE_ERR);
+      MOZ_ASSERT_IF(deserializationFailed, init.mData.isNull());
+      MOZ_ASSERT_IF(deserializationFailed, init.mPorts.IsEmpty());
+      MOZ_ASSERT_IF(deserializationFailed, !init.mOrigin.IsEmpty());
+      MOZ_ASSERT_IF(deserializationFailed, !init.mSource.IsNull());
+
+      if (!deserializationFailed && result.MaybeSetPendingException(aCx)) {
+        return;
+      }
     }
 
     RefPtr<MessageEvent> event =
-      MessageEvent::Constructor(this, NS_LITERAL_STRING("message"), init);
+      MessageEvent::Constructor(this,
+                                deserializationFailed ? NS_LITERAL_STRING("messageerror") :
+                                                        NS_LITERAL_STRING("message"),
+                                init);
     event->SetTrusted(true);
 
-    ErrorResult result;
+    result = NS_OK;
     DispatchEvent(*event, result);
     if (result.Failed()) {
       result.SuppressException();
     }
   });
 }
 
 namespace {
@@ -786,39 +798,49 @@ GetOrCreateServiceWorkerWithoutWarnings(
 }
 
 }
 
 bool
 ServiceWorkerContainer::FillInMessageEventInit(JSContext* const aCx,
                                                nsIGlobalObject* const aGlobal,
                                                ReceivedMessage& aMessage,
-                                               MessageEventInit& aInit)
+                                               MessageEventInit& aInit,
+                                               ErrorResult& aRv)
 {
-  ErrorResult result;
-  JS::Rooted<JS::Value> messageData(aCx);
-  aMessage.mClonedData.Read(aCx, &messageData, result);
-  if (result.Failed()) {
-    return false;
-  }
-
-  aInit.mData = messageData;
-
-  if (!aMessage.mClonedData.TakeTransferredPortsAsSequence(aInit.mPorts)) {
-    return false;
+  // Determining the source and origin should preceed attempting deserialization
+  // because on a "messageerror" event (i.e. when deserialization fails), the
+  // dispatched message needs to contain such an origin and source, per spec:
+  //
+  // "If this throws an exception, catch it, fire an event named messageerror
+  //  at destination, using MessageEvent, with the origin attribute initialized
+  //  to origin and the source attribute initialized to source, and then abort
+  //  these steps." - 6.4 of postMessage
+  //  See: https://w3c.github.io/ServiceWorker/#service-worker-postmessage
+  const RefPtr<ServiceWorker> serviceWorkerInstance =
+    GetOrCreateServiceWorkerWithoutWarnings(aGlobal, aMessage.mServiceWorker);
+  if (serviceWorkerInstance) {
+    aInit.mSource.SetValue().SetAsServiceWorker() = serviceWorkerInstance;
   }
 
   const nsresult rv = FillInOriginNoSuffix(aMessage.mServiceWorker, aInit.mOrigin);
   if (NS_FAILED(rv)) {
     return false;
   }
 
-  const RefPtr<ServiceWorker> serviceWorkerInstance =
-    GetOrCreateServiceWorkerWithoutWarnings(aGlobal, aMessage.mServiceWorker);
-  if (serviceWorkerInstance) {
-    aInit.mSource.SetValue().SetAsServiceWorker() = serviceWorkerInstance;
+  JS::Rooted<JS::Value> messageData(aCx);
+  aMessage.mClonedData.Read(aCx, &messageData, aRv);
+  if (aRv.Failed()) {
+    return false;
+  }
+
+  aInit.mData = messageData;
+
+  if (!aMessage.mClonedData.TakeTransferredPortsAsSequence(aInit.mPorts)) {
+    xpc::Throw(aCx, NS_ERROR_OUT_OF_MEMORY);
+    return false;
   }
 
   return true;
 }
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/serviceworkers/ServiceWorkerContainer.h
+++ b/dom/serviceworkers/ServiceWorkerContainer.h
@@ -61,17 +61,18 @@ public:
     NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
   };
 
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ServiceWorkerContainer, DOMEventTargetHelper)
 
   IMPL_EVENT_HANDLER(controllerchange)
   IMPL_EVENT_HANDLER(error)
-  
+  IMPL_EVENT_HANDLER(messageerror)
+
   // Almost a manual expansion of IMPL_EVENT_HANDLER(message), but
   // with the additional StartMessages() when setting the handler, as
   // required by the spec.
   inline mozilla::dom::EventHandlerNonNull* GetOnmessage()
   {
     return GetEventHandler(nsGkAtoms::onmessage);
   }
   inline void SetOnmessage(mozilla::dom::EventHandlerNonNull* aCallback)
@@ -155,17 +156,18 @@ private:
 
   void
   DispatchMessage(RefPtr<ReceivedMessage> aMessage);
 
   static bool
   FillInMessageEventInit(JSContext* aCx,
                          nsIGlobalObject* aGlobal,
                          ReceivedMessage& aMessage,
-                         MessageEventInit& aInit);
+                         MessageEventInit& aInit,
+                         ErrorResult& aRv);
 
   RefPtr<Inner> mInner;
 
   // This only changes when a worker hijacks everything in its scope by calling
   // claim.
   RefPtr<ServiceWorker> mControllerWorker;
 
   RefPtr<Promise> mReadyPromise;
--- a/dom/serviceworkers/ServiceWorkerPrivate.cpp
+++ b/dom/serviceworkers/ServiceWorkerPrivate.cpp
@@ -509,38 +509,50 @@ public:
 
   bool
   WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override
   {
     JS::Rooted<JS::Value> messageData(aCx);
     nsCOMPtr<nsIGlobalObject> sgo = aWorkerPrivate->GlobalScope();
     ErrorResult rv;
     mData->Read(aCx, &messageData, rv);
-    if (NS_WARN_IF(rv.Failed())) {
+
+    // If deserialization fails, we will fire a messageerror event
+    bool deserializationFailed = rv.ErrorCodeIs(NS_ERROR_DOM_DATA_CLONE_ERR);
+
+    if (!deserializationFailed && NS_WARN_IF(rv.Failed())) {
       return true;
     }
 
     Sequence<OwningNonNull<MessagePort>> ports;
     if (!mData->TakeTransferredPortsAsSequence(ports)) {
       return true;
     }
 
     RootedDictionary<ExtendableMessageEventInit> init(aCx);
 
     init.mBubbles = false;
     init.mCancelable = false;
 
-    init.mData = messageData;
-    init.mPorts = ports;
+    // On a messageerror event, we disregard ports:
+    // https://w3c.github.io/ServiceWorker/#service-worker-postmessage
+    if (!deserializationFailed) {
+      init.mData = messageData;
+      init.mPorts = ports;
+    }
+
     init.mSource.SetValue().SetAsClient() =
       new Client(sgo, mClientInfoAndState);
 
+    rv = NS_OK;
     RefPtr<EventTarget> target = aWorkerPrivate->GlobalScope();
     RefPtr<ExtendableMessageEvent> extendableEvent =
-      ExtendableMessageEvent::Constructor(target, NS_LITERAL_STRING("message"),
+      ExtendableMessageEvent::Constructor(target,
+                                          deserializationFailed ? NS_LITERAL_STRING("messageerror") :
+                                                                  NS_LITERAL_STRING("message"),
                                           init, rv);
     if (NS_WARN_IF(rv.Failed())) {
       rv.SuppressException();
       return false;
     }
 
     extendableEvent->SetTrusted(true);
 
--- a/dom/serviceworkers/test/mochitest.ini
+++ b/dom/serviceworkers/test/mochitest.ini
@@ -221,16 +221,17 @@ support-files =
   service_worker_client.html
   utils.js
   bug1290951_worker_main.sjs
   bug1290951_worker_imported.sjs
   sw_storage_not_allow.js
   update_worker.sjs
   self_update_worker.sjs
   !/dom/events/test/event_leak_utils.js
+  onmessageerror_worker.js
 
 [test_bug1151916.html]
 [test_bug1240436.html]
 [test_bug1408734.html]
 [test_claim.html]
 [test_claim_oninstall.html]
 [test_controller.html]
 [test_cookie_fetch.html]
@@ -274,16 +275,17 @@ tags = mcb
 [test_navigator.html]
 [test_not_intercept_plugin.html]
 [test_notification_constructor_error.html]
 [test_notification_get.html]
 [test_notificationclick.html]
 [test_notificationclick_focus.html]
 [test_notificationclick-otherwindow.html]
 [test_notificationclose.html]
+[test_onmessageerror.html]
 [test_opaque_intercept.html]
 [test_openWindow.html]
 tags = openwindow
 [test_origin_after_redirect.html]
 [test_origin_after_redirect_cached.html]
 [test_origin_after_redirect_to_https.html]
 [test_origin_after_redirect_to_https_cached.html]
 [test_post_message.html]
new file mode 100644
--- /dev/null
+++ b/dom/serviceworkers/test/onmessageerror_worker.js
@@ -0,0 +1,49 @@
+async function getSwContainer() {
+  const clients = await self.clients.matchAll({ type: 'window',
+                                                includeUncontrolled: true });
+
+  for (let client of clients) {
+    if (client.url.endsWith('test_onmessageerror.html')) {
+      return client;
+    }
+  }
+}
+
+self.addEventListener('message', async (e) => {
+  const config = e.data;
+  const swContainer = await getSwContainer();
+
+  if (config == 'send-bad-message') {
+    const serializable = true;
+    const deserializable = false;
+
+    swContainer.postMessage(new StructuredCloneTester(serializable, deserializable));
+
+    return;
+  }
+
+  if (!config.serializable) {
+    swContainer.postMessage({
+      result: 'Error',
+      reason: 'Service Worker received an unserializable object',
+    });
+
+    return;
+  }
+
+  if (!config.deserializable) {
+    swContainer.postMessage({
+      result: 'Error',
+      reason: 'Service Worker received (and deserialized) an un-deserializable object',
+    });
+
+    return;
+  }
+
+  swContainer.postMessage({ received: 'message' });
+});
+
+self.addEventListener('messageerror', async () => {
+  const swContainer = await getSwContainer();
+  swContainer.postMessage({ received: 'messageerror' });
+});
new file mode 100644
--- /dev/null
+++ b/dom/serviceworkers/test/test_onmessageerror.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Test onmessageerror event handlers</title>
+  </head>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
+  <script type="text/javascript" src="utils.js"></script>
+  <script>
+    /**
+     * Test that ServiceWorkerGlobalScope and ServiceWorkerContainer handle
+     * `messageerror` events, using a test helper class `StructuredCloneTester`.
+     * Intances of this class can be configured to fail to serialize or
+     * deserialize, as it's difficult to artificially create the case where an
+     * object successfully serializes but fails to deserialize (which can be
+     * caused by out-of-memory failures or the target global not supporting a
+     * serialized interface).
+     */
+
+    let registration = null;
+    let serviceWorker = null;
+    let serviceWorkerContainer = null;
+    const swScript = 'onmessageerror_worker.js';
+
+    add_task(async () => {
+      await SpecialPowers.pushPrefEnv({
+        set: [
+          ['dom.serviceWorkers.enabled', true],
+          ['dom.serviceWorkers.testing.enabled', true],
+          ['dom.testing.structuredclonetester.enabled', true],
+        ],
+      });
+
+      swContainer = navigator.serviceWorker;
+
+      registration = await swContainer.register(swScript);
+      ok(registration, 'Service Worker regsisters');
+
+      serviceWorker = registration.installing;
+      await waitForState(serviceWorker, 'activated');
+    }); // setup
+
+    add_task(async () => {
+      const serializable = true;
+      const deserializable = true;
+      let sct = new StructuredCloneTester(serializable, deserializable);
+
+      const p = new Promise((resolve, reject) => {
+        function onMessage(e) {
+          const expectedBehavior = 'Serializable and deserializable ' +
+            'StructuredCloneTester serializes and deserializes';
+
+          is(e.data.received, 'message', expectedBehavior);
+          swContainer.removeEventListener('message', onMessage);
+          resolve();
+        }
+
+        swContainer.addEventListener('message', onMessage);
+      });
+
+      serviceWorker.postMessage({ serializable, deserializable, sct });
+
+      await p;
+    });
+
+    add_task(async () => {
+      const serializable = false;
+      // if it's not serializable, being deserializable or not doesn't matter
+      const deserializable = false;
+      let sct = new StructuredCloneTester(serializable, deserializable);
+
+      try {
+        serviceWorker.postMessage({ serializable, deserializable, sct });
+        ok(false, 'StructuredCloneTester serialization should have thrown -- ' +
+          'this line should not have been reached.');
+      } catch (e) {
+        const expectedBehavior = 'Unserializable StructuredCloneTester fails ' +
+          `to send, with exception name: ${e.name}`;
+        is(e.name, 'DataCloneError', expectedBehavior);
+      }
+    });
+
+    add_task(async () => {
+      const serializable = true;
+      const deserializable = false;
+      let sct = new StructuredCloneTester(serializable, deserializable);
+
+      const p = new Promise((resolve, reject) => {
+        function onMessage(e) {
+          const expectedBehavior = 'ServiceWorkerGlobalScope handles ' +
+            'messageerror events';
+
+          is(e.data.received, 'messageerror', expectedBehavior);
+          swContainer.removeEventListener('message', onMessage);
+          resolve();
+        }
+
+        swContainer.addEventListener('message', onMessage);
+      });
+
+      serviceWorker.postMessage({ serializable, deserializable, sct });
+
+      await p;
+    }); // test ServiceWorkerGlobalScope onmessageerror
+
+    add_task(async () => {
+      const p = new Promise((resolve, reject) => {
+        function onMessageError(e) {
+          ok(true, 'ServiceWorkerContainer handles messageerror events');
+          swContainer.removeEventListener('messageerror', onMessageError);
+          resolve();
+        }
+
+        swContainer.addEventListener('messageerror', onMessageError);
+      });
+
+      serviceWorker.postMessage('send-bad-message');
+
+      await p;
+    }); // test ServiceWorkerContainer onmessageerror
+
+    add_task(async () => {
+      await SpecialPowers.popPrefEnv();
+      ok(await registration.unregister(), 'Service Worker unregisters');
+    }); // teardown
+  </script>
+  <body>
+  </body>
+</html>
--- a/dom/webidl/ServiceWorkerContainer.webidl
+++ b/dom/webidl/ServiceWorkerContainer.webidl
@@ -29,16 +29,17 @@ interface ServiceWorkerContainer : Event
   [NewObject]
   Promise<sequence<ServiceWorkerRegistration>> getRegistrations();
 
   void startMessages();
 
   attribute EventHandler oncontrollerchange;
   attribute EventHandler onerror;
   attribute EventHandler onmessage;
+  attribute EventHandler onmessageerror;
 };
 
 // Testing only.
 partial interface ServiceWorkerContainer {
   [Throws,Pref="dom.serviceWorkers.testing.enabled"]
   DOMString getScopeForUrl(DOMString url);
 };
 
--- a/dom/webidl/ServiceWorkerGlobalScope.webidl
+++ b/dom/webidl/ServiceWorkerGlobalScope.webidl
@@ -24,16 +24,17 @@ interface ServiceWorkerGlobalScope : Wor
 
   attribute EventHandler oninstall;
   attribute EventHandler onactivate;
 
   attribute EventHandler onfetch;
 
   // The event.source of these MessageEvents are instances of Client
   attribute EventHandler onmessage;
+  attribute EventHandler onmessageerror;
 };
 
 // These are from w3c.github.io/push-api/
 partial interface ServiceWorkerGlobalScope {
   attribute EventHandler onpush;
   attribute EventHandler onpushsubscriptionchange;
 };
 
--- a/dom/workers/WorkerScope.h
+++ b/dom/workers/WorkerScope.h
@@ -337,16 +337,17 @@ public:
   Registration();
 
   already_AddRefed<Promise>
   SkipWaiting(ErrorResult& aRv);
 
   IMPL_EVENT_HANDLER(activate)
   IMPL_EVENT_HANDLER(install)
   IMPL_EVENT_HANDLER(message)
+  IMPL_EVENT_HANDLER(messageerror)
 
   IMPL_EVENT_HANDLER(push)
   IMPL_EVENT_HANDLER(pushsubscriptionchange)
 
   EventHandlerNonNull*
   GetOnfetch();
 
   void
--- a/testing/web-platform/meta/service-workers/service-worker/interfaces-sw.https.html.ini
+++ b/testing/web-platform/meta/service-workers/service-worker/interfaces-sw.https.html.ini
@@ -1,21 +1,15 @@
 [interfaces-sw.https.html]
   [ServiceWorkerGlobalScope interface: attribute onforeignfetch]
     expected: FAIL
 
-  [ServiceWorkerGlobalScope interface: attribute onmessageerror]
-    expected: FAIL
-
   [ServiceWorkerGlobalScope interface: self must inherit property "onforeignfetch" with the proper type (6)]
     expected: FAIL
 
-  [ServiceWorkerGlobalScope interface: self must inherit property "onmessageerror" with the proper type (8)]
-    expected: FAIL
-
   [Client interface: attribute reserved]
     expected: FAIL
 
   [WindowClient interface: attribute ancestorOrigins]
     expected: FAIL
 
   [ServiceWorkerRegistration interface: attribute installing]
     expected: FAIL
@@ -75,19 +69,16 @@
     expected: FAIL
 
   [CacheStorage interface object length]
     expected: FAIL
 
   [ServiceWorkerGlobalScope interface: self must inherit property "onforeignfetch" with the proper type]
     expected: FAIL
 
-  [ServiceWorkerGlobalScope interface: self must inherit property "onmessageerror" with the proper type]
-    expected: FAIL
-
   [ServiceWorkerRegistration interface: self.registration must inherit property "installing" with the proper type]
     expected: FAIL
 
   [ServiceWorkerRegistration interface: self.registration must inherit property "waiting" with the proper type]
     expected: FAIL
 
   [ServiceWorkerRegistration interface: self.registration must inherit property "active" with the proper type]
     expected: FAIL