Bug 1482070 - Move NetworkResponseListener to its own file. r=Honza
authorAlexandre Poirot <poirot.alex@gmail.com>
Thu, 16 Aug 2018 08:57:54 -0700
changeset 487543 68bff34cbddd1703e706bce5124c4c358352702a
parent 487542 6871a98b8b0b27c70848cbd3241db7272a1ee60b
child 487544 202fb9c610d07d734245e8b621eda8f80e3d2250
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersHonza
bugs1482070
milestone63.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 1482070 - Move NetworkResponseListener to its own file. r=Honza Summary: Depends On D3605 Reviewers: Honza! Tags: #secure-revision Bug #: 1482070 Differential Revision: https://phabricator.services.mozilla.com/D3606 MozReview-Commit-ID: IH9R6N7auXO
devtools/server/actors/network-monitor/moz.build
devtools/server/actors/network-monitor/network-observer.js
devtools/server/actors/network-monitor/network-response-listener.js
--- a/devtools/server/actors/network-monitor/moz.build
+++ b/devtools/server/actors/network-monitor/moz.build
@@ -2,10 +2,11 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'channel-event-sink.js',
     'network-observer.js',
+    'network-response-listener.js',
     'stack-trace-collector.js',
 )
--- a/devtools/server/actors/network-monitor/network-observer.js
+++ b/devtools/server/actors/network-monitor/network-observer.js
@@ -1,38 +1,31 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft= javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const {Cc, Ci, Cm, Cr, components} = require("chrome");
-const ChromeUtils = require("ChromeUtils");
+const {Cc, Ci} = require("chrome");
 const Services = require("Services");
-const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+const flags = require("devtools/shared/flags");
 
 loader.lazyRequireGetter(this, "NetworkHelper",
-                         "devtools/shared/webconsole/network-helper");
+  "devtools/shared/webconsole/network-helper");
 loader.lazyRequireGetter(this, "DevToolsUtils",
-                         "devtools/shared/DevToolsUtils");
-loader.lazyRequireGetter(this, "flags",
-                         "devtools/shared/flags");
+  "devtools/shared/DevToolsUtils");
 loader.lazyRequireGetter(this, "NetworkThrottleManager",
-                         "devtools/shared/webconsole/throttle", true);
-loader.lazyRequireGetter(this, "CacheEntry",
-                         "devtools/shared/platform/cache-entry", true);
-loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
+  "devtools/shared/webconsole/throttle", true);
 loader.lazyServiceGetter(this, "gActivityDistributor",
-                         "@mozilla.org/network/http-activity-distributor;1",
-                         "nsIHttpActivityDistributor");
-loader.lazyRequireGetter(this, "ChannelEventSinkFactory",
-                         "devtools/server/actors/network-monitor/channel-event-sink",
-                         true);
+  "@mozilla.org/network/http-activity-distributor;1",
+  "nsIHttpActivityDistributor");
+loader.lazyRequireGetter(this, "NetworkResponseListener",
+  "devtools/server/actors/network-monitor/network-response-listener", true);
 
 // Network logging
 
 // The maximum uint32 value.
 const PR_UINT32_MAX = 4294967295;
 
 // HTTP status codes.
 const HTTP_MOVED_PERMANENTLY = 301;
@@ -96,500 +89,17 @@ function matchRequest(channel, filters) 
         // outerWindowID getter from browser.xml (non-remote <xul:browser>) may
         // throw when closing a tab while resources are still loading.
       }
     }
   }
 
   return false;
 }
-
-/**
- * The network response listener implements the nsIStreamListener and
- * nsIRequestObserver interfaces. This is used within the NetworkObserver feature
- * to get the response body of the request.
- *
- * The code is mostly based on code listings from:
- *
- *   http://www.softwareishard.com/blog/firebug/
- *      nsitraceablechannel-intercept-http-traffic/
- *
- * @constructor
- * @param object owner
- *        The response listener owner. This object needs to hold the
- *        |openResponses| object.
- * @param object httpActivity
- *        HttpActivity object associated with this request. See NetworkObserver
- *        for more information.
- */
-function NetworkResponseListener(owner, httpActivity) {
-  this.owner = owner;
-  this.receivedData = "";
-  this.httpActivity = httpActivity;
-  this.bodySize = 0;
-  // Indicates if the response had a size greater than response body limit.
-  this.truncated = false;
-  // Note that this is really only needed for the non-e10s case.
-  // See bug 1309523.
-  const channel = this.httpActivity.channel;
-  this._wrappedNotificationCallbacks = channel.notificationCallbacks;
-  channel.notificationCallbacks = this;
-}
-
-NetworkResponseListener.prototype = {
-  QueryInterface:
-    ChromeUtils.generateQI([Ci.nsIStreamListener, Ci.nsIInputStreamCallback,
-                            Ci.nsIRequestObserver, Ci.nsIInterfaceRequestor]),
-
-  // nsIInterfaceRequestor implementation
-
-  /**
-   * This object implements nsIProgressEventSink, but also needs to forward
-   * interface requests to the notification callbacks of other objects.
-   */
-  getInterface(iid) {
-    if (iid.equals(Ci.nsIProgressEventSink)) {
-      return this;
-    }
-    if (this._wrappedNotificationCallbacks) {
-      return this._wrappedNotificationCallbacks.getInterface(iid);
-    }
-    throw Cr.NS_ERROR_NO_INTERFACE;
-  },
-
-  /**
-   * Forward notifications for interfaces this object implements, in case other
-   * objects also implemented them.
-   */
-  _forwardNotification(iid, method, args) {
-    if (!this._wrappedNotificationCallbacks) {
-      return;
-    }
-    try {
-      const impl = this._wrappedNotificationCallbacks.getInterface(iid);
-      impl[method].apply(impl, args);
-    } catch (e) {
-      if (e.result != Cr.NS_ERROR_NO_INTERFACE) {
-        throw e;
-      }
-    }
-  },
-
-  /**
-   * This NetworkResponseListener tracks the NetworkObserver.openResponses object
-   * to find the associated uncached headers.
-   * @private
-   */
-  _foundOpenResponse: false,
-
-  /**
-   * If the channel already had notificationCallbacks, hold them here internally
-   * so that we can forward getInterface requests to that object.
-   */
-  _wrappedNotificationCallbacks: null,
-
-  /**
-   * The response listener owner.
-   */
-  owner: null,
-
-  /**
-   * The response will be written into the outputStream of this nsIPipe.
-   * Both ends of the pipe must be blocking.
-   */
-  sink: null,
-
-  /**
-   * The HttpActivity object associated with this response.
-   */
-  httpActivity: null,
-
-  /**
-   * Stores the received data as a string.
-   */
-  receivedData: null,
-
-  /**
-   * The uncompressed, decoded response body size.
-   */
-  bodySize: null,
-
-  /**
-   * Response size on the wire, potentially compressed / encoded.
-   */
-  transferredSize: null,
-
-  /**
-   * The nsIRequest we are started for.
-   */
-  request: null,
-
-  /**
-   * Set the async listener for the given nsIAsyncInputStream. This allows us to
-   * wait asynchronously for any data coming from the stream.
-   *
-   * @param nsIAsyncInputStream stream
-   *        The input stream from where we are waiting for data to come in.
-   * @param nsIInputStreamCallback listener
-   *        The input stream callback you want. This is an object that must have
-   *        the onInputStreamReady() method. If the argument is null, then the
-   *        current callback is removed.
-   * @return void
-   */
-  setAsyncListener: function(stream, listener) {
-    // Asynchronously wait for the stream to be readable or closed.
-    stream.asyncWait(listener, 0, 0, Services.tm.mainThread);
-  },
-
-  /**
-   * Stores the received data, if request/response body logging is enabled. It
-   * also does limit the number of stored bytes, based on the
-   * `devtools.netmonitor.responseBodyLimit` pref.
-   *
-   * Learn more about nsIStreamListener at:
-   * https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIStreamListener
-   *
-   * @param nsIRequest request
-   * @param nsISupports context
-   * @param nsIInputStream inputStream
-   * @param unsigned long offset
-   * @param unsigned long count
-   */
-  onDataAvailable: function(request, context, inputStream, offset, count) {
-    this._findOpenResponse();
-    const data = NetUtil.readInputStreamToString(inputStream, count);
-
-    this.bodySize += count;
-
-    if (!this.httpActivity.discardResponseBody) {
-      const limit = Services.prefs.getIntPref("devtools.netmonitor.responseBodyLimit");
-      if (this.receivedData.length <= limit || limit == 0) {
-        this.receivedData +=
-          NetworkHelper.convertToUnicode(data, request.contentCharset);
-      }
-      if (this.receivedData.length > limit && limit > 0) {
-        this.receivedData = this.receivedData.substr(0, limit);
-        this.truncated = true;
-      }
-    }
-  },
-
-  /**
-   * See documentation at
-   * https://developer.mozilla.org/En/NsIRequestObserver
-   *
-   * @param nsIRequest request
-   * @param nsISupports context
-   */
-  onStartRequest: function(request) {
-    // Converter will call this again, we should just ignore that.
-    if (this.request) {
-      return;
-    }
-
-    this.request = request;
-    this._getSecurityInfo();
-    this._findOpenResponse();
-    // We need to track the offset for the onDataAvailable calls where
-    // we pass the data from our pipe to the converter.
-    this.offset = 0;
-
-    const channel = this.request;
-
-    // Bug 1372115 - We should load bytecode cached requests from cache as the actual
-    // channel content is going to be optimized data that reflects platform internals
-    // instead of the content user expects (i.e. content served by HTTP server)
-    // Note that bytecode cached is one example, there may be wasm or other usecase in
-    // future.
-    let isOptimizedContent = false;
-    try {
-      if (channel instanceof Ci.nsICacheInfoChannel) {
-        isOptimizedContent = channel.alternativeDataType;
-      }
-    } catch (e) {
-      // Accessing `alternativeDataType` for some SW requests throws.
-    }
-    if (isOptimizedContent) {
-      let charset;
-      try {
-        charset = this.request.contentCharset;
-      } catch (e) {
-        // Accessing the charset sometimes throws NS_ERROR_NOT_AVAILABLE when
-        // reloading the page
-      }
-      if (!charset) {
-        charset = this.httpActivity.charset;
-      }
-      NetworkHelper.loadFromCache(this.httpActivity.url, charset,
-                                  this._onComplete.bind(this));
-      return;
-    }
-
-    // In the multi-process mode, the conversion happens on the child
-    // side while we can only monitor the channel on the parent
-    // side. If the content is gzipped, we have to unzip it
-    // ourself. For that we use the stream converter services.  Do not
-    // do that for Service workers as they are run in the child
-    // process.
-    if (!this.httpActivity.fromServiceWorker &&
-        channel instanceof Ci.nsIEncodedChannel &&
-        channel.contentEncodings &&
-        !channel.applyConversion) {
-      const encodingHeader = channel.getResponseHeader("Content-Encoding");
-      const scs = Cc["@mozilla.org/streamConverters;1"]
-        .getService(Ci.nsIStreamConverterService);
-      const encodings = encodingHeader.split(/\s*\t*,\s*\t*/);
-      let nextListener = this;
-      const acceptedEncodings = ["gzip", "deflate", "br", "x-gzip", "x-deflate"];
-      for (const i in encodings) {
-        // There can be multiple conversions applied
-        const enc = encodings[i].toLowerCase();
-        if (acceptedEncodings.indexOf(enc) > -1) {
-          this.converter = scs.asyncConvertData(enc, "uncompressed",
-                                                nextListener, null);
-          nextListener = this.converter;
-        }
-      }
-      if (this.converter) {
-        this.converter.onStartRequest(this.request, null);
-      }
-    }
-    // Asynchronously wait for the data coming from the request.
-    this.setAsyncListener(this.sink.inputStream, this);
-  },
-
-  /**
-   * Parse security state of this request and report it to the client.
-   */
-  _getSecurityInfo: DevToolsUtils.makeInfallible(function() {
-    // Many properties of the securityInfo (e.g., the server certificate or HPKP
-    // status) are not available in the content process and can't be even touched safely,
-    // because their C++ getters trigger assertions. This function is called in content
-    // process for synthesized responses from service workers, in the parent otherwise.
-    if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
-      return;
-    }
-
-    // Take the security information from the original nsIHTTPChannel instead of
-    // the nsIRequest received in onStartRequest. If response to this request
-    // was a redirect from http to https, the request object seems to contain
-    // security info for the https request after redirect.
-    const secinfo = this.httpActivity.channel.securityInfo;
-    const info = NetworkHelper.parseSecurityInfo(secinfo, this.httpActivity);
-
-    this.httpActivity.owner.addSecurityInfo(info);
-  }),
-
-  /**
-   * Fetches cache information from CacheEntry
-   * @private
-   */
-  _fetchCacheInformation: function() {
-    const httpActivity = this.httpActivity;
-    CacheEntry.getCacheEntry(this.request, (descriptor) => {
-      httpActivity.owner.addResponseCache({
-        responseCache: descriptor
-      });
-    });
-  },
-
-  /**
-   * Handle the onStopRequest by closing the sink output stream.
-   *
-   * For more documentation about nsIRequestObserver go to:
-   * https://developer.mozilla.org/En/NsIRequestObserver
-   */
-  onStopRequest: function() {
-    // Bug 1429365: onStopRequest may be called after onComplete for resources loaded
-    // from bytecode cache.
-    if (!this.httpActivity) {
-      return;
-    }
-    this._findOpenResponse();
-    this.sink.outputStream.close();
-  },
-
-  // nsIProgressEventSink implementation
-
-  /**
-   * Handle progress event as data is transferred.  This is used to record the
-   * size on the wire, which may be compressed / encoded.
-   */
-  onProgress: function(request, context, progress, progressMax) {
-    this.transferredSize = progress;
-    // Need to forward as well to keep things like Download Manager's progress
-    // bar working properly.
-    this._forwardNotification(Ci.nsIProgressEventSink, "onProgress", arguments);
-  },
-
-  onStatus: function() {
-    this._forwardNotification(Ci.nsIProgressEventSink, "onStatus", arguments);
-  },
-
-  /**
-   * Find the open response object associated to the current request. The
-   * NetworkObserver._httpResponseExaminer() method saves the response headers in
-   * NetworkObserver.openResponses. This method takes the data from the open
-   * response object and puts it into the HTTP activity object, then sends it to
-   * the remote Web Console instance.
-   *
-   * @private
-   */
-  _findOpenResponse: function() {
-    if (!this.owner || this._foundOpenResponse) {
-      return;
-    }
-
-    const channel = this.httpActivity.channel;
-    const openResponse = this.owner.openResponses.get(channel);
-    if (!openResponse) {
-      return;
-    }
-    this._foundOpenResponse = true;
-    this.owner.openResponses.delete(channel);
-
-    this.httpActivity.owner.addResponseHeaders(openResponse.headers);
-    this.httpActivity.owner.addResponseCookies(openResponse.cookies);
-  },
-
-  /**
-   * Clean up the response listener once the response input stream is closed.
-   * This is called from onStopRequest() or from onInputStreamReady() when the
-   * stream is closed.
-   * @return void
-   */
-  onStreamClose: function() {
-    if (!this.httpActivity) {
-      return;
-    }
-    // Remove our listener from the request input stream.
-    this.setAsyncListener(this.sink.inputStream, null);
-
-    this._findOpenResponse();
-    if (this.request.fromCache || this.httpActivity.responseStatus == 304) {
-      this._fetchCacheInformation();
-    }
-
-    if (!this.httpActivity.discardResponseBody && this.receivedData.length) {
-      this._onComplete(this.receivedData);
-    } else if (!this.httpActivity.discardResponseBody &&
-               this.httpActivity.responseStatus == 304) {
-      // Response is cached, so we load it from cache.
-      let charset;
-      try {
-        charset = this.request.contentCharset;
-      } catch (e) {
-        // Accessing the charset sometimes throws NS_ERROR_NOT_AVAILABLE when
-        // reloading the page
-      }
-      if (!charset) {
-        charset = this.httpActivity.charset;
-      }
-      NetworkHelper.loadFromCache(this.httpActivity.url, charset,
-                                  this._onComplete.bind(this));
-    } else {
-      this._onComplete();
-    }
-  },
-
-  /**
-   * Handler for when the response completes. This function cleans up the
-   * response listener.
-   *
-   * @param string [data]
-   *        Optional, the received data coming from the response listener or
-   *        from the cache.
-   */
-  _onComplete: function(data) {
-    const response = {
-      mimeType: "",
-      text: data || "",
-    };
-
-    response.size = this.bodySize;
-    response.transferredSize = this.transferredSize + this.httpActivity.headersSize;
-
-    try {
-      response.mimeType = this.request.contentType;
-    } catch (ex) {
-      // Ignore.
-    }
-
-    if (!response.mimeType ||
-        !NetworkHelper.isTextMimeType(response.mimeType)) {
-      response.encoding = "base64";
-      try {
-        response.text = btoa(response.text);
-      } catch (err) {
-        // Ignore.
-      }
-    }
-
-    if (response.mimeType && this.request.contentCharset) {
-      response.mimeType += "; charset=" + this.request.contentCharset;
-    }
-
-    this.receivedData = "";
-
-    this.httpActivity.owner.addResponseContent(
-      response,
-      {
-        discardResponseBody: this.httpActivity.discardResponseBody,
-        truncated: this.truncated
-      }
-    );
-
-    this._wrappedNotificationCallbacks = null;
-    this.httpActivity = null;
-    this.sink = null;
-    this.inputStream = null;
-    this.converter = null;
-    this.request = null;
-    this.owner = null;
-  },
-
-  /**
-   * The nsIInputStreamCallback for when the request input stream is ready -
-   * either it has more data or it is closed.
-   *
-   * @param nsIAsyncInputStream stream
-   *        The sink input stream from which data is coming.
-   * @returns void
-   */
-  onInputStreamReady: function(stream) {
-    if (!(stream instanceof Ci.nsIAsyncInputStream) || !this.httpActivity) {
-      return;
-    }
-
-    let available = -1;
-    try {
-      // This may throw if the stream is closed normally or due to an error.
-      available = stream.available();
-    } catch (ex) {
-      // Ignore.
-    }
-
-    if (available != -1) {
-      if (available != 0) {
-        if (this.converter) {
-          this.converter.onDataAvailable(this.request, null, stream,
-                                         this.offset, available);
-        } else {
-          this.onDataAvailable(this.request, null, stream, this.offset,
-                               available);
-        }
-      }
-      this.offset += available;
-      this.setAsyncListener(stream, this);
-    } else {
-      this.onStreamClose();
-      this.offset = 0;
-    }
-  },
-};
+exports.matchRequest = matchRequest;
 
 /**
  * The network monitor uses the nsIHttpActivityDistributor to monitor network
  * requests. The nsIObserverService is also used for monitoring
  * http-on-examine-response notifications. All network request information is
  * routed to the remote Web Console.
  *
  * @constructor
copy from devtools/server/actors/network-monitor/network-observer.js
copy to devtools/server/actors/network-monitor/network-response-listener.js
--- a/devtools/server/actors/network-monitor/network-observer.js
+++ b/devtools/server/actors/network-monitor/network-response-listener.js
@@ -1,113 +1,47 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft= javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const {Cc, Ci, Cm, Cr, components} = require("chrome");
+const {Cc, Ci, Cr} = require("chrome");
 const ChromeUtils = require("ChromeUtils");
 const Services = require("Services");
-const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
 
 loader.lazyRequireGetter(this, "NetworkHelper",
                          "devtools/shared/webconsole/network-helper");
 loader.lazyRequireGetter(this, "DevToolsUtils",
                          "devtools/shared/DevToolsUtils");
-loader.lazyRequireGetter(this, "flags",
-                         "devtools/shared/flags");
 loader.lazyRequireGetter(this, "NetworkThrottleManager",
                          "devtools/shared/webconsole/throttle", true);
 loader.lazyRequireGetter(this, "CacheEntry",
                          "devtools/shared/platform/cache-entry", true);
+loader.lazyRequireGetter(this, "matchRequest",
+                         "devtools/server/actors/network-monitor/network-observer", true);
 loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
 loader.lazyServiceGetter(this, "gActivityDistributor",
                          "@mozilla.org/network/http-activity-distributor;1",
                          "nsIHttpActivityDistributor");
-loader.lazyRequireGetter(this, "ChannelEventSinkFactory",
-                         "devtools/server/actors/network-monitor/channel-event-sink",
-                         true);
 
 // Network logging
 
 // The maximum uint32 value.
 const PR_UINT32_MAX = 4294967295;
 
 // HTTP status codes.
 const HTTP_MOVED_PERMANENTLY = 301;
 const HTTP_FOUND = 302;
 const HTTP_SEE_OTHER = 303;
 const HTTP_TEMPORARY_REDIRECT = 307;
 
 /**
- * Check if a given network request should be logged by a network monitor
- * based on the specified filters.
- *
- * @param nsIHttpChannel channel
- *        Request to check.
- * @param filters
- *        NetworkObserver filters to match against.
- * @return boolean
- *         True if the network request should be logged, false otherwise.
- */
-function matchRequest(channel, filters) {
-  // Log everything if no filter is specified
-  if (!filters.outerWindowID && !filters.window) {
-    return true;
-  }
-
-  // Ignore requests from chrome or add-on code when we are monitoring
-  // content.
-  // TODO: one particular test (browser_styleeditor_fetch-from-cache.js) needs
-  // the flags.testing check. We will move to a better way to serve
-  // its needs in bug 1167188, where this check should be removed.
-  if (!flags.testing && channel.loadInfo &&
-      channel.loadInfo.loadingDocument === null &&
-      channel.loadInfo.loadingPrincipal ===
-      Services.scriptSecurityManager.getSystemPrincipal()) {
-    return false;
-  }
-
-  if (filters.window) {
-    // Since frames support, this.window may not be the top level content
-    // frame, so that we can't only compare with win.top.
-    let win = NetworkHelper.getWindowForRequest(channel);
-    while (win) {
-      if (win == filters.window) {
-        return true;
-      }
-      if (win.parent == win) {
-        break;
-      }
-      win = win.parent;
-    }
-  }
-
-  if (filters.outerWindowID) {
-    const topFrame = NetworkHelper.getTopFrameForRequest(channel);
-    // topFrame is typically null for some chrome requests like favicons
-    if (topFrame) {
-      try {
-        if (topFrame.outerWindowID == filters.outerWindowID) {
-          return true;
-        }
-      } catch (e) {
-        // outerWindowID getter from browser.xml (non-remote <xul:browser>) may
-        // throw when closing a tab while resources are still loading.
-      }
-    }
-  }
-
-  return false;
-}
-
-/**
  * The network response listener implements the nsIStreamListener and
  * nsIRequestObserver interfaces. This is used within the NetworkObserver feature
  * to get the response body of the request.
  *
  * The code is mostly based on code listings from:
  *
  *   http://www.softwareishard.com/blog/firebug/
  *      nsitraceablechannel-intercept-http-traffic/
@@ -129,16 +63,18 @@ function NetworkResponseListener(owner, 
   this.truncated = false;
   // Note that this is really only needed for the non-e10s case.
   // See bug 1309523.
   const channel = this.httpActivity.channel;
   this._wrappedNotificationCallbacks = channel.notificationCallbacks;
   channel.notificationCallbacks = this;
 }
 
+exports.NetworkResponseListener = NetworkResponseListener;
+
 NetworkResponseListener.prototype = {
   QueryInterface:
     ChromeUtils.generateQI([Ci.nsIStreamListener, Ci.nsIInputStreamCallback,
                             Ci.nsIRequestObserver, Ci.nsIInterfaceRequestor]),
 
   // nsIInterfaceRequestor implementation
 
   /**