Bug 1252596 - Implement webRequest.onErrorOccurred. r?kmag draft
authorGiorgio Maone <g.maone@informaction.com>
Mon, 07 Mar 2016 09:35:24 +0100
changeset 344064 47ed2637a323bed2179d56d32e3dd9134de87b2a
parent 343962 6202ade0e6d688ffb67932398e56cfc6fa04ceb3
child 516888 6cdc919fffd30fcb931c3157b49fe7f71e3fa5ce
push id13754
push userg.maone@informaction.com
push dateWed, 23 Mar 2016 21:27:19 +0000
reviewerskmag
bugs1252596
milestone48.0a1
Bug 1252596 - Implement webRequest.onErrorOccurred. r?kmag MozReview-Commit-ID: GFeFjKlsMAD
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/WebRequest.jsm
--- a/toolkit/components/extensions/ext-webRequest.js
+++ b/toolkit/components/extensions/ext-webRequest.js
@@ -8,17 +8,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/MatchPattern.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebRequest",
                                   "resource://gre/modules/WebRequest.jsm");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   SingletonEventManager,
   runSafeSync,
-  ignoreEvent,
 } = ExtensionUtils;
 
 // EventManager-like class specifically for WebRequest. Inherits from
 // SingletonEventManager. Takes care of converting |details| parameter
 // when invoking listeners.
 function WebRequestEventManager(context, eventName) {
   let name = `webRequest.${eventName}`;
   let register = (callback, filter, info) => {
@@ -48,17 +47,17 @@ function WebRequestEventManager(context,
 
       // Fills in tabId typically.
       let result = {};
       extensions.emit("fill-browser-data", data.browser, data2, result);
       if (result.cancel) {
         return;
       }
 
-      let optional = ["requestHeaders", "responseHeaders", "statusCode", "statusLine", "redirectUrl"];
+      let optional = ["requestHeaders", "responseHeaders", "statusCode", "statusLine", "error", "redirectUrl"];
       for (let opt of optional) {
         if (opt in data) {
           data2[opt] = data[opt];
         }
       }
 
       return runSafeSync(context, callback, data2);
     };
@@ -102,18 +101,16 @@ extensions.registerSchemaAPI("webRequest
   return {
     webRequest: {
       onBeforeRequest: new WebRequestEventManager(context, "onBeforeRequest").api(),
       onBeforeSendHeaders: new WebRequestEventManager(context, "onBeforeSendHeaders").api(),
       onSendHeaders: new WebRequestEventManager(context, "onSendHeaders").api(),
       onHeadersReceived: new WebRequestEventManager(context, "onHeadersReceived").api(),
       onBeforeRedirect: new WebRequestEventManager(context, "onBeforeRedirect").api(),
       onResponseStarted: new WebRequestEventManager(context, "onResponseStarted").api(),
+      onErrorOccurred: new WebRequestEventManager(context, "onErrorOccurred").api(),
       onCompleted: new WebRequestEventManager(context, "onCompleted").api(),
       handlerBehaviorChanged: function() {
         // TODO: Flush all caches.
       },
-
-      // TODO
-      onErrorOccurred: ignoreEvent(context, "webRequest.onErrorOccurred"),
     },
   };
 });
--- a/toolkit/components/extensions/test/mochitest/file_WebRequest_page1.html
+++ b/toolkit/components/extensions/test/mochitest/file_WebRequest_page1.html
@@ -21,11 +21,12 @@
 
 <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>
 </body>
 </html>
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest.html
@@ -28,31 +28,34 @@ const expected_requested = [BASE + "/fil
                             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"];
+                              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",
@@ -60,16 +63,18 @@ const expected_response = [BASE + "/file
                            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) {
@@ -88,68 +93,69 @@ function compareLists(list1, list2, kind
   is(String(list1), String(list2), `${kind} URLs correct`);
 }
 
 function backgroundScript() {
   let checkCompleted = true;
   let savedTabId = -1;
 
   function shouldRecord(url) {
-    return url.startsWith(BASE) || /^data:.*\bwebRequestTest\b/.test(url);
+    return url.startsWith(BASE) || /^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.assertTrue(code === details.statusCode, `HTTP status code ${code} for ${details.url} (found ${details.statusCode})`);
+        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 checkType(details) {
     let expected_type = "???";
-    if (details.url.indexOf("style") != -1) {
+    if (details.url.includes("style")) {
       expected_type = "stylesheet";
-    } else if (details.url.indexOf("image") != -1) {
+    } else if (details.url.includes("image")) {
       expected_type = "image";
-    } else if (details.url.indexOf("script") != -1) {
+    } else if (details.url.includes("script")) {
       expected_type = "script";
-    } else if (details.url.indexOf("page1") != -1) {
+    } else if (details.url.includes("page1")) {
       expected_type = "main_frame";
-    } else if (/page2|redirection|dummy_page|data:text\/(?:plain|html),/.test(details.url)) {
+    } else if (/page2|redirection|dummy_page|data:text\/(?:plain|html),|\/\/invalid\b/.test(details.url)) {
       expected_type = "sub_frame";
-    } else if (details.url.indexOf("xhr") != -1) {
+    } 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} (${details.requestId} in [${ids && [...ids].join(", ")}])`);
+    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 recorded = {requested: [],
                   beforeSendHeaders: [],
                   beforeRedirect: [],
                   sendHeaders: [],
                   responseStarted: [],
+                  error: [],
                   completed: []};
   let testHeaders = {
     request: {
       added: {
         "X-WebRequest-request": "text",
         "X-WebRequest-request-binary": "binary",
       },
       modified: {
@@ -281,27 +287,27 @@ function backgroundScript() {
         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.indexOf("page1") != -1) {
+      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.indexOf("page2") != -1) {
+      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.indexOf("_bad.") != -1) {
+    if (details.url.includes("_bad")) {
       return {cancel: true};
     }
     return {};
   }
 
   function onBeforeSendHeaders(details) {
     browser.test.log(`onBeforeSendHeaders ${details.url}`);
     checkRequestId(details);
@@ -311,17 +317,17 @@ function backgroundScript() {
       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) {
+    if (details.url.includes("_redirect.")) {
       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");
@@ -332,17 +338,17 @@ function backgroundScript() {
       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.indexOf("_redirect.") != -1) {
+    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}`);
@@ -379,27 +385,33 @@ function backgroundScript() {
 
   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) {
+    onRecord("error", details);
+    browser.test.assertTrue(/^NS_ERROR_/.test(details.error), `onErrorOccurred reported for ${details.url} (${details.error})`);
+  }
+
   function onCompleted(details) {
     checkIpAndRecord("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(checkIpAndRecord.bind(null, "responseStarted"), {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);
@@ -469,16 +481,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_response, "responseStarted");
+  compareLists(recorded.error, expected_error, "error");
   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/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -8,73 +8,76 @@ const EXPORTED_SYMBOLS = ["WebRequest"];
 
 /* exported WebRequest */
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 
+const {nsIHttpActivityObserver, nsISocketTransport} = Ci;
+
 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");
 
 function attachToChannel(channel, key, data) {
   if (channel instanceof Ci.nsIWritablePropertyBag2) {
-    let wrapper = {value: data};
-    wrapper.wrappedJSObject = wrapper;
+    let wrapper = {wrappedJSObject: data};
     channel.setPropertyAsInterface(key, wrapper);
   }
+  return data;
 }
 
 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 data && data.wrappedJSObject;
   }
   return null;
 }
 
+function getData(channel) {
+  const key = "mozilla.webRequest.data";
+  return extractFromChannel(channel, key) || attachToChannel(channel, key, {});
+}
+
 var RequestId = {
   count: 1,
-  KEY: "mozilla.webRequest.requestId",
   create(channel = null) {
     let id = (this.count++).toString();
     if (channel) {
-      attachToChannel(channel, this.KEY, id);
+      getData(channel).requestId = id;
     }
     return id;
   },
 
   get(channel) {
-    return channel && extractFromChannel(channel, this.KEY) || this.create(channel);
+    return channel && getData(channel).requestId || 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};
 }
 
-function parseExtra(extra, allowed) {
+function parseExtra(extra, allowed = []) {
   if (extra) {
     for (let ex of extra) {
       if (allowed.indexOf(ex) == -1) {
         throw new Error(`Invalid option ${ex}`);
       }
     }
   }
 
@@ -82,27 +85,30 @@ function parseExtra(extra, allowed) {
   for (let al of allowed) {
     if (extra && extra.indexOf(al) != -1) {
       result[al] = true;
     }
   }
   return result;
 }
 
-function mergeStatus(data, channel) {
+function mergeStatus(data, channel, event) {
   try {
     data.statusCode = channel.responseStatus;
     let statusText = channel.responseStatusText;
     let maj = {};
     let min = {};
     channel.QueryInterface(Ci.nsIHttpChannelInternal).getResponseVersion(maj, min);
     data.statusLine = `HTTP/${maj.value}.${min.value} ${data.statusCode} ${statusText}`;
   } catch (e) {
-    // NS_ERROR_NOT_AVAILABLE might be thrown.
-    Cu.reportError(e);
+    // NS_ERROR_NOT_AVAILABLE might be thrown if it's an internal redirect, happening before
+    // any actual HTTP traffic. Otherwise, let's report.
+    if (event !== "onRedirect" || e.result !== Cr.NS_ERROR_NOT_AVAILABLE) {
+      Cu.reportError(`webRequest Error: ${e} trying to merge status in ${event}@${channel.name}`);
+    }
   }
 }
 
 var HttpObserverManager;
 
 var ContentPolicyManager = {
   policyData: new Map(),
   policies: new Map(),
@@ -122,35 +128,39 @@ 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 listenerKind = "onStop";
       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(data);
-        if (response && response.cancel) {
-          return {cancel: true};
+        if (response) {
+          if (response.cancel) {
+            listenerKind = "onError";
+            data.error = "NS_ERROR_ABORT";
+            return {cancel: true};
+          }
+          // FIXME: Need to handle redirection here (for non-HTTP URIs only)
         }
-
-        // FIXME: Need to handle redirection here. (Bug 1163862)
       } catch (e) {
         Cu.reportError(e);
       } finally {
-        runLater(() => this.runChannelListener("onStop", data));
+        runLater(() => this.runChannelListener(listenerKind, data));
       }
     }
 
     return {};
   },
 
   runChannelListener(kind, data) {
     let listeners = HttpObserverManager.listeners[kind];
@@ -262,40 +272,50 @@ var ChannelEventSink = {
 };
 
 ChannelEventSink.init();
 
 HttpObserverManager = {
   modifyInitialized: false,
   examineInitialized: false,
   redirectInitialized: false,
+  activityInitialized: false,
+  needTracing: false,
 
   listeners: {
     opening: new Map(),
     modify: new Map(),
     afterModify: new Map(),
     headersReceived: new Map(),
     onRedirect: new Map(),
     onStart: new Map(),
+    onError: new Map(),
     onStop: new Map(),
   },
 
+  get activityDistributor() {
+    return Cc["@mozilla.org/network/http-activity-distributor;1"].getService(Ci.nsIHttpActivityDistributor);
+  },
+
   addOrRemove() {
     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");
     }
+    this.needTracing = this.listeners.onStart.size ||
+                       this.listeners.onError.size ||
+                       this.listeners.onStop.size;
 
-    let needExamine = this.listeners.headersReceived.size ||
-                      this.listeners.onStart.size ||
-                      this.listeners.onStop.size;
+    let needExamine = this.needTracing ||
+                      this.listeners.headersReceived.size;
+
     if (needExamine && !this.examineInitialized) {
       this.examineInitialized = true;
       Services.obs.addObserver(this, "http-on-examine-response", false);
       Services.obs.addObserver(this, "http-on-examine-cached-response", false);
       Services.obs.addObserver(this, "http-on-examine-merged-response", false);
     } else if (!needExamine && this.examineInitialized) {
       this.examineInitialized = false;
       Services.obs.removeObserver(this, "http-on-examine-response");
@@ -306,16 +326,25 @@ HttpObserverManager = {
     let needRedirect = this.listeners.onRedirect.size;
     if (needRedirect && !this.redirectInitialized) {
       this.redirectInitialized = true;
       ChannelEventSink.register();
     } else if (!needRedirect && this.redirectInitialized) {
       this.redirectInitialized = false;
       ChannelEventSink.unregister();
     }
+
+    let needActivity = this.listeners.onError.size;
+    if (needActivity && !this.activityInitialized) {
+      this.activityInitialized = true;
+      this.activityDistributor.addObserver(this);
+    } else if (!needActivity && this.activityInitialized) {
+      this.activityInitialized = false;
+      this.activityDistributor.removeObserver(this);
+    }
   },
 
   addListener(kind, callback, opts) {
     this.listeners[kind].set(callback, opts);
     this.addOrRemove();
   },
 
   removeListener(kind, callback) {
@@ -334,28 +363,32 @@ HttpObserverManager = {
                       .notificationCallbacks
                       .getInterface(Components.interfaces.nsILoadContext);
       } catch (e) {
         return null;
       }
     }
   },
 
-  getHeaders(channel, method) {
+  getHeaders(channel, method, event) {
     let headers = [];
     let visitor = {
       visitHeader(name, value) {
         headers.push({name, value});
       },
 
       QueryInterface: XPCOMUtils.generateQI([Ci.nsIHttpHeaderVisitor,
                                              Ci.nsISupports]),
     };
 
-    channel[method](visitor);
+    try {
+      channel[method](visitor);
+    } catch (e) {
+      Cu.reportError(`webRequest Error: ${e} trying to perform ${method} in ${event}@${channel.name}`);
+    }
     return headers;
   },
 
   replaceHeaders(headers, originalNames, setHeader) {
     let failures = new Set();
     // Start by clearing everything.
     for (let name of originalNames) {
       try {
@@ -393,24 +426,84 @@ HttpObserverManager = {
       case "http-on-examine-response":
       case "http-on-examine-cached-response":
       case "http-on-examine-merged-response":
         this.examine(channel, topic, data);
         break;
     }
   },
 
+  // We map activity values with tentative error names, e.g. "STATUS_RESOLVING" => "NS_ERROR_NET_ON_RESOLVING".
+  get activityErrorsMap() {
+    let prefix = /^(?:ACTIVITY_SUBTYPE_|STATUS_)/;
+    let map = new Map();
+    for (let iface of [nsIHttpActivityObserver, nsISocketTransport]) {
+      for (let c of Object.keys(iface).filter(name => prefix.test(name))) {
+        map.set(iface[c], c.replace(prefix, "NS_ERROR_NET_ON_"));
+      }
+    }
+    delete this.activityErrorsMap;
+    this.activityErrorsMap = map;
+    return this.activityErrorsMap;
+  },
+  GOOD_LAST_ACTIVITY: nsIHttpActivityObserver.ACTIVITY_SUBTYPE_RESPONSE_HEADER,
+  observeActivity(channel, activityType, activitySubtype /* , aTimestamp, aExtraSizeData, aExtraStringData */) {
+    let channelData = getData(channel);
+    let lastActivity = channelData.lastActivity || 0;
+    if (activitySubtype === nsIHttpActivityObserver.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE &&
+        lastActivity && lastActivity !== this.GOOD_LAST_ACTIVITY) {
+      let loadContext = this.getLoadContext(channel);
+      if (!this.errorCheck(channel, loadContext, channelData)) {
+        this.runChannelListener(channel, loadContext, "onError",
+                                {error: this.activityErrorsMap.get(lastActivity) ||
+                                        `NS_ERROR_NET_UNKNOWN_${lastActivity}`});
+      }
+    } else if (lastActivity !== this.GOOD_LAST_ACTIVITY &&
+               lastActivity !== nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE) {
+      channelData.lastActivity = activitySubtype;
+    }
+  },
+
   shouldRunListener(policyType, uri, filter) {
     return WebRequestCommon.typeMatches(policyType, filter.types) &&
            WebRequestCommon.urlMatches(uri, filter.urls);
   },
 
+  get resultsMap() {
+    delete this.resultsMap;
+    this.resultsMap = new Map(Object.keys(Cr).map(name => [Cr[name], name]));
+    return this.resultsMap;
+  },
+  maybeError(channel, extraData = null, channelData = null) {
+    if (!(extraData && extraData.error)) {
+      if (!Components.isSuccessCode(channel.status)) {
+        extraData = {error: this.resultsMap.get(channel.status)};
+      }
+    }
+    return extraData;
+  },
+  errorCheck(channel, loadContext, channelData = null) {
+    let errorData = this.maybeError(channel, null, channelData);
+    if (errorData) {
+      this.runChannelListener(channel, loadContext, "onError", errorData);
+    }
+    return errorData;
+  },
+
   runChannelListener(channel, loadContext, kind, extraData = null) {
-    if (channel.status === Cr.NS_ERROR_ABORT) {
-      return false;
+    if (this.activityInitialized) {
+      let channelData = getData(channel);
+      if (kind === "onError") {
+        if (channelData.errorNotified) {
+          return false;
+        }
+        channelData.errorNotified = true;
+      } else if (this.errorCheck(channel, loadContext, channelData)) {
+        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;
 
@@ -444,39 +537,40 @@ HttpObserverManager = {
         // The remoteAddress getter throws if the address is unavailable,
         // but ip is an optional property so just ignore the exception.
       }
 
       if (extraData) {
         Object.assign(data, extraData);
       }
       if (opts.requestHeaders) {
-        data.requestHeaders = this.getHeaders(channel, "visitRequestHeaders");
+        data.requestHeaders = this.getHeaders(channel, "visitRequestHeaders", kind);
         requestHeaderNames = data.requestHeaders.map(h => h.name);
       }
       if (opts.responseHeaders) {
-        data.responseHeaders = this.getHeaders(channel, "visitResponseHeaders");
+        data.responseHeaders = this.getHeaders(channel, "visitResponseHeaders", kind);
         responseHeaderNames = data.responseHeaders.map(h => h.name);
       }
       if (includeStatus) {
-        mergeStatus(data, channel);
+        mergeStatus(data, channel, kind);
       }
 
       let result = null;
       try {
         result = callback(data);
       } catch (e) {
         Cu.reportError(e);
       }
 
       if (!result || !opts.blocking) {
         return true;
       }
       if (result.cancel) {
         channel.cancel(Cr.NS_ERROR_ABORT);
+        this.errorCheck(channel, loadContext);
         return false;
       }
       if (result.redirectUrl) {
         channel.redirectTo(BrowserUtils.makeURI(result.redirectUrl));
         return false;
       }
       if (opts.requestHeaders && result.requestHeaders) {
         this.replaceHeaders(
@@ -502,17 +596,17 @@ HttpObserverManager = {
         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) {
+    if (this.needTracing) {
       if (channel instanceof Ci.nsITraceableChannel) {
         let responseStatus = channel.responseStatus;
         // skip redirections, https://bugzilla.mozilla.org/show_bug.cgi?id=728901#c8
         if (responseStatus < 300 || responseStatus >= 400) {
           let listener = new StartStopListener(this, loadContext);
           let orig = channel.setNewListener(listener);
           listener.orig = orig;
         }
@@ -569,33 +663,37 @@ 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 onErrorOccurred = new HttpEvent("onError");
 
 var WebRequest = {
   // 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,
 
   // http-on-examine-*observer.
   onHeadersReceived: onHeadersReceived,
 
-  // nsIChannelEventSink
+  // nsIChannelEventSink.
   onBeforeRedirect: onBeforeRedirect,
 
   // OnStartRequest channel listener.
   onResponseStarted: onResponseStarted,
 
   // OnStopRequest channel listener.
   onCompleted: onCompleted,
+
+  // nsIHttpActivityObserver.
+  onErrorOccurred: onErrorOccurred,
 };
 
 Services.ppmm.loadProcessScript("resource://gre/modules/WebRequestContent.js", true);