Bug 1163862 - Switch to HTTP observer where possible + support requestId & data: URIs r=billm
☠☠ backed out by 535dbd1cfd00 ☠ ☠
authorGiorgio Maone <g.maone@informaction.com>
Wed, 24 Feb 2016 00:21:42 +0100
changeset 285622 8ae0fba610234292b64a86f62722767f151abc17
parent 285621 8b81d4ca06800157280f0b4e3b747f438339118e
child 285623 bc5e02dcb4015e4c3ae674fcf68e8a53e935b6c7
push id72448
push userwmccloskey@mozilla.com
push dateThu, 25 Feb 2016 21:59:36 +0000
treeherdermozilla-inbound@8ae0fba61023 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm
bugs1163862
milestone47.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 1163862 - Switch to HTTP observer where possible + support requestId & data: URIs r=billm MozReview-Commit-ID: EBRWBgGPqyD
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
@@ -20,10 +20,12 @@
 <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,68 @@ 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();
+  function checkRequestId(details) {
+    browser.test.assertEq(requestIDs.get(details.url), details.requestId, `correct requestId for ${details.url} (${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);
   }
 
   function onBeforeRequest(details) {
-    browser.test.log(`onBeforeRequest ${details.url}`);
+    browser.test.log(`onBeforeRequest ${details.requestId} ${details.url}`);
+
+    browser.test.assertTrue(details.requestId > 0, `valid request ID ${details.requestId}`);
+    requestIDs.set(details.url, 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 +165,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);
     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);
@@ -188,33 +205,37 @@ function backgroundScript() {
       let expectedUrl = details.url.replace("_redirect.", "_good.");
       browser.test.assertEq(details.redirectUrl, expectedUrl, "correct redirectUrl value");
     }
     return {};
   }
 
   function onRecord(kind, details) {
     checkResourceType(details.type);
-    if (details.url.startsWith(BASE)) {
+    checkRequestId(details);
+    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 +259,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 +313,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",