Bug 1163862 - Switch to HTTP observer + support requestId & data: URIs + test fixes draft
authorGiorgio Maone <g.maone@informaction.com>
Fri, 26 Feb 2016 19:08:32 +0100
changeset 335008 86c92017498bbf3e6e5977880c4073521a28fa43
parent 334720 918df3a0bc1c4d07299e4f66274a7da923534577
child 515050 bd90a9ca989b47f48c64c2e591f94298bb12d5c4
push id11696
push userg.maone@informaction.com
push dateFri, 26 Feb 2016 18:19:41 +0000
bugs1163862
milestone47.0a1
Bug 1163862 - Switch to HTTP observer + support requestId & data: URIs + test fixes MozReview-Commit-ID: 30nEXQpWEHg
toolkit/components/extensions/ext-webRequest.js
toolkit/components/extensions/test/mochitest/file_WebRequest_page1.html
toolkit/components/extensions/test/mochitest/test_ext_webrequest.html
toolkit/modules/addons/MatchPattern.jsm
toolkit/modules/addons/WebRequest.jsm
toolkit/modules/addons/WebRequestContent.js
toolkit/modules/tests/browser/browser_WebRequest.js
--- a/toolkit/components/extensions/ext-webRequest.js
+++ b/toolkit/components/extensions/ext-webRequest.js
@@ -28,16 +28,17 @@ function WebRequestEventManager(context,
       }
 
       let tabId = TabManager.getBrowserId(data.browser);
       if (tabId == -1) {
         return;
       }
 
       let data2 = {
+        requestId: data.requestId,
         url: data.url,
         method: data.method,
         type: data.type,
         timeStamp: Date.now(),
         frameId: ExtensionManagement.getFrameId(data.windowId),
         parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
       };
 
--- a/toolkit/components/extensions/test/mochitest/file_WebRequest_page1.html
+++ b/toolkit/components/extensions/test/mochitest/file_WebRequest_page1.html
@@ -6,24 +6,26 @@
 <link rel="stylesheet" href="file_style_good.css">
 <link rel="stylesheet" href="file_style_bad.css">
 <link rel="stylesheet" href="file_style_redirect.css">
 </head>
 <body>
 
 <div id="test">Sample text</div>
 
+<img id="img_redirect" src="file_image_redirect.png">
 <img id="img_good" src="file_image_good.png">
 <img id="img_bad" src="file_image_bad.png">
-<img id="img_redirect" src="file_image_redirect.png">
 
 <script src="file_script_good.js"></script>
 <script src="file_script_bad.js"></script>
 <script src="file_script_redirect.js"></script>
 
 <script src="file_script_xhr.js"></script>
 
 <script src="nonexistent_script_url.js"></script>
 
 <iframe src="file_WebRequest_page2.html" width="200" height="200"></iframe>
 <iframe src="redirection.sjs" width="200" height="200"></iframe>
+<iframe src="data:text/plain,webRequestTest" width="200" height="200"></iframe>
+
 </body>
 </html>
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest.html
@@ -26,17 +26,19 @@ const expected_requested = [BASE + "/fil
                             BASE + "/file_image_redirect.png",
                             BASE + "/file_script_good.js",
                             BASE + "/file_script_bad.js",
                             BASE + "/file_script_redirect.js",
                             BASE + "/file_script_xhr.js",
                             BASE + "/file_WebRequest_page2.html",
                             BASE + "/nonexistent_script_url.js",
                             BASE + "/redirection.sjs",
-                            BASE + "/xhr_resource"];
+                            BASE + "/dummy_page.html",
+                            BASE + "/xhr_resource",
+                            "data:text/plain,webRequestTest"];
 
 const expected_beforeSendHeaders = [BASE + "/file_WebRequest_page1.html",
                               BASE + "/file_style_good.css",
                               BASE + "/file_style_redirect.css",
                               BASE + "/file_image_good.png",
                               BASE + "/file_image_redirect.png",
                               BASE + "/file_script_good.js",
                               BASE + "/file_script_redirect.js",
@@ -48,26 +50,28 @@ const expected_beforeSendHeaders = [BASE
                               BASE + "/xhr_resource"];
 
 const expected_sendHeaders = expected_beforeSendHeaders.filter(u => !/_redirect\./.test(u))
                             .concat(BASE + "/redirection.sjs");
 
 const expected_redirect = expected_beforeSendHeaders.filter(u => /_redirect\./.test(u))
                             .concat(BASE + "/redirection.sjs");
 
-const expected_complete = [BASE + "/file_WebRequest_page1.html",
+const expected_response = [BASE + "/file_WebRequest_page1.html",
                            BASE + "/file_style_good.css",
                            BASE + "/file_image_good.png",
                            BASE + "/file_script_good.js",
                            BASE + "/file_script_xhr.js",
                            BASE + "/file_WebRequest_page2.html",
                            BASE + "/nonexistent_script_url.js",
                            BASE + "/dummy_page.html",
                            BASE + "/xhr_resource"];
 
+const expected_complete = expected_response.concat("data:text/plain,webRequestTest");
+
 function removeDupes(list) {
   let j = 0;
   for (let i = 1; i < list.length; i++) {
     if (list[i] != list[j]) {
       j++;
       if (i != j) {
         list[j] = list[i];
       }
@@ -80,57 +84,77 @@ function compareLists(list1, list2, kind
   list1.sort();
   removeDupes(list1);
   list2.sort();
   removeDupes(list2);
   is(String(list1), String(list2), `${kind} URLs correct`);
 }
 
 function backgroundScript() {
-  const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
-
   let checkCompleted = true;
   let savedTabId = -1;
 
+  function shouldRecord(url) {
+    return url.startsWith(BASE) || /^data:.*\bwebRequestTest\b/.test(url);
+  }
+
   function checkType(details) {
     let expected_type = "???";
     if (details.url.indexOf("style") != -1) {
       expected_type = "stylesheet";
     } else if (details.url.indexOf("image") != -1) {
       expected_type = "image";
     } else if (details.url.indexOf("script") != -1) {
       expected_type = "script";
     } else if (details.url.indexOf("page1") != -1) {
       expected_type = "main_frame";
-    } else if (/page2|redirection|dummy_page/.test(details.url)) {
+    } else if (/page2|redirection|dummy_page|data:text\/(?:plain|html),/.test(details.url)) {
       expected_type = "sub_frame";
     } else if (details.url.indexOf("xhr") != -1) {
       expected_type = "xmlhttprequest";
     }
     browser.test.assertEq(details.type, expected_type, "resource type is correct");
   }
 
+  let requestIDs = new Map();
+  let idDisposalEvents = new Set(["completed", "error", "redirect"]);
+  function checkRequestId(details, event = "unknown") {
+    let ids = requestIDs.get(details.url);
+    browser.test.assertTrue(ids && ids.has(details.requestId), `correct requestId for ${details.url} (${details.requestId} in [${ids && [...ids].join(", ")}])`);
+    if (ids && idDisposalEvents.has(event)) {
+      ids.delete(details.requestId);
+    }
+  }
+
   let frameIDs = new Map();
 
   let recorded = {requested: [],
                   beforeSendHeaders: [],
                   beforeRedirect: [],
                   sendHeaders: [],
                   responseStarted: [],
                   completed: []};
 
   function checkResourceType(type) {
     let key = type.toUpperCase();
-    browser.test.assertTrue(key in browser.webRequest.ResourceType);
+    browser.test.assertTrue(key in browser.webRequest.ResourceType, `valid resource type ${key}`);
   }
 
   function onBeforeRequest(details) {
-    browser.test.log(`onBeforeRequest ${details.url}`);
+    browser.test.log(`onBeforeRequest ${details.requestId} ${details.url}`);
+
+    browser.test.assertTrue(details.requestId > 0, `valid requestId ${details.requestId}`);
+    let ids = requestIDs.get(details.url);
+    if (ids) {
+      ids.add(details.requestId);
+    } else {
+      requestIDs.set(details.url, new Set([details.requestId]));
+    }
     checkResourceType(details.type);
-    if (details.url.startsWith(BASE)) {
+    if (shouldRecord(details.url)) {
       recorded.requested.push(details.url);
 
       if (savedTabId == -1) {
         browser.test.assertTrue(details.tabId !== undefined, "tab ID defined");
         savedTabId = details.tabId;
       }
 
       browser.test.assertEq(details.tabId, savedTabId, "correct tab ID");
@@ -150,36 +174,38 @@ function backgroundScript() {
     if (details.url.indexOf("_bad.") != -1) {
       return {cancel: true};
     }
     return {};
   }
 
   function onBeforeSendHeaders(details) {
     browser.test.log(`onBeforeSendHeaders ${details.url}`);
+    checkRequestId(details);
     checkResourceType(details.type);
-    if (details.url.startsWith(BASE)) {
+    if (shouldRecord(details.url)) {
       recorded.beforeSendHeaders.push(details.url);
 
       browser.test.assertEq(details.tabId, savedTabId, "correct tab ID");
       checkType(details);
 
       let id = frameIDs.get(details.url);
       browser.test.assertEq(id, details.frameId, "frame ID same in onBeforeSendHeaders as onBeforeRequest");
     }
     if (details.url.indexOf("_redirect.") != -1) {
       return {redirectUrl: details.url.replace("_redirect.", "_good.")};
     }
     return {};
   }
 
   function onBeforeRedirect(details) {
     browser.test.log(`onBeforeRedirect ${details.url} -> ${details.redirectUrl}`);
+    checkRequestId(details, "redirect");
     checkResourceType(details.type);
-    if (details.url.startsWith(BASE)) {
+    if (shouldRecord(details.url)) {
       recorded.beforeRedirect.push(details.url);
 
       browser.test.assertEq(details.tabId, savedTabId, "correct tab ID");
       checkType(details);
 
       let id = frameIDs.get(details.url);
       browser.test.assertEq(id, details.frameId, "frame ID same in onBeforeRedirect as onBeforeRequest");
       frameIDs.set(details.redirectUrl, details.frameId);
@@ -187,34 +213,39 @@ function backgroundScript() {
     if (details.url.indexOf("_redirect.") != -1) {
       let expectedUrl = details.url.replace("_redirect.", "_good.");
       browser.test.assertEq(details.redirectUrl, expectedUrl, "correct redirectUrl value");
     }
     return {};
   }
 
   function onRecord(kind, details) {
+    browser.test.log(`${kind} ${details.url}`);
     checkResourceType(details.type);
-    if (details.url.startsWith(BASE)) {
+    checkRequestId(details, kind);
+    if (shouldRecord(details.url)) {
       recorded[kind].push(details.url);
     }
   }
 
   let completedUrls = {
     responseStarted: new Set(),
     completed: new Set(),
   };
 
   function checkIpAndRecord(kind, details) {
     onRecord(kind, details);
 
     // When resources are cached, the ip property is not present,
     // so only check for the ip property the first time around.
     if (checkCompleted && !completedUrls[kind].has(details.url)) {
-      browser.test.assertEq(details.ip, "127.0.0.1", "correct ip");
+      // We can only tell IPs for HTTP requests.
+      if (/^https?:/.test(details.url)) {
+        browser.test.assertEq(details.ip, "127.0.0.1", "correct ip");
+      }
       completedUrls[kind].add(details.url);
     }
   }
 
   browser.webRequest.onBeforeRequest.addListener(onBeforeRequest, {urls: ["<all_urls>"]}, ["blocking"]);
   browser.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, {urls: ["<all_urls>"]}, ["blocking"]);
   browser.webRequest.onSendHeaders.addListener(onRecord.bind(null, "sendHeaders"), {urls: ["<all_urls>"]});
   browser.webRequest.onBeforeRedirect.addListener(onBeforeRedirect, {urls: ["<all_urls>"]});
@@ -238,17 +269,17 @@ function backgroundScript() {
 function* test_once(skipCompleted) {
   let extensionData = {
     manifest: {
       permissions: [
         "webRequest",
         "webRequestBlocking",
       ],
     },
-    background: "(" + backgroundScript.toString() + ")()",
+    background: `const BASE = ${JSON.stringify(BASE)}; (${backgroundScript.toString()})()`,
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   let [, resourceTypes] = yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
   info("webrequest extension loaded");
 
   if (skipCompleted) {
     extension.sendMessage("skipCompleted");
@@ -292,17 +323,17 @@ function* test_once(skipCompleted) {
 
   extension.sendMessage("getResults");
   let recorded = yield extension.awaitMessage("results");
 
   compareLists(recorded.requested, expected_requested, "requested");
   compareLists(recorded.beforeSendHeaders, expected_beforeSendHeaders, "beforeSendHeaders");
   compareLists(recorded.sendHeaders, expected_sendHeaders, "sendHeaders");
   compareLists(recorded.beforeRedirect, expected_redirect, "beforeRedirect");
-  compareLists(recorded.responseStarted, expected_complete, "responseStarted");
+  compareLists(recorded.responseStarted, expected_response, "responseStarted");
   compareLists(recorded.completed, expected_complete, "completed");
 
   yield extension.unload();
   info("webrequest extension unloaded");
 }
 
 // Run the test twice to make sure it works with caching.
 add_task(function*() { yield test_once(false); });
--- a/toolkit/modules/addons/MatchPattern.jsm
+++ b/toolkit/modules/addons/MatchPattern.jsm
@@ -10,17 +10,18 @@ Cu.import("resource://gre/modules/XPCOMU
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 
 this.EXPORTED_SYMBOLS = ["MatchPattern"];
 
 /* globals MatchPattern */
 
-const PERMITTED_SCHEMES = ["http", "https", "file", "ftp", "app"];
+const PERMITTED_SCHEMES = ["http", "https", "file", "ftp", "app", "data"];
+const PERMITTED_SCHEMES_REGEXP = PERMITTED_SCHEMES.join("|");
 
 // This function converts a glob pattern (containing * and possibly ?
 // as wildcards) to a regular expression.
 function globToRegexp(pat, allowQuestion) {
   // Escape everything except ? and *.
   pat = pat.replace(/[.+^${}()|[\]\\]/g, "\\$&");
 
   if (allowQuestion) {
@@ -37,17 +38,17 @@ function globToRegexp(pat, allowQuestion
 function SingleMatchPattern(pat) {
   if (pat == "<all_urls>") {
     this.schemes = PERMITTED_SCHEMES;
     this.host = "*";
     this.path = new RegExp(".*");
   } else if (!pat) {
     this.schemes = [];
   } else {
-    let re = new RegExp("^(http|https|file|ftp|app|\\*)://(\\*|\\*\\.[^*/]+|[^*/]+|)(/.*)$");
+    let re = new RegExp(`^(${PERMITTED_SCHEMES_REGEXP}|\\*)://(\\*|\\*\\.[^*/]+|[^*/]+|)(/.*)$`);
     let match = re.exec(pat);
     if (!match) {
       Cu.reportError(`Invalid match pattern: '${pat}'`);
       this.schemes = [];
       return;
     }
 
     if (match[1] == "*") {
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -16,20 +16,54 @@ const Cr = Components.results;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
                                   "resource://gre/modules/BrowserUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebRequestCommon",
                                   "resource://gre/modules/WebRequestCommon.jsm");
 
-// TODO
-// Figure out how to handle requestId. Gecko seems to have no such thing. (Bug 1163862)
-// We also don't know the method for content policy. (Bug 1163862)
-// We don't even have a window ID for HTTP observer stuff. (Bug 1163861)
+function attachToChannel(channel, key, data) {
+  if (channel instanceof Ci.nsIWritablePropertyBag2) {
+    let wrapper = {value: data};
+    wrapper.wrappedJSObject = wrapper;
+    channel.setPropertyAsInterface(key, wrapper);
+  }
+}
+
+function extractFromChannel(channel, key) {
+  if (channel instanceof Ci.nsIPropertyBag2 && channel.hasKey(key)) {
+    let data = channel.get(key);
+    if (data && data.wrappedJSObject) {
+      data = data.wrappedJSObject;
+    }
+    return "value" in data ? data.value : data;
+  }
+  return null;
+}
+
+var RequestId = {
+  count: 1,
+  KEY: "mozilla.webRequest.requestId",
+  create(channel = null) {
+    let id = this.count++;
+    if (channel) {
+      attachToChannel(channel, this.KEY, id);
+    }
+    return id;
+  },
+
+  get(channel) {
+    return channel && extractFromChannel(channel, this.KEY) || this.create(channel);
+  },
+};
+
+function runLater(job) {
+  Services.tm.currentThread.dispatch(job, Ci.nsIEventTarget.DISPATCH_NORMAL);
+}
 
 function parseFilter(filter) {
   if (!filter) {
     filter = {};
   }
 
   // FIXME: Support windowId filtering.
   return {urls: filter.urls || null, types: filter.types || null};
@@ -48,16 +82,18 @@ function parseExtra(extra, allowed) {
   for (let al of allowed) {
     if (extra && extra.indexOf(al) != -1) {
       result[al] = true;
     }
   }
   return result;
 }
 
+var HttpObserverManager;
+
 var ContentPolicyManager = {
   policyData: new Map(),
   policies: new Map(),
   idMap: new Map(),
   nextId: 0,
 
   init() {
     Services.ppmm.initialProcessData.webRequestContentPolicies = this.policyData;
@@ -72,42 +108,60 @@ var ContentPolicyManager = {
     for (let id of msg.data.ids) {
       let callback = this.policies.get(id);
       if (!callback) {
         // It's possible that this listener has been removed and the
         // child hasn't learned yet.
         continue;
       }
       let response = null;
+      let data = {
+        url: msg.data.url,
+        windowId: msg.data.windowId,
+        parentWindowId: msg.data.parentWindowId,
+        type: msg.data.type,
+        browser: browser,
+        requestId: RequestId.create(),
+      };
       try {
-        response = callback({
-          url: msg.data.url,
-          windowId: msg.data.windowId,
-          parentWindowId: msg.data.parentWindowId,
-          type: msg.data.type,
-          browser: browser,
-        });
+        response = callback(data);
+        if (response && response.cancel) {
+          return {cancel: true};
+        }
+
+        // FIXME: Need to handle redirection here. (Bug 1163862)
       } catch (e) {
         Cu.reportError(e);
+      } finally {
+        runLater(() => this.runChannelListener("onStop", data));
       }
-
-      if (response && response.cancel) {
-        return {cancel: true};
-      }
-
-      // FIXME: Need to handle redirection here. (Bug 1163862)
     }
 
     return {};
   },
 
+  runChannelListener(kind, data) {
+    let listeners = HttpObserverManager.listeners[kind];
+    let uri = BrowserUtils.makeURI(data.url);
+    let policyType = data.type;
+    for (let [callback, opts] of listeners.entries()) {
+      if (!HttpObserverManager.shouldRunListener(policyType, uri, opts.filter)) {
+        continue;
+      }
+      callback(data);
+    }
+  },
+
   addListener(callback, opts) {
+    // Clone opts, since we're going to modify them for IPC.
+    opts = Object.assign({}, opts);
     let id = this.nextId++;
     opts.id = id;
     if (opts.filter.urls) {
+      opts.filter = Object.assign({}, opts.filter);
       opts.filter.urls = opts.filter.urls.serialize();
     }
     Services.ppmm.broadcastAsyncMessage("WebRequest:AddContentPolicy", opts);
 
     this.policyData.set(id, opts);
 
     this.policies.set(id, callback);
     this.idMap.set(callback, id);
@@ -146,18 +200,16 @@ StartStopListener.prototype = {
     return result;
   },
 
   onDataAvailable(...args) {
     return this.orig.onDataAvailable(...args);
   },
 };
 
-var HttpObserverManager;
-
 var ChannelEventSink = {
   _classDescription: "WebRequest channel event sink",
   _classID: Components.ID("115062f8-92f1-11e5-8b7f-080027b0f7ec"),
   _contractID: "@mozilla.org/webrequest/channel-event-sink;1",
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink,
                                          Ci.nsIFactory]),
 
@@ -173,17 +225,17 @@ var ChannelEventSink = {
 
   unregister() {
     let catMan = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
     catMan.deleteCategoryEntry("net-channel-event-sinks", this._contractID, false);
   },
 
   // nsIChannelEventSink implementation
   asyncOnChannelRedirect(oldChannel, newChannel, flags, redirectCallback) {
-    Services.tm.currentThread.dispatch(() => redirectCallback.onRedirectVerifyCallback(Cr.NS_OK), Ci.nsIEventTarget.DISPATCH_NORMAL);
+    runLater(() => redirectCallback.onRedirectVerifyCallback(Cr.NS_OK));
     try {
       HttpObserverManager.onChannelReplaced(oldChannel, newChannel);
     } catch (e) {
       // we don't wanna throw: it would abort the redirection
     }
   },
 
   // nsIFactory implementation
@@ -198,26 +250,27 @@ var ChannelEventSink = {
 ChannelEventSink.init();
 
 HttpObserverManager = {
   modifyInitialized: false,
   examineInitialized: false,
   redirectInitialized: false,
 
   listeners: {
+    opening: new Map(),
     modify: new Map(),
     afterModify: new Map(),
     headersReceived: new Map(),
     onRedirect: new Map(),
     onStart: new Map(),
     onStop: new Map(),
   },
 
   addOrRemove() {
-    let needModify = this.listeners.modify.size || this.listeners.afterModify.size;
+    let needModify = this.listeners.opening.size || this.listeners.modify.size || this.listeners.afterModify.size;
     if (needModify && !this.modifyInitialized) {
       this.modifyInitialized = true;
       Services.obs.addObserver(this, "http-on-modify-request", false);
     } else if (!needModify && this.modifyInitialized) {
       this.modifyInitialized = false;
       Services.obs.removeObserver(this, "http-on-modify-request");
     }
 
@@ -284,32 +337,37 @@ HttpObserverManager = {
     };
 
     channel[method](visitor);
     return headers;
   },
 
   observe(subject, topic, data) {
     let channel = subject.QueryInterface(Ci.nsIHttpChannel);
-
-    if (topic == "http-on-modify-request") {
-      this.modify(channel, topic, data);
-    } else if (topic == "http-on-examine-response" ||
-               topic == "http-on-examine-cached-response" ||
-               topic == "http-on-examine-merged-response") {
-      this.examine(channel, topic, data);
+    switch (topic) {
+      case "http-on-modify-request":
+        this.modify(channel, topic, data);
+        break;
+      case "http-on-examine-response":
+      case "http-on-examine-cached-response":
+      case "http-on-examine-merged-response":
+        this.examine(channel, topic, data);
+        break;
     }
   },
 
   shouldRunListener(policyType, uri, filter) {
     return WebRequestCommon.typeMatches(policyType, filter.types) &&
            WebRequestCommon.urlMatches(uri, filter.urls);
   },
 
   runChannelListener(channel, loadContext, kind, extraData = null) {
+    if (channel.status === Cr.NS_ERROR_ABORT) {
+      return false;
+    }
     let listeners = this.listeners[kind];
     let browser = loadContext ? loadContext.topFrameElement : null;
     let loadInfo = channel.loadInfo;
     let policyType = loadInfo ?
                      loadInfo.externalContentPolicyType :
                      Ci.nsIContentPolicy.TYPE_OTHER;
 
     let requestHeaders;
@@ -321,16 +379,17 @@ HttpObserverManager = {
                         kind === "onStop";
 
     for (let [callback, opts] of listeners.entries()) {
       if (!this.shouldRunListener(policyType, channel.URI, opts.filter)) {
         continue;
       }
 
       let data = {
+        requestId: RequestId.get(channel),
         url: channel.URI.spec,
         method: channel.requestMethod,
         browser: browser,
         type: WebRequestCommon.typeForPolicyType(policyType),
         windowId: loadInfo ? loadInfo.outerWindowID : 0,
         parentWindowId: loadInfo ? loadInfo.parentOuterWindowID : 0,
       };
 
@@ -367,17 +426,17 @@ HttpObserverManager = {
       } catch (e) {
         Cu.reportError(e);
       }
 
       if (!result || !opts.blocking) {
         return true;
       }
       if (result.cancel) {
-        channel.cancel();
+        channel.cancel(Cr.NS_ERROR_ABORT);
         return false;
       }
       if (result.redirectUrl) {
         channel.redirectTo(BrowserUtils.makeURI(result.redirectUrl));
         return false;
       }
       if (opts.requestHeaders && result.requestHeaders) {
         // Start by clearing everything.
@@ -402,17 +461,18 @@ HttpObserverManager = {
     }
 
     return true;
   },
 
   modify(channel, topic, data) {
     let loadContext = this.getLoadContext(channel);
 
-    if (this.runChannelListener(channel, loadContext, "modify")) {
+    if (this.runChannelListener(channel, loadContext, "opening") &&
+        this.runChannelListener(channel, loadContext, "modify")) {
       this.runChannelListener(channel, loadContext, "afterModify");
     }
   },
 
   examine(channel, topic, data) {
     let loadContext = this.getLoadContext(channel);
 
     if (this.listeners.onStart.size || this.listeners.onStop.size) {
@@ -445,19 +505,21 @@ HttpObserverManager = {
 };
 
 var onBeforeRequest = {
   addListener(callback, filter = null, opt_extraInfoSpec = null) {
     // FIXME: Add requestBody support.
     let opts = parseExtra(opt_extraInfoSpec, ["blocking"]);
     opts.filter = parseFilter(filter);
     ContentPolicyManager.addListener(callback, opts);
+    HttpObserverManager.addListener("opening", callback, opts);
   },
 
   removeListener(callback) {
+    HttpObserverManager.removeListener("opening", callback);
     ContentPolicyManager.removeListener(callback);
   },
 };
 
 function HttpEvent(internalEvent, options) {
   this.internalEvent = internalEvent;
   this.options = options;
 }
@@ -477,17 +539,17 @@ HttpEvent.prototype = {
 var onBeforeSendHeaders = new HttpEvent("modify", ["requestHeaders", "blocking"]);
 var onSendHeaders = new HttpEvent("afterModify", ["requestHeaders"]);
 var onHeadersReceived = new HttpEvent("headersReceived", ["blocking", "responseHeaders"]);
 var onBeforeRedirect = new HttpEvent("onRedirect", ["responseHeaders"]);
 var onResponseStarted = new HttpEvent("onStart", ["responseHeaders"]);
 var onCompleted = new HttpEvent("onStop", ["responseHeaders"]);
 
 var WebRequest = {
-  // Handled via content policy.
+  // http-on-modify observer for HTTP(S), content policy for the other protocols (notably, data:)
   onBeforeRequest: onBeforeRequest,
 
   // http-on-modify observer.
   onBeforeSendHeaders: onBeforeSendHeaders,
 
   // http-on-modify observer.
   onSendHeaders: onSendHeaders,
 
--- a/toolkit/modules/addons/WebRequestContent.js
+++ b/toolkit/modules/addons/WebRequestContent.js
@@ -12,16 +12,18 @@ var Cr = Components.results;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
                                   "resource://gre/modules/MatchPattern.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebRequestCommon",
                                   "resource://gre/modules/WebRequestCommon.jsm");
 
+const IS_HTTP = /^https?:/;
+
 var ContentPolicy = {
   _classDescription: "WebRequest content policy",
   _classID: Components.ID("938e5d24-9ccc-4b55-883e-c252a41f7ce9"),
   _contractID: "@mozilla.org/webrequest/policy;1",
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPolicy,
                                          Ci.nsIFactory,
                                          Ci.nsISupportsWeakReference]),
@@ -73,16 +75,22 @@ var ContentPolicy = {
 
   unregister() {
     let catMan = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
     catMan.deleteCategoryEntry("content-policy", this._contractID, false);
   },
 
   shouldLoad(policyType, contentLocation, requestOrigin,
              node, mimeTypeGuess, extra, requestPrincipal) {
+    let url = contentLocation.spec;
+    if (IS_HTTP.test(url)) {
+      // We'll handle this in our parent process HTTP observer.
+      return Ci.nsIContentPolicy.ACCEPT;
+    }
+
     let block = false;
     let ids = [];
     for (let [id, {blocking, filter}] of this.contentPolicies.entries()) {
       if (WebRequestCommon.typeMatches(policyType, filter.types) &&
           WebRequestCommon.urlMatches(contentLocation, filter.urls)) {
         if (blocking) {
           block = true;
         }
@@ -141,17 +149,17 @@ var ContentPolicy = {
       } catch (e) {
         if (e.result != Cr.NS_NOINTERFACE) {
           throw e;
         }
       }
     }
 
     let data = {ids,
-                url: contentLocation.spec,
+                url,
                 type: WebRequestCommon.typeForPolicyType(policyType),
                 windowId,
                 parentWindowId};
 
     if (block) {
       let rval = mm.sendSyncMessage("WebRequest:ShouldLoad", data);
       if (rval.length == 1 && rval[0].cancel) {
         return Ci.nsIContentPolicy.REJECT;
--- a/toolkit/modules/tests/browser/browser_WebRequest.js
+++ b/toolkit/modules/tests/browser/browser_WebRequest.js
@@ -112,16 +112,17 @@ const expected_requested = [BASE + "/fil
                             BASE + "/file_image_redirect.png",
                             BASE + "/file_script_good.js",
                             BASE + "/file_script_bad.js",
                             BASE + "/file_script_redirect.js",
                             BASE + "/file_script_xhr.js",
                             BASE + "/file_WebRequest_page2.html",
                             BASE + "/nonexistent_script_url.js",
                             BASE +  "/WebRequest_redirection.sjs",
+                            BASE + "/dummy_page.html",
                             BASE + "/xhr_resource"];
 
 const expected_sendHeaders = [BASE + "/file_WebRequest_page1.html",
                               BASE + "/file_style_good.css",
                               BASE + "/file_style_redirect.css",
                               BASE + "/file_image_good.png",
                               BASE + "/file_image_redirect.png",
                               BASE + "/file_script_good.js",