Bug 1314492 refactor webrequest tests, r=kmag
authorShane Caraveo <scaraveo@mozilla.com>
Thu, 10 Nov 2016 16:01:50 -0800
changeset 322031 df489872d561d35b4b416651d338784575de4911
parent 322030 0d8346f8bbcbf47bcae441656caaf13f17d4a83a
child 322032 95ef280ccc1bafebc7bdcbf47c399713195e5b62
push id21
push usermaklebus@msu.edu
push dateThu, 01 Dec 2016 06:22:08 +0000
reviewerskmag
bugs1314492
milestone52.0a1
Bug 1314492 refactor webrequest tests, r=kmag MozReview-Commit-ID: D0dleERLM3K
toolkit/components/extensions/test/mochitest/.eslintrc.js
toolkit/components/extensions/test/mochitest/file_WebRequest_page1.html
toolkit/components/extensions/test/mochitest/file_WebRequest_page2.html
toolkit/components/extensions/test/mochitest/head_webrequest.js
toolkit/components/extensions/test/mochitest/mochitest.ini
toolkit/components/extensions/test/mochitest/test_ext_webrequest.html
toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html
toolkit/modules/addons/WebRequest.jsm
--- a/toolkit/components/extensions/test/mochitest/.eslintrc.js
+++ b/toolkit/components/extensions/test/mochitest/.eslintrc.js
@@ -14,14 +14,22 @@ module.exports = { // eslint-disable-lin
 
     "waitForLoad": true,
     "promiseConsoleOutput": true,
 
     "ExtensionTestUtils": false,
     "NetUtil": true,
     "webrequest_test": false,
     "XPCOMUtils": true,
+
+    // head_webrequest.js symbols
+    "addStylesheet": true,
+    "addLink": true,
+    "addImage": true,
+    "addScript": true,
+    "addFrame": true,
+    "makeExtension": false,
   },
 
   "rules": {
     "no-shadow": 0,
   },
 };
deleted file mode 100644
--- a/toolkit/components/extensions/test/mochitest/file_WebRequest_page1.html
+++ /dev/null
@@ -1,43 +0,0 @@
-<!DOCTYPE HTML>
-
-<html>
-<head>
-<meta charset="utf-8">
-<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">
-
-<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>
-<iframe src="data:text/plain,webRequestTest_bad" width="200" height="200"></iframe>
-<iframe src="https://invalid.localhost/" width="200" height="200"></iframe>
-<a href="file_WebRequest_page3.html?trigger=a" target="webrequest_link">link</a>
-<form method="post" action="file_WebRequest_page3.html?trigger=form" target="webrequest_form"></form>
-<script>
-"use strict";
-for (let a of document.links) {
-  a.click();
-}
-for (let f of document.forms) {
-  f.submit();
-}
-</script>
-</body>
-</html>
deleted file mode 100644
--- a/toolkit/components/extensions/test/mochitest/file_WebRequest_page2.html
+++ /dev/null
@@ -1,25 +0,0 @@
-<!DOCTYPE HTML>
-
-<html>
-<head>
-<meta charset="utf-8">
-<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 class="test">Sample text</div>
-
-<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="nonexistent_script_url.js"></script>
-
-</body>
-</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/head_webrequest.js
@@ -0,0 +1,315 @@
+"use strict";
+
+let commonEvents = {
+  "onBeforeRequest":     [{urls: ["<all_urls>"]}, ["blocking"]],
+  "onBeforeSendHeaders": [{urls: ["<all_urls>"]}, ["blocking", "requestHeaders"]],
+  "onSendHeaders":       [{urls: ["<all_urls>"]}, ["requestHeaders"]],
+  "onBeforeRedirect":    [{urls: ["<all_urls>"]}],
+  "onHeadersReceived":   [{urls: ["<all_urls>"]}, ["blocking", "responseHeaders"]],
+  "onResponseStarted":   [{urls: ["<all_urls>"]}],
+  "onCompleted":         [{urls: ["<all_urls>"]}, ["responseHeaders"]],
+  "onErrorOccurred":     [{urls: ["<all_urls>"]}],
+};
+
+function background(events) {
+  let expect;
+  let defaultOrigin;
+
+  browser.test.onMessage.addListener((msg, expected) => {
+    if (msg !== "set-expected") {
+      return;
+    }
+    expect = expected.expect;
+    defaultOrigin = expected.origin;
+    let promises = [];
+    // Initialize some stuff we'll need in the tests.
+    for (let entry of Object.values(expect)) {
+      // a place for the test infrastructure to store some state.
+      entry.test = {};
+      // Each entry in expected gets a Promise that will be resolved in the
+      // last event for that entry.  This will either be onCompleted, or the
+      // last entry if an events list was provided.
+      promises.push(new Promise(resolve => { entry.test.resolve = resolve; }));
+      // If events was left undefined, we're expecting all normal events we're
+      // listening for, exclude onBeforeRedirect and onErrorOccurred
+      if (entry.events === undefined) {
+        entry.events = Object.keys(events).filter(name => name != "onErrorOccurred" && name != "onBeforeRedirect");
+      }
+    }
+    // When every expected entry has finished our test is done.
+    Promise.all(promises).then(() => {
+      browser.test.sendMessage("done");
+    });
+    browser.test.sendMessage("continue");
+  });
+
+  // Retrieve the per-file/test expected values.
+  function getExpected(details) {
+    let url = new URL(details.url);
+    let filename;
+    if (url.protocol == "data:") {
+      // pathname is everything after protocol.
+      filename = url.pathname;
+    } else {
+      filename = url.pathname.split("/").pop();
+    }
+    let expected = expect[filename];
+    if (!expected) {
+      browser.test.fail(`unexpected request ${filename}`);
+      return;
+    }
+    // Save filename for redirect verification.
+    expected.test.filename = filename;
+    return expected;
+  }
+
+  // Process any test header modifications that can happen in request or response phases.
+  // If a test includes headers, it needs a complete header object, no undefined
+  // objects even if empty:
+  // request: {
+  //   add: {"HeaderName": "value",},
+  //   modify: {"HeaderName": "value",},
+  //   remove: ["HeaderName",],
+  // },
+  // response: {
+  //   add: {"HeaderName": "value",},
+  //   modify: {"HeaderName": "value",},
+  //   remove: ["HeaderName",],
+  // },
+  function processHeaders(phase, expected, details) {
+    // This should only happen once per phase [request|response].
+    browser.test.assertFalse(!!expected.test[phase], `First processing of headers for ${phase}`);
+    expected.test[phase] = true;
+
+    let headers = details[`${phase}Headers`];
+    browser.test.assertTrue(Array.isArray(headers), `${phase}Headers array present`);
+
+    let {add, modify, remove} = expected.headers[phase];
+
+    for (let name in add) {
+      browser.test.assertTrue(!headers.find(h => h.name === name), `header ${name} to be added not present yet in ${phase}Headers`);
+      let header = {name: name};
+      if (name.endsWith("-binary")) {
+        header.binaryValue = Array.from(add[name], c => c.charCodeAt(0));
+      } else {
+        header.value = add[name];
+      }
+      headers.push(header);
+    }
+
+    let modifiedAny = false;
+    for (let header of headers) {
+      if (header.name.toLowerCase() in modify) {
+        header.value = modify[header.name.toLowerCase()];
+        modifiedAny = true;
+      }
+    }
+    browser.test.assertTrue(modifiedAny, `at least one ${phase}Headers element to modify`);
+
+    let deletedAny = false;
+    for (let j = headers.length; j-- > 0;) {
+      if (remove.includes(headers[j].name.toLowerCase())) {
+        headers.splice(j, 1);
+        deletedAny = true;
+      }
+    }
+    browser.test.assertTrue(deletedAny, `at least one ${phase}Headers element to delete`);
+
+    return headers;
+  }
+
+  // phase is request or response.
+  function checkHeaders(phase, expected, details) {
+    if (!/^https?:/.test(details.url)) {
+      return;
+    }
+
+    let headers = details[`${phase}Headers`];
+    browser.test.assertTrue(Array.isArray(headers), `valid ${phase}Headers array`);
+
+    let {add, modify, remove} = expected.headers[phase];
+    for (let name in add) {
+      let value = headers.find(h => h.name.toLowerCase() === name.toLowerCase()).value;
+      browser.test.assertEq(value, add[name], `header ${name} correctly injected in ${phase}Headers`);
+    }
+
+    for (let name in modify) {
+      let value = headers.find(h => h.name.toLowerCase() === name.toLowerCase()).value;
+      browser.test.assertEq(value, modify[name], `header ${name} matches modified value`);
+    }
+
+    for (let name of remove) {
+      let found = headers.find(h => h.name.toLowerCase() === name.toLowerCase());
+      browser.test.assertFalse(!!found, `deleted header ${name} still found in ${phase}Headers`);
+    }
+  }
+
+  function getListener(name) {
+    return details => {
+      let result = {};
+      browser.test.log(`${name} ${details.requestId} ${details.url}`);
+      let expected = getExpected(details);
+      if (!expected) {
+        return result;
+      }
+      let expectedEvent = expected.events[0] == name;
+      browser.test.assertTrue(expectedEvent, `recieved ${name}`);
+      if (expectedEvent) {
+        expected.events.shift();
+      }
+      browser.test.assertEq(expected.type, details.type, "resource type is correct");
+      browser.test.assertEq(expected.origin || defaultOrigin, details.originUrl, "origin is correct");
+
+      if (name == "onBeforeRequest") {
+        // Save some values to test request consistency in later events.
+        browser.test.assertTrue(details.tabId !== undefined, `tabId ${details.tabId}`);
+        browser.test.assertTrue(details.requestId !== undefined, `requestId ${details.requestId}`);
+        // Validate requestId if it's already set, this happens with redirects.
+        if (expected.test.requestId !== undefined) {
+          browser.test.assertEq("string", typeof expected.test.requestId, `requestid ${expected.test.requestId} is string`);
+          browser.test.assertEq("string", typeof details.requestId, `requestid ${details.requestId} is string`);
+          browser.test.assertEq("number", typeof parseInt(details.requestId, 10), "parsed requestid is number");
+          browser.test.assertNotEq(expected.test.requestId, details.requestId,
+                                  `last requestId ${expected.test.requestId} different from this one ${details.requestId}`);
+        } else {
+          // Save any values we want to validate in later events.
+          expected.test.requestId = details.requestId;
+          expected.test.tabId = details.tabId;
+        }
+        // Tests we don't need to do every event.
+        browser.test.assertTrue(details.type.toUpperCase() in browser.webRequest.ResourceType, `valid resource type ${details.type}`);
+      } else {
+        // On events after onBeforeRequest, check the previous values.
+        browser.test.assertEq(expected.test.requestId, details.requestId, "correct requestId");
+        browser.test.assertEq(expected.test.tabId, details.tabId, "correct tabId");
+      }
+      if (name == "onBeforeSendHeaders") {
+        if (expected.headers && expected.headers.request) {
+          result.requestHeaders = processHeaders("request", expected, details);
+        }
+        if (expected.redirect) {
+          browser.test.log(`${name} redirect request`);
+          result.redirectUrl = details.url.replace(expected.test.filename, expected.redirect);
+        }
+      }
+      if (name == "onSendHeaders") {
+        if (expected.headers && expected.headers.request) {
+          checkHeaders("request", expected, details);
+        }
+      }
+      if (name == "onHeadersReceived") {
+        browser.test.assertEq(expected.status || 200, details.statusCode,
+                              `expected HTTP status recieved for ${details.url}`);
+        if (expected.headers && expected.headers.response) {
+          result.responseHeaders = processHeaders("response", expected, details);
+        }
+      }
+      if (name == "onCompleted") {
+        // If we have already completed a GET request for this url,
+        // and it was found, we expect for the response to come fromCache.
+        // expected.cached may be undefined, force boolean.
+        let expectCached = !!expected.cached && details.method === "GET" && details.statusCode != 404;
+        browser.test.assertEq(expectCached, details.fromCache, "fromCache is correct");
+        // We can only tell IPs for non-cached HTTP requests.
+        if (!details.fromCache && /^https?:/.test(details.url)) {
+          browser.test.assertEq("127.0.0.1", details.ip, `correct ip for ${details.url}`);
+        }
+        if (expected.headers && expected.headers.response) {
+          checkHeaders("response", expected, details);
+        }
+      }
+
+      if (expected.cancel && expected.cancel == name) {
+        browser.test.log(`${name} cancel request`);
+        browser.test.sendMessage("cancelled");
+        result.cancel = true;
+      }
+      // If we've used up all the events for this test, resolve the promise.
+      // If something wrong happens and more events come through, there will be
+      // failures.
+      if (expected.events.length <= 0) {
+        expected.test.resolve();
+      }
+      return result;
+    };
+  }
+
+  for (let [name, args] of Object.entries(events)) {
+    browser.test.log(`adding listener for ${name}`);
+    try {
+      browser.webRequest[name].addListener(getListener(name), ...args);
+    } catch (e) {
+      browser.test.assertTrue(/\brequestBody\b/.test(e.message),
+                              "Request body is unsupported");
+
+      // RequestBody is disabled in release builds.
+      if (!/\brequestBody\b/.test(e.message)) {
+        throw e;
+      }
+
+      args.splice(args.indexOf("requestBody"), 1);
+      browser.webRequest[name].addListener(getListener(name), ...args);
+    }
+  }
+}
+
+/* exported makeExtension */
+
+function makeExtension(events = commonEvents) {
+  return ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: [
+        "webRequest",
+        "webRequestBlocking",
+        "<all_urls>",
+      ],
+    },
+    background: `(${background})(${JSON.stringify(events)})`,
+  });
+}
+
+/* exported addStylesheet */
+
+function addStylesheet(file) {
+  let link = document.createElement("link");
+  link.setAttribute("rel", "stylesheet");
+  link.setAttribute("href", file);
+  document.body.appendChild(link);
+}
+
+/* exported addLink */
+
+function addLink(file) {
+  let a = document.createElement("a");
+  a.setAttribute("href", file);
+  a.setAttribute("target", "_blank");
+  document.body.appendChild(a);
+  return a;
+}
+
+/* exported addImage */
+
+function addImage(file) {
+  let img = document.createElement("img");
+  img.setAttribute("src", file);
+  document.body.appendChild(img);
+}
+
+/* exported addScript */
+
+function addScript(file) {
+  let script = document.createElement("script");
+  script.setAttribute("type", "text/javascript");
+  script.setAttribute("src", file);
+  document.getElementsByTagName("head").item(0).appendChild(script);
+}
+
+/* exported addFrame */
+
+function addFrame(file) {
+  let frame = document.createElement("iframe");
+  frame.setAttribute("width", "200");
+  frame.setAttribute("height", "200");
+  frame.setAttribute("src", file);
+  document.body.appendChild(frame);
+}
--- a/toolkit/components/extensions/test/mochitest/mochitest.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest.ini
@@ -1,16 +1,15 @@
 [DEFAULT]
 support-files =
   head.js
   file_mixed.html
+  head_webrequest.js
   file_csp.html
   file_csp.html^headers^
-  file_WebRequest_page1.html
-  file_WebRequest_page2.html
   file_WebRequest_page3.html
   file_webNavigation_clientRedirect.html
   file_webNavigation_clientRedirect_httpHeaders.html
   file_webNavigation_clientRedirect_httpHeaders.html^headers^
   file_webNavigation_frameClientRedirect.html
   file_webNavigation_frameRedirect.html
   file_webNavigation_manualSubframe.html
   file_webNavigation_manualSubframe_page1.html
@@ -88,19 +87,19 @@ skip-if = os == 'android' # Bug 1258975 
 [test_ext_background_teardown.html]
 [test_ext_tab_teardown.html]
 skip-if = (os == 'android') # Android does not support tabs API. Bug 1260250
 [test_ext_unload_frame.html]
 [test_ext_i18n.html]
 skip-if = (os == 'android') # Bug 1258975 on android.
 [test_ext_web_accessible_resources.html]
 skip-if = (os == 'android') # Bug 1258975 on android.
-[test_ext_webrequest.html]
+[test_ext_webrequest_background_events.html]
 skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
-[test_ext_webrequest_background_events.html]
+[test_ext_webrequest_basic.html]
 skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
 [test_ext_webrequest_suspend.html]
 skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
 [test_ext_webrequest_upload.html]
 skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
 [test_ext_webnavigation.html]
 skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
 [test_ext_webnavigation_filters.html]
deleted file mode 100644
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest.html
+++ /dev/null
@@ -1,548 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <title>Test for simple WebExtension</title>
-  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
-  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
-  <script type="text/javascript" src="head.js"></script>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-</head>
-<body>
-
-<script type="text/javascript">
-"use strict";
-
-SimpleTest.requestCompleteLog();
-
-const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
-
-const expected_requested = [BASE + "/file_WebRequest_page1.html",
-                            BASE + "/file_style_good.css",
-                            BASE + "/file_style_bad.css",
-                            BASE + "/file_style_redirect.css",
-                            BASE + "/file_image_good.png",
-                            BASE + "/file_image_bad.png",
-                            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 + "/dummy_page.html",
-                            BASE + "/xhr_resource",
-                            "https://invalid.localhost/",
-                            "data:text/plain,webRequestTest_bad",
-                            "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",
-                              BASE + "/file_script_xhr.js",
-                              BASE + "/file_WebRequest_page2.html",
-                              BASE + "/nonexistent_script_url.js",
-                              BASE + "/redirection.sjs",
-                              BASE + "/dummy_page.html",
-                              BASE + "/xhr_resource",
-                              "https://invalid.localhost/"];
-
-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_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_error = expected_requested.filter(u => /_bad\b|\binvalid\b/.test(u));
-
-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];
-      }
-    }
-  }
-  list.length = j + 1;
-}
-
-function compareLists(list1, list2, kind) {
-  list1.sort();
-  removeDupes(list1);
-  list2.sort();
-  removeDupes(list2);
-  is(String(list1), String(list2), `${kind} URLs correct`);
-}
-
-function backgroundScript() {
-  let checkCompleted = true;
-  let savedTabId = -1;
-
-  function shouldRecord(url) {
-    return url.startsWith(BASE) && !url.includes("_page3.html") ||
-           /^data:.*\bwebRequestTest|\/invalid\./.test(url);
-  }
-
-  let statuses = [
-    {url: /_script_good\b/, code: 200, line: /^HTTP\/1.1 200 OK\b/i},
-    {url: /\bredirection\b/, code: 302, line: /^HTTP\/1.1 302\b/},
-    {url: /\bnonexistent_script_/, code: 404, line: /^HTTP\/1.1 404 Not Found\b/i},
-  ];
-  function checkStatus(details) {
-    for (let {url, code, line} of statuses) {
-      if (url.test(details.url)) {
-        browser.test.assertEq(code, details.statusCode, `HTTP status code ${code} for ${details.url} (found ${details.statusCode})`);
-        browser.test.assertTrue(line.test(details.statusLine), `HTTP status line ${line} for ${details.url} (found ${details.statusLine})`);
-      }
-    }
-  }
-
-  function checkOrigin(details) {
-    let isCorrectOrigin = details.url.includes("_page1.html") ? details.originUrl.endsWith("/test_ext_webrequest.html")
-                                                              : /\/file_WebRequest_page\d\.html\b/.test(details.originUrl);
-    browser.test.assertTrue(isCorrectOrigin, `originUrl for ${details.url} is correct (${details.originUrl})`);
-  }
-
-  function checkType(details) {
-    let expected_type = "???";
-    if (details.url.includes("style")) {
-      expected_type = "stylesheet";
-    } else if (details.url.includes("image")) {
-      expected_type = "image";
-    } else if (details.url.includes("script")) {
-      expected_type = "script";
-    } else if (details.url.includes("page1")) {
-      expected_type = "main_frame";
-    } else if (/page2|redirection|dummy_page|data:text\/(?:plain|html),|\/\/invalid\b/.test(details.url)) {
-      expected_type = "sub_frame";
-    } else if (details.url.includes("xhr")) {
-      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} in ${event} (${details.requestId} in [${ids && [...ids].join(", ")}])`);
-    if (ids && idDisposalEvents.has(event)) {
-      ids.delete(details.requestId);
-    }
-  }
-
-  let frameIDs = new Map();
-  let skippedRequests = new Set();
-  let redirectedRequests = new Set();
-
-  let recorded = {requested: [],
-                  beforeSendHeaders: [],
-                  beforeRedirect: [],
-                  sendHeaders: [],
-                  responseStarted: [],
-                  responseStarted2: [],
-                  error: [],
-                  completed: [],
-                 };
-  let testHeaders = {
-    request: {
-      added: {
-        "X-WebRequest-request": "text",
-        "X-WebRequest-request-binary": "binary",
-      },
-      modified: {
-        "user-agent": "WebRequest",
-      },
-      deleted: [
-        "referer",
-      ],
-    },
-    response: {
-      added: {
-        "X-WebRequest-response": "text",
-        "X-WebRequest-response-binary": "binary",
-      },
-      modified: {
-        "server": "WebRequest",
-        "content-type": "text/html; charset=utf-8",
-      },
-      deleted: [
-        "connection",
-      ],
-    },
-  };
-
-  function checkResourceType(type) {
-    let key = type.toUpperCase();
-    browser.test.assertTrue(key in browser.webRequest.ResourceType, `valid resource type ${key}`);
-  }
-
-  function processHeaders(phase, details) {
-    let headers = details[`${phase}Headers`];
-    browser.test.assertTrue(Array.isArray(headers), `${phase}Headers array present`);
-
-    let processedMark = "webrequest-processed";
-    if (headers.find(h => h.name.toLowerCase() === processedMark)) {
-      // This may happen because of redirections or cache
-      browser.test.log(`${phase}Headers in ${details.requestId} already processed`);
-      skippedRequests.add(details.requestId);
-      return null;
-    }
-    headers.push({name: processedMark, value: "1"});
-
-    let {added, modified, deleted} = testHeaders[phase];
-
-    for (let name in added) {
-      browser.test.assertTrue(!headers.find(h => h.name === name), `header ${name} to be added not present yet in ${phase}Headers`);
-      let header = {name: name};
-      if (name.endsWith("-binary")) {
-        header.binaryValue = Array.from(added[name], c => c.charCodeAt(0));
-      } else {
-        header.value = added[name];
-      }
-      headers.push(header);
-    }
-
-    let modifiedAny = false;
-    for (let header of headers) {
-      if (header.name.toLowerCase() in modified) {
-        header.value = modified[header.name.toLowerCase()];
-        modifiedAny = true;
-      }
-    }
-    browser.test.assertTrue(modifiedAny, `at least one ${phase}Headers element to modify`);
-
-    let deletedAny = false;
-    for (let j = headers.length; j-- > 0;) {
-      if (deleted.includes(headers[j].name.toLowerCase())) {
-        headers.splice(j, 1);
-        deletedAny = true;
-      }
-    }
-    browser.test.assertTrue(deletedAny, `at least one ${phase}Headers element to delete`);
-
-    return headers;
-  }
-
-  function checkHeaders(phase, details) {
-    if (!/^https?:/.test(details.url)) {
-      return;
-    }
-
-    let headers = details[`${phase}Headers`];
-    browser.test.assertTrue(Array.isArray(headers), `valid ${phase}Headers array`);
-
-    let {added, modified, deleted} = testHeaders[phase];
-    for (let name in added) {
-      browser.test.assertTrue(headers.some(h => h.name.toLowerCase() === name.toLowerCase() && h.value === added[name]), `header ${name} correctly injected in ${phase}Headers`);
-    }
-
-    let modifiedAny = false;
-    for (let header of headers.filter(h => h.name in modified)) {
-      let {name, value} = header;
-      if (name.toLowerCase() === "content-type" && skippedRequests.has(details.requestId)) {
-        // Changes to Content-Type headers are not persisted in the cache.
-        continue;
-      }
-
-      browser.test.assertTrue(value === modified[name], `header "${name}: ${value}" matches modified value ("${modified[name]}")`);
-      modifiedAny = true;
-    }
-    browser.test.assertTrue(modifiedAny, `at least one modified ${phase}Headers element`);
-
-    for (let name of deleted) {
-      browser.test.assertFalse(headers.some(h => h.name === name), `deleted header ${name} still found in ${phase}Headers`);
-    }
-  }
-
-  let lastRequestId = -1;
-  let lastRequestUrl = null;
-  function validateRequestIdType(currentId) {
-    browser.test.assertTrue(typeof lastRequestId === "string");
-    browser.test.assertTrue(typeof currentId === "string");
-    browser.test.assertTrue(typeof parseInt(currentId, 10) === "number");
-    browser.test.assertTrue(parseInt(lastRequestId, 10) !== parseInt(currentId, 10));
-  }
-
-  function onBeforeRequest(details) {
-    browser.test.log(`onBeforeRequest ${details.requestId} ${details.url}`);
-
-    if (!lastRequestUrl) {
-      lastRequestUrl = details.url;
-      lastRequestId = details.requestId;
-    } else if (lastRequestUrl != details.url) {
-      validateRequestIdType(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);
-    checkOrigin(details);
-    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");
-      checkType(details);
-
-      frameIDs.set(details.url, details.frameId);
-      if (details.url.includes("page1")) {
-        browser.test.assertEq(details.frameId, 0, "frame ID correct");
-        browser.test.assertEq(details.parentFrameId, -1, "parent frame ID correct");
-      }
-      if (details.url.includes("page2")) {
-        browser.test.assertTrue(details.frameId != 0, "sub-frame gets its own frame ID");
-        browser.test.assertTrue(details.frameId !== undefined, "sub-frame ID defined");
-        browser.test.assertEq(details.parentFrameId, 0, "parent frame id is correct");
-      }
-    }
-    if (details.url.includes("_bad")) {
-      return {cancel: true};
-    }
-    return {};
-  }
-
-  function onBeforeSendHeaders(details) {
-    browser.test.log(`onBeforeSendHeaders ${details.url}`);
-    checkRequestId(details);
-    checkOrigin(details);
-    checkResourceType(details.type);
-    processHeaders("request", details);
-    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.includes("_redirect.")) {
-      redirectedRequests.add(details.requestId);
-      return {redirectUrl: details.url.replace("_redirect.", "_good.")};
-    }
-    return {requestHeaders: details.requestHeaders};
-  }
-
-  function onBeforeRedirect(details) {
-    browser.test.log(`onBeforeRedirect ${details.url} -> ${details.redirectUrl}`);
-    checkRequestId(details, "redirect");
-    checkOrigin(details);
-    checkResourceType(details.type);
-    if (shouldRecord(details.url)) {
-      recorded.beforeRedirect.push(details.url);
-
-      browser.test.assertEq(details.tabId, savedTabId, "correct tab ID");
-      checkType(details);
-      checkStatus(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);
-    }
-    if (details.url.includes("_redirect.")) {
-      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.requestId} ${details.url}`);
-    checkResourceType(details.type);
-    checkRequestId(details, kind);
-    checkOrigin(details);
-    if (kind in recorded && shouldRecord(details.url)) {
-      recorded[kind].push(details.url);
-    }
-  }
-
-  function onSendHeaders(details) {
-    onRecord("sendHeaders", details);
-    checkHeaders("request", details);
-  }
-
-  let completedUrls = {};
-
-  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 (!(kind in completedUrls)) {
-      completedUrls[kind] = new Set();
-    }
-    if (checkCompleted && !completedUrls[kind].has(details.url)) {
-      // 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);
-    }
-    checkStatus(details);
-  }
-
-  function checkFromCache(kind, details) {
-    if (checkCompleted) {
-      // If we have already completed a GET request for this url,
-      // and it was found, we expect for the response to come fromCache.
-      const completed = kind in completedUrls && completedUrls[kind].has(details.url);
-      const expected = completed && details.method === "GET" && details.statusCode != 404;
-      browser.test.assertEq(expected, details.fromCache, "fromCache is correct");
-    }
-    checkIpAndRecord(kind, details);
-  }
-
-  function onHeadersReceived(details) {
-    checkIpAndRecord("headersReceived", details);
-    processHeaders("response", details);
-    browser.test.log(`After processing response headers: ${details.responseHeaders.toSource()}`);
-    return {responseHeaders: details.responseHeaders};
-  }
-
-  function onErrorOccurred(details) {
-    if (details.url.endsWith("_good.png") && redirectedRequests.has(details.requestId)) {
-      // Redirected image requests sometimes result in multiple attempts to
-      // load the same image in parallel. In this case, the later request is
-      // canceled, and the same image loading context is shared by both images.
-      redirectedRequests.delete(details.requestId);
-      browser.test.assertEq("NS_BINDING_ABORTED", details.error, `onErrorOccurred reported for ${details.url}`);
-    } else {
-      onRecord("error", details);
-      browser.test.assertTrue(/^NS_ERROR_/.test(details.error), `onErrorOccurred reported for ${details.url} (${details.error})`);
-    }
-  }
-
-  function onCompleted(details) {
-    checkFromCache("completed", details);
-    checkHeaders("response", details);
-  }
-
-  browser.webRequest.onBeforeRequest.addListener(onBeforeRequest, {urls: ["<all_urls>"]}, ["blocking"]);
-  browser.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, {urls: ["<all_urls>"]}, ["blocking", "requestHeaders"]);
-  browser.webRequest.onSendHeaders.addListener(onSendHeaders, {urls: ["<all_urls>"]}, ["requestHeaders"]);
-  browser.webRequest.onBeforeRedirect.addListener(onBeforeRedirect, {urls: ["<all_urls>"]});
-  browser.webRequest.onHeadersReceived.addListener(onHeadersReceived, {urls: ["<all_urls>"]}, ["blocking", "responseHeaders"]);
-  browser.webRequest.onResponseStarted.addListener(checkFromCache.bind(null, "responseStarted"), {urls: ["<all_urls>"]});
-  browser.webRequest.onResponseStarted.addListener(checkFromCache.bind(null, "responseStarted2"), {urls: ["<all_urls>"]});
-  browser.webRequest.onErrorOccurred.addListener(onErrorOccurred, {urls: ["<all_urls>"]});
-  browser.webRequest.onCompleted.addListener(onCompleted, {urls: ["<all_urls>"]}, ["responseHeaders"]);
-
-  function onTestMessage(msg) {
-    if (msg == "skipCompleted") {
-      checkCompleted = false;
-      browser.test.sendMessage("ackSkipCompleted");
-    } else {
-      browser.test.sendMessage("results", recorded);
-    }
-  }
-
-  browser.test.onMessage.addListener(onTestMessage);
-
-  browser.test.sendMessage("ready", browser.webRequest.ResourceType);
-}
-
-function* test_once(skipCompleted) {
-  let extensionData = {
-    manifest: {
-      permissions: [
-        "webRequest",
-        "webRequestBlocking",
-      ],
-    },
-    background: `const BASE = ${JSON.stringify(BASE)}; (${backgroundScript})()`,
-  };
-
-  let extension = ExtensionTestUtils.loadExtension(extensionData);
-  yield extension.startup();
-  let resourceTypes = yield extension.awaitMessage("ready");
-  info("webrequest extension loaded");
-
-  if (skipCompleted) {
-    extension.sendMessage("skipCompleted");
-    yield extension.awaitMessage("ackSkipCompleted");
-  }
-
-  for (let key in resourceTypes) {
-    let value = resourceTypes[key];
-    is(key, value.toUpperCase());
-  }
-
-  // Check a few Firefox-specific types.
-  is(resourceTypes.XBL, "xbl", "XBL resource type supported");
-  is(resourceTypes.FONT, "font", "Font resource type supported");
-  is(resourceTypes.WEBSOCKET, "websocket", "Websocket resource type supported");
-
-  yield new Promise(resolve => { setTimeout(resolve, 0); });
-
-  let win = window.open();
-
-  // Clear the image cache, since it gets in the way otherwise.
-  let imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools);
-  let cache = imgTools.getImgCacheForDocument(win.document);
-  cache.clearCache(false);
-
-  // yield waitForLoad(win);
-  info("about:blank loaded");
-
-  win.location = "file_WebRequest_page1.html";
-
-  yield waitForLoad(win);
-  info("test page loaded");
-
-  is(win.success, 2, "Good script ran");
-  is(win.failure, undefined, "Failure script didn't run");
-
-  let style = win.getComputedStyle(win.document.getElementById("test"), null);
-  is(style.getPropertyValue("color"), "rgb(255, 0, 0)", "Good CSS loaded");
-
-  win.close();
-
-  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_response, "responseStarted");
-  compareLists(recorded.error, expected_error, "error");
-  compareLists(recorded.completed, expected_complete, "completed");
-  compareLists(recorded.responseStarted2, recorded.responseStarted, "multiple non-blocking listeners");
-  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); });
-add_task(function* () { yield test_once(true); });
-</script>
-
-</body>
-</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html
@@ -0,0 +1,282 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <script type="text/javascript" src="head_webrequest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+let extension;
+add_task(function* setup() {
+  // Clear the image cache, since it gets in the way otherwise.
+  let imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools);
+  let cache = imgTools.getImgCacheForDocument(document);
+  cache.clearCache(false);
+
+  extension = makeExtension();
+  yield extension.startup();
+});
+
+// expect is a set of test values used by the background script.
+//
+// type: type of request action
+// events: optional, If defined only the events listed are expected for the
+//                   request. If undefined, all events except onErrorOccurred
+//                   and onBeforeRedirect are expected.  Must be in order received.
+// redirect: url to redirect to during onBeforeSendHeaders
+// status: number    expected status during onHeadersReceived, 200 default
+// cancel: event in which we return cancel=true.  cancelled message is sent.
+// cached: expected fromCache value, default is false, checked in onCompletion
+// headers: request or response headers to modify
+
+add_task(function* test_webRequest_links() {
+  let expect = {
+    "file_style_bad.css": {
+      type: "stylesheet",
+      events: ["onBeforeRequest", "onErrorOccurred"],
+      cancel: "onBeforeRequest",
+    },
+    "file_style_redirect.css": {
+      status: 302,
+      type: "stylesheet",
+      events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"],
+      redirect: "file_style_good.css",
+    },
+    "file_style_good.css": {
+      type: "stylesheet",
+    },
+  };
+  extension.sendMessage("set-expected", {expect, origin: location.href});
+  yield extension.awaitMessage("continue");
+  addStylesheet("file_style_bad.css");
+  yield extension.awaitMessage("cancelled");
+  // we redirect to style_good which completes the test
+  addStylesheet("file_style_redirect.css");
+  yield extension.awaitMessage("done");
+
+  let style = window.getComputedStyle(document.getElementById("test"), null);
+  is(style.getPropertyValue("color"), "rgb(255, 0, 0)", "Good CSS loaded");
+});
+
+add_task(function* test_webRequest_images() {
+  let expect = {
+    "file_image_bad.png": {
+      type: "image",
+      events: ["onBeforeRequest", "onErrorOccurred"],
+      cancel: "onBeforeRequest",
+    },
+    "file_image_redirect.png": {
+      status: 302,
+      type: "image",
+      events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"],
+      redirect: "file_image_good.png",
+    },
+    "file_image_good.png": {
+      type: "image",
+    },
+  };
+  extension.sendMessage("set-expected", {expect, origin: location.href});
+  yield extension.awaitMessage("continue");
+  addImage("file_image_bad.png");
+  yield extension.awaitMessage("cancelled");
+  // we redirect to image_good which completes the test
+  addImage("file_image_redirect.png");
+  yield extension.awaitMessage("done");
+});
+
+add_task(function* test_webRequest_scripts() {
+  let expect = {
+    "file_script_bad.js": {
+      type: "script",
+      events: ["onBeforeRequest", "onErrorOccurred"],
+      cancel: "onBeforeRequest",
+    },
+    "file_script_redirect.js": {
+      status: 302,
+      type: "script",
+      events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"],
+      redirect: "file_script_good.js",
+    },
+    "file_script_good.js": {
+      type: "script",
+    },
+  };
+  extension.sendMessage("set-expected", {expect, origin: location.href});
+  yield extension.awaitMessage("continue");
+  addScript("file_script_bad.js");
+  yield extension.awaitMessage("cancelled");
+  // we redirect to script_good which completes the test
+  addScript("file_script_redirect.js");
+  yield extension.awaitMessage("done");
+
+  is(window.success, 1, "Good script ran");
+  is(window.failure, undefined, "Failure script didn't run");
+});
+
+add_task(function* test_webRequest_xhr_get() {
+  let expect = {
+    "file_script_xhr.js": {
+      type: "script",
+    },
+    "xhr_resource": {
+      status: 404,
+      type: "xmlhttprequest",
+    },
+  };
+  extension.sendMessage("set-expected", {expect, origin: location.href});
+  yield extension.awaitMessage("continue");
+  addScript("file_script_xhr.js");
+  yield extension.awaitMessage("done");
+});
+
+add_task(function* test_webRequest_nonexistent() {
+  let expect = {
+    "nonexistent_script_url.js": {
+      status: 404,
+      type: "script",
+    },
+  };
+  extension.sendMessage("set-expected", {expect, origin: location.href});
+  yield extension.awaitMessage("continue");
+  addScript("nonexistent_script_url.js");
+  yield extension.awaitMessage("done");
+});
+
+add_task(function* test_webRequest_checkCached() {
+  let expect = {
+    "file_image_good.png": {
+      type: "image",
+      cached: true,
+    },
+    "file_script_good.js": {
+      type: "script",
+      cached: true,
+    },
+    "file_style_good.css": {
+      type: "stylesheet",
+      cached: true,
+    },
+    "nonexistent_script_url.js": {
+      status: 404,
+      type: "script",
+      cached: false,
+    },
+  };
+  extension.sendMessage("set-expected", {expect, origin: location.href});
+  yield extension.awaitMessage("continue");
+  addImage("file_image_good.png");
+  addScript("file_script_good.js");
+  addStylesheet("file_style_good.css");
+  addScript("nonexistent_script_url.js");
+  yield extension.awaitMessage("done");
+
+  is(window.success, 2, "Good script ran");
+  is(window.failure, undefined, "Failure script didn't run");
+});
+
+add_task(function* test_webRequest_headers() {
+  let expect = {
+    "file_script_nonexistent.js": {
+      type: "script",
+      status: 404,
+      headers: {
+        request: {
+          add: {
+            "X-WebRequest-request": "text",
+            "X-WebRequest-request-binary": "binary",
+          },
+          modify: {
+            "user-agent": "WebRequest",
+          },
+          remove: [
+            "referer",
+          ],
+        },
+        response: {
+          add: {
+            "X-WebRequest-response": "text",
+            "X-WebRequest-response-binary": "binary",
+          },
+          modify: {
+            "server": "WebRequest",
+            "content-type": "text/html; charset=utf-8",
+          },
+          remove: [
+            "connection",
+          ],
+        },
+      },
+      completion: "onCompleted",
+    },
+  };
+  extension.sendMessage("set-expected", {expect, origin: location.href});
+  yield extension.awaitMessage("continue");
+  addScript("file_script_nonexistent.js");
+  yield extension.awaitMessage("done");
+});
+
+add_task(function* test_webRequest_tabId() {
+  let expect = {
+    "file_WebRequest_page3.html": {
+      type: "main_frame",
+    },
+  };
+  extension.sendMessage("set-expected", {expect, origin: location.href});
+  yield extension.awaitMessage("continue");
+  let a = addLink("file_WebRequest_page3.html?trigger=a");
+  a.click();
+  yield extension.awaitMessage("done");
+});
+
+add_task(function* test_webRequest_frames() {
+  let expect = {
+    "text/plain,webRequestTest": {
+      type: "sub_frame",
+      events: ["onBeforeRequest", "onCompleted"],
+    },
+    "text/plain,webRequestTest_bad": {
+      type: "sub_frame",
+      events: ["onBeforeRequest", "onErrorOccurred"],
+      cancel: "onBeforeRequest",
+    },
+    "redirection.sjs": {
+      status: 302,
+      type: "sub_frame",
+      events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", "onBeforeRedirect"],
+    },
+    "dummy_page.html": {
+      type: "sub_frame",
+      status: 404,
+    },
+    "badrobot": {
+      type: "sub_frame",
+      status: 404,
+      events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onErrorOccurred"],
+    },
+  };
+  extension.sendMessage("set-expected", {expect, origin: location.href});
+  yield extension.awaitMessage("continue");
+  addFrame("data:text/plain,webRequestTest");
+  addFrame("data:text/plain,webRequestTest_bad");
+  yield extension.awaitMessage("cancelled");
+  addFrame("redirection.sjs");
+  addFrame("https://invalid.localhost/badrobot");
+  yield extension.awaitMessage("done");
+});
+
+add_task(function* teardown() {
+  yield extension.unload();
+});
+</script>
+</head>
+<body>
+<div id="test">Sample text</div>
+
+</body>
+</html>
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -190,17 +190,19 @@ class RequestHeaderChanger extends Heade
     try {
       this.channel.setRequestHeader(name, value, false);
     } catch (e) {
       Cu.reportError(new Error(`Error setting request header ${name}: ${e}`));
     }
   }
 
   visitHeaders(visitor) {
-    this.channel.visitRequestHeaders(visitor);
+    if (this.channel instanceof Ci.nsIHttpChannel) {
+      this.channel.visitRequestHeaders(visitor);
+    }
   }
 }
 
 class ResponseHeaderChanger extends HeaderChanger {
   setHeader(name, value) {
     try {
       if (name.toLowerCase() === "content-type" && value) {
         // The Content-Type header value can't be modified, so we
@@ -214,23 +216,25 @@ class ResponseHeaderChanger extends Head
         this.channel.setResponseHeader(name, value, false);
       }
     } catch (e) {
       Cu.reportError(new Error(`Error setting response header ${name}: ${e}`));
     }
   }
 
   visitHeaders(visitor) {
-    this.channel.visitResponseHeaders((name, value) => {
-      if (name.toLowerCase() === "content-type") {
-        value = getData(this.channel).contentType || value;
-      }
+    if (this.channel instanceof Ci.nsIHttpChannel) {
+      this.channel.visitResponseHeaders((name, value) => {
+        if (name.toLowerCase() === "content-type") {
+          value = getData(this.channel).contentType || value;
+        }
 
-      visitor(name, value);
-    });
+        visitor(name, value);
+      });
+    }
   }
 }
 
 var HttpObserverManager;
 
 var ContentPolicyManager = {
   policyData: new Map(),
   policies: new Map(),