Bugzilla #1320189 - Fix WebChannel error reporting for content errors r?markh draft bz1320189
authorvladikoff <vlad.filippov@gmail.com>
Thu, 24 Nov 2016 17:53:51 -0500
branchbz1320189
changeset 443712 20ea12f25c76f8f0e99d834f5f68c44aa23edab3
parent 443602 bad312aefb42982f492ad2cf36f4c6c3d698f4f7
child 724624 a990b39f7b50319910015d5a704b776bfd88910c
push id37077
push uservlad@vladikoff.com
push dateFri, 25 Nov 2016 05:02:02 +0000
reviewersmarkh
bugs1320189
milestone53.0a1
Bugzilla #1320189 - Fix WebChannel error reporting for content errors r?markh MozReview-Commit-ID: JT0KmWpIL0V
browser/base/content/test/general/browser_remoteTroubleshoot.js
browser/base/content/test/general/browser_web_channel.html
browser/base/content/test/general/browser_web_channel.js
toolkit/modules/WebChannel.jsm
--- a/browser/base/content/test/general/browser_remoteTroubleshoot.js
+++ b/browser/base/content/test/general/browser_remoteTroubleshoot.js
@@ -32,18 +32,18 @@ function promiseNewChannelResponse(uri) 
     gBrowser.removeTab(tab);
     return data;
   });
 }
 
 add_task(function*() {
   // We haven't set a permission yet - so even the "good" URI should fail.
   let got = yield promiseNewChannelResponse(TEST_URI_GOOD);
-  // Should have no data.
-  Assert.ok(got.message === undefined, "should have failed to get any data");
+  // Should return an error.
+  Assert.ok(got.message.errno === 2, "should have failed with errno 2, no such channel");
 
   // Add a permission manager entry for our URI.
   Services.perms.add(TEST_URI_GOOD,
                      "remote-troubleshooting",
                      Services.perms.ALLOW_ACTION);
   registerCleanupFunction(() => {
     Services.perms.remove(TEST_URI_GOOD, "remote-troubleshooting");
   });
@@ -71,19 +71,19 @@ add_task(function*() {
                  "should have correct update channel.");
   }
 
 
   // And check some keys we know we decline to return.
   Assert.ok(!got.message.modifiedPreferences, "should not have a modifiedPreferences key");
   Assert.ok(!got.message.crashes, "should not have crash info");
 
-  // Now a http:// URI - should get nothing even with the permission setup.
+  // Now a http:// URI - should receive an error
   got = yield promiseNewChannelResponse(TEST_URI_BAD);
-  Assert.ok(got.message === undefined, "should have failed to get any data");
+  Assert.ok(got.message.errno === 2, "should have failed with errno 2, no such channel");
 
   // Check that the page can send an object as well if it's in the whitelist
   let webchannelWhitelistPref = "webchannel.allowObject.urlWhitelist";
   let origWhitelist = Services.prefs.getCharPref(webchannelWhitelistPref);
   let newWhitelist = origWhitelist + " https://example.com";
   Services.prefs.setCharPref(webchannelWhitelistPref, newWhitelist);
   registerCleanupFunction(() => {
     Services.prefs.clearUserPref(webchannelWhitelistPref);
--- a/browser/base/content/test/general/browser_web_channel.html
+++ b/browser/base/content/test/general/browser_web_channel.html
@@ -31,16 +31,22 @@
         test_unsolicited();
         break;
       case "bubbles":
         test_bubbles();
         break;
       case "object":
         test_object();
         break;
+      case "error_thrown":
+        test_error_thrown();
+        break;
+      case "error_invalid_channel":
+        test_error_invalid_channel();
+        break;
       default:
         throw new Error(`INVALID TEST NAME ${testName}`);
     }
   };
 
   function test_generic() {
     var event = new window.CustomEvent("WebChannelMessageToChrome", {
       detail: JSON.stringify({
@@ -167,16 +173,54 @@
       })
     });
     // Test fails if objectMessage is received, we send stringMessage to know
     // when we should stop listening for objectMessage
     window.dispatchEvent(objectMessage);
     window.dispatchEvent(stringMessage);
   }
 
+  function test_error_thrown() {
+   var event = new window.CustomEvent("WebChannelMessageToChrome", {
+     detail: JSON.stringify({
+       id: "error",
+       message: {
+         command: "oops"
+       }
+     })
+   });
+
+   // echo the response back to chrome - chrome will check it is the
+   // expected error.
+   window.addEventListener("WebChannelMessageToContent", function(e) {
+     echoEventToChannel(e, "echo");
+   }, true);
+
+   window.dispatchEvent(event);
+  }
+
+  function test_error_invalid_channel() {
+   var event = new window.CustomEvent("WebChannelMessageToChrome", {
+     detail: JSON.stringify({
+       id: "invalid-channel",
+       message: {
+         command: "oops"
+       }
+     })
+   });
+
+   // echo the response back to chrome - chrome will check it is the
+   // expected error.
+   window.addEventListener("WebChannelMessageToContent", function(e) {
+     echoEventToChannel(e, "echo");
+   }, true);
+
+   window.dispatchEvent(event);
+  }
+
   function echoEventToChannel(e, channelId) {
     var echoedEvent = new window.CustomEvent("WebChannelMessageToChrome", {
       detail: JSON.stringify({
         id: channelId,
         message: e.detail.message,
       })
     });
 
--- a/browser/base/content/test/general/browser_web_channel.js
+++ b/browser/base/content/test/general/browser_web_channel.js
@@ -391,17 +391,78 @@ var gTests = [
         gBrowser,
         url: HTTP_PATH + HTTP_ENDPOINT + "?object"
       }, function* () {
         yield testDonePromise;
         Services.prefs.setCharPref(webchannelWhitelistPref, origWhitelist);
         channel.stopListening();
       });
     }
-  }
+  },
+  {
+    desc: "WebChannel errors handling the message are delivered back to content",
+    run: function* () {
+      const ERRNO_UNKNOWN_ERROR              = 999; // WebChannel.jsm doesn't export this.
+
+      // The channel where we purposely fail responding to a command.
+      let channel = new WebChannel("error", Services.io.newURI(HTTP_PATH, null, null));
+      // The channel where we see the response when the content sees the error
+      let echoChannel = new WebChannel("echo", Services.io.newURI(HTTP_PATH, null, null));
+
+      let testDonePromise = new Promise((resolve, reject) => {
+        // listen for the confirmation that content saw the error.
+        echoChannel.listen((id, message, sender) => {
+          is(id, "echo");
+          is(message.error, "oh no");
+          is(message.errno, ERRNO_UNKNOWN_ERROR);
+          resolve();
+        });
+
+        // listen for a message telling us to simulate an error.
+        channel.listen((id, message, sender) => {
+          is(id, "error");
+          is(message.command, "oops");
+          throw new Error("oh no");
+        });
+      });
+      yield BrowserTestUtils.withNewTab({
+        gBrowser,
+        url: HTTP_PATH + HTTP_ENDPOINT + "?error_thrown"
+      }, function* () {
+        yield testDonePromise;
+        channel.stopListening();
+        echoChannel.stopListening();
+      });
+    }
+  },
+  {
+    desc: "WebChannel errors due to an invalid channel are delivered back to content",
+    run: function* () {
+      const ERRNO_NO_SUCH_CHANNEL            = 2; // WebChannel.jsm doesn't export this.
+      // The channel where we see the response when the content sees the error
+      let echoChannel = new WebChannel("echo", Services.io.newURI(HTTP_PATH, null, null));
+
+      let testDonePromise = new Promise((resolve, reject) => {
+        // listen for the confirmation that content saw the error.
+        echoChannel.listen((id, message, sender) => {
+          is(id, "echo");
+          is(message.error, "No Such Channel");
+          is(message.errno, ERRNO_NO_SUCH_CHANNEL);
+          resolve();
+        });
+      });
+      yield BrowserTestUtils.withNewTab({
+        gBrowser,
+        url: HTTP_PATH + HTTP_ENDPOINT + "?error_invalid_channel"
+      }, function* () {
+        yield testDonePromise;
+        echoChannel.stopListening();
+      });
+    }
+  },
 ]; // gTests
 
 function test() {
   waitForExplicitFinish();
 
   Task.spawn(function* () {
     for (let testCase of gTests) {
       info("Running: " + testCase.desc);
--- a/toolkit/modules/WebChannel.jsm
+++ b/toolkit/modules/WebChannel.jsm
@@ -4,16 +4,18 @@
 
 /**
  * WebChannel is an abstraction that uses the Message Manager and Custom Events
  * to create a two-way communication channel between chrome and content code.
  */
 
 this.EXPORTED_SYMBOLS = ["WebChannel", "WebChannelBroker"];
 
+const ERRNO_MISSING_PRINCIPAL          = 1;
+const ERRNO_NO_SUCH_CHANNEL            = 2;
 const ERRNO_UNKNOWN_ERROR              = 999;
 const ERROR_UNKNOWN                    = "UNKNOWN_ERROR";
 
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
@@ -78,32 +80,32 @@ var WebChannelBroker = Object.create({
       } catch (e) {
         Cu.reportError("Failed to parse WebChannel data as a JSON object");
         return;
       }
     }
 
     if (data && data.id) {
       if (!event.principal) {
-        this._sendErrorEventToContent(data.id, sendingContext, "Message principal missing");
+        this._sendErrorEventToContent(data.id, sendingContext, ERRNO_MISSING_PRINCIPAL, "Message principal missing");
       } else {
         let validChannelFound = false;
         data.message = data.message || {};
 
         for (var channel of this._channelMap.keys()) {
           if (channel.id === data.id &&
             channel._originCheckCallback(event.principal)) {
             validChannelFound = true;
             channel.deliver(data, sendingContext);
           }
         }
 
         // if no valid origins send an event that there is no such valid channel
         if (!validChannelFound) {
-          this._sendErrorEventToContent(data.id, sendingContext, "No Such Channel");
+          this._sendErrorEventToContent(data.id, sendingContext, ERRNO_NO_SUCH_CHANNEL, "No Such Channel");
         }
       }
     } else {
       Cu.reportError("WebChannel channel id missing");
     }
   },
   /**
    * The global message manager operates on every <browser>
@@ -122,25 +124,28 @@ var WebChannelBroker = Object.create({
    * @param id {String}
    *        The WebChannel id to include in the message
    * @param sendingContext {Object}
    *        Message sending context
    * @param [errorMsg] {String}
    *        Error message
    * @private
    */
-  _sendErrorEventToContent: function(id, sendingContext, errorMsg) {
+  _sendErrorEventToContent: function(id, sendingContext, errorNo, errorMsg) {
     let { browser: targetBrowser, eventTarget, principal: targetPrincipal } = sendingContext;
 
     errorMsg = errorMsg || "Web Channel Broker error";
 
     if (targetBrowser && targetBrowser.messageManager) {
       targetBrowser.messageManager.sendAsyncMessage("WebChannelMessageToContent", {
         id: id,
-        error: errorMsg,
+        message: {
+          errno: errorNo,
+          error: errorMsg,
+        },
       }, { eventTarget: eventTarget }, targetPrincipal);
     } else {
       Cu.reportError("Failed to send a WebChannel error. Target invalid.");
     }
     Cu.reportError(id.toString() + " error message. " + errorMsg);
   },
 });