Bug 1404917 - Fetch response content only on-demand. r=Honza draft
authorAlexandre Poirot <poirot.alex@gmail.com>
Thu, 26 Oct 2017 09:03:40 -0700
changeset 697580 ec779d8f68267b2591ef6b773254bea304e0db08
parent 697579 54f9f2b5d6d35fc68553d88c4a5b8df2cd63884b
child 697581 a78f76756e5b2b27f207883274f94aae12258cb9
push id89047
push userbmo:poirot.alex@gmail.com
push dateTue, 14 Nov 2017 08:46:30 +0000
reviewersHonza
bugs1404917
milestone59.0a1
Bug 1404917 - Fetch response content only on-demand. r=Honza Response content should only be fetched whenever it is strictly needed as it is the response body. A possibly very large string. So, netmonitor UI should only retrieve it when users select the Response Panel or do any other action that require having access to it (like "Copy response" context menu). MozReview-Commit-ID: CtpJ8PKsCsm
devtools/client/netmonitor/src/components/RequestListContent.js
devtools/client/netmonitor/src/components/RequestListItem.js
devtools/client/netmonitor/src/components/ResponsePanel.js
devtools/client/netmonitor/src/components/TabboxPanel.js
devtools/client/netmonitor/src/connector/firefox-connector.js
devtools/client/netmonitor/src/connector/firefox-data-provider.js
devtools/client/netmonitor/src/connector/index.js
devtools/client/netmonitor/src/constants.js
devtools/client/netmonitor/src/har/har-automation.js
devtools/client/netmonitor/src/har/har-builder.js
devtools/client/netmonitor/src/har/test/browser.ini
devtools/client/netmonitor/src/har/test/browser_net_har_copy_all_as_har.js
devtools/client/netmonitor/src/har/test/browser_net_har_post_data.js
devtools/client/netmonitor/src/har/test/browser_net_har_post_data_on_get.js
devtools/client/netmonitor/src/har/test/browser_net_har_throttle_upload.js
devtools/client/netmonitor/src/reducers/requests.js
devtools/client/netmonitor/src/request-list-context-menu.js
devtools/client/netmonitor/test/browser.ini
devtools/client/netmonitor/test/browser_net_autoscroll.js
devtools/client/netmonitor/test/browser_net_brotli.js
devtools/client/netmonitor/test/browser_net_clear.js
devtools/client/netmonitor/test/browser_net_content-type.js
devtools/client/netmonitor/test/browser_net_headers-alignment.js
devtools/client/netmonitor/test/browser_net_resend_cors.js
devtools/client/netmonitor/test/browser_net_security-error.js
devtools/client/netmonitor/test/browser_net_security-state.js
devtools/client/netmonitor/test/browser_net_security-tab-visibility.js
devtools/client/netmonitor/test/browser_net_simple-request-data.js
devtools/client/netmonitor/test/browser_net_streaming-response.js
devtools/client/netmonitor/test/head.js
devtools/client/netmonitor/test/html_infinite-get-page.html
devtools/client/netmonitor/test/shared-head.js
devtools/client/styleeditor/test/browser.ini
devtools/client/styleeditor/test/browser_styleeditor_fetch-from-cache.js
devtools/client/webconsole/new-console-output/components/message-types/NetworkEventMessage.js
devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
devtools/client/webconsole/new-console-output/store.js
devtools/client/webconsole/new-console-output/test/mochitest/browser_netmonitor_shows_reqs_in_webconsole.js
devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_network_attach.js
devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_network_messages_openinnet.js
devtools/client/webconsole/new-console-output/test/mochitest/head.js
devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js
devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js
devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js
--- a/devtools/client/netmonitor/src/components/RequestListContent.js
+++ b/devtools/client/netmonitor/src/components/RequestListContent.js
@@ -66,16 +66,17 @@ class RequestListContent extends Compone
 
   componentWillMount() {
     const { dispatch, connector } = this.props;
     this.contextMenu = new RequestListContextMenu({
       cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
       getTabTarget: connector.getTabTarget,
       getLongString: connector.getLongString,
       openStatistics: (open) => dispatch(Actions.openStatistics(connector, open)),
+      requestData: connector.requestData,
     });
     this.tooltip = new HTMLTooltip(window.parent.document, { type: "arrow" });
   }
 
   componentDidMount() {
     // Install event handler for displaying a tooltip
     this.tooltip.startTogglingOnHover(this.refs.contentEl, this.onHover, {
       toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
--- a/devtools/client/netmonitor/src/components/RequestListItem.js
+++ b/devtools/client/netmonitor/src/components/RequestListItem.js
@@ -43,17 +43,16 @@ const { div } = DOM;
  * relevant for rendering the RequestListItem. Other properties (like request and
  * response headers, cookies, bodies) are ignored. These are very useful for the
  * network details, but not here.
  */
 const UPDATED_REQ_ITEM_PROPS = [
   "mimeType",
   "eventTimings",
   "securityState",
-  "responseContentDataUri",
   "status",
   "statusText",
   "fromCache",
   "fromServiceWorker",
   "method",
   "url",
   "remoteAddress",
   "cause",
--- a/devtools/client/netmonitor/src/components/ResponsePanel.js
+++ b/devtools/client/netmonitor/src/components/ResponsePanel.js
@@ -36,16 +36,17 @@ const JSON_VIEW_MIME_TYPE = "application
  * Response panel component
  * Displays the GET parameters and POST data of a request
  */
 class ResponsePanel extends Component {
   static get propTypes() {
     return {
       request: PropTypes.object.isRequired,
       openLink: PropTypes.func,
+      connector: PropTypes.object.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
       imageDimensions: {
@@ -53,16 +54,46 @@ class ResponsePanel extends Component {
         height: 0,
       },
     };
 
     this.updateImageDimemsions = this.updateImageDimemsions.bind(this);
     this.isJSON = this.isJSON.bind(this);
   }
 
+  /**
+   * `componentDidMount` is called when opening the ResponsePanel for the first time
+   */
+  componentDidMount() {
+    this.maybeFetchResponseContent(this.props);
+  }
+
+  /**
+   * `componentWillReceiveProps` is the only method called when switching between two
+   * requests while the response panel is displayed.
+   */
+  componentWillReceiveProps(nextProps) {
+    this.maybeFetchResponseContent(nextProps);
+  }
+
+  /**
+   * When switching to another request, lazily fetch response content
+   * from the backend. The Response Panel will first be empty and then
+   * display the content.
+   */
+  maybeFetchResponseContent(props) {
+    if (props.request.responseContentAvailable &&
+        (!props.request.responseContent ||
+         !props.request.responseContent.content)) {
+      // This method will set `props.request.responseContent.content`
+      // asynchronously and force another render.
+      props.connector.requestData(props.request.id, "responseContent");
+    }
+  }
+
   updateImageDimemsions({ target }) {
     this.setState({
       imageDimensions: {
         width: target.naturalWidth,
         height: target.naturalHeight,
       },
     });
   }
--- a/devtools/client/netmonitor/src/components/TabboxPanel.js
+++ b/devtools/client/netmonitor/src/components/TabboxPanel.js
@@ -73,17 +73,17 @@ function TabboxPanel({
         title: PARAMS_TITLE,
       },
         ParamsPanel({ connector, openLink, request }),
       ),
       TabPanel({
         id: PANELS.RESPONSE,
         title: RESPONSE_TITLE,
       },
-        ResponsePanel({ request, openLink }),
+        ResponsePanel({ request, openLink, connector }),
       ),
       TabPanel({
         id: PANELS.TIMINGS,
         title: TIMINGS_TITLE,
       },
         TimingsPanel({ request }),
       ),
       request.cause && request.cause.stacktrace && request.cause.stacktrace.length > 0 &&
--- a/devtools/client/netmonitor/src/connector/firefox-connector.js
+++ b/devtools/client/netmonitor/src/connector/firefox-connector.js
@@ -17,16 +17,17 @@ class FirefoxConnector {
     this.willNavigate = this.willNavigate.bind(this);
     this.displayCachedEvents = this.displayCachedEvents.bind(this);
     this.onDocLoadingMarker = this.onDocLoadingMarker.bind(this);
     this.sendHTTPRequest = this.sendHTTPRequest.bind(this);
     this.setPreferences = this.setPreferences.bind(this);
     this.triggerActivity = this.triggerActivity.bind(this);
     this.getTabTarget = this.getTabTarget.bind(this);
     this.viewSourceInDebugger = this.viewSourceInDebugger.bind(this);
+    this.requestData = this.requestData.bind(this);
 
     // Internals
     this.getLongString = this.getLongString.bind(this);
     this.getNetworkRequest = this.getNetworkRequest.bind(this);
   }
 
   async connect(connection, actions, getState) {
     this.actions = actions;
@@ -280,11 +281,15 @@ class FirefoxConnector {
    * @param {string} sourceURL source url
    * @param {number} sourceLine source line number
    */
   viewSourceInDebugger(sourceURL, sourceLine) {
     if (this.toolbox) {
       this.toolbox.viewSourceInDebugger(sourceURL, sourceLine);
     }
   }
+
+  requestData(request, type) {
+    return this.dataProvider.requestData(request, type);
+  }
 }
 
 module.exports = new FirefoxConnector();
--- a/devtools/client/netmonitor/src/connector/firefox-data-provider.js
+++ b/devtools/client/netmonitor/src/connector/firefox-data-provider.js
@@ -25,16 +25,20 @@ class FirefoxDataProvider {
     // Options
     this.webConsoleClient = webConsoleClient;
     this.actions = actions;
 
     // Internal properties
     this.payloadQueue = [];
     this.rdpRequestMap = new Map();
 
+    // Map[key string => Promise] used by `requestData` to prevent requesting the same
+    // request data twice.
+    this.lazyRequestData = new Map();
+
     // Fetching data from the backend
     this.getLongString = this.getLongString.bind(this);
 
     // Event handlers
     this.onNetworkEvent = this.onNetworkEvent.bind(this);
     this.onNetworkEventUpdate = this.onNetworkEventUpdate.bind(this);
   }
 
@@ -112,16 +116,18 @@ class FirefoxDataProvider {
       requestHeadersObj,
       responseHeadersObj,
       postDataObj,
       requestCookiesObj,
       responseCookiesObj
     );
 
     this.pushRequestToQueue(id, payload);
+
+    return payload;
   }
 
   async fetchResponseContent(mimeType, responseContent) {
     let payload = {};
     if (mimeType && responseContent && responseContent.content) {
       let { encoding, text } = responseContent.content;
       let response = await this.getLongString(text);
 
@@ -238,20 +244,34 @@ class FirefoxDataProvider {
    * @return {boolean} return whether a specific networkEvent has been updated completely.
    */
   isRequestPayloadReady(id) {
     let record = this.rdpRequestMap.get(id);
     if (!record) {
       return false;
     }
 
-    // The payload is ready when all values in the record are true.
-    // (i.e. all data received).
-    let props = Object.getOwnPropertyNames(record);
-    return props.every(prop => record[prop] === true);
+    let { payload } = this.getRequestFromQueue(id);
+
+    // The payload is ready when all values in the record are true. (i.e. all data
+    // received, but the lazy one. responseContent is the only one for now).
+    // Note that we never fetch response header/cookies for request with security issues.
+    // (Be careful, securityState can be undefined, for example for WebSocket requests)
+    // Also note that service worker don't have security info set.
+    // Bug 1404917 should simplify this heuristic by making all these field be lazily
+    // fetched, only on-demand.
+    return record.requestHeaders &&
+      record.requestCookies &&
+      record.eventTimings &&
+      (record.securityInfo || record.fromServiceWorker) &&
+      (
+       (record.responseHeaders && record.responseCookies) ||
+       payload.securityState == "broken" ||
+       (payload.responseContentAvailable && !payload.status)
+      );
   }
 
   /**
    * Merge upcoming networkEventUpdate payload into existing one.
    *
    * @param {string} id request id
    * @param {object} payload request data payload
    */
@@ -314,18 +334,24 @@ class FirefoxDataProvider {
       },
       startedDateTime,
     } = networkInfo;
 
     // Create tracking record for this request.
     this.rdpRequestMap.set(actor, {
       requestHeaders: false,
       requestCookies: false,
+      responseHeaders: false,
+      responseCookies: false,
+      securityInfo: false,
       eventTimings: false,
-      responseContent: false,
+
+      // This isn't a request data, but we need to know about request being served from
+      // service worker later, from isRequestPayloadReady.
+      fromServiceWorker,
     });
 
     this.addRequest(actor, {
       cause,
       fromCache,
       fromServiceWorker,
       isXHR,
       method,
@@ -343,177 +369,204 @@ class FirefoxDataProvider {
    * @param {object} packet the message received from the server.
    * @param {object} networkInfo the network request information.
    */
   onNetworkEventUpdate(type, data) {
     let { packet, networkInfo } = data;
     let { actor } = networkInfo;
     let { updateType } = packet;
 
+    // When we pause and resume, we may receive `networkEventUpdate` for a request
+    // that started during the pause and we missed its `networkEvent`.
+    if (!this.rdpRequestMap.has(actor)) {
+      return;
+    }
+
     switch (updateType) {
       case "requestHeaders":
-        this.requestData(actor, updateType).then(response => {
-          this.onRequestHeaders(response)
-            .then(() => this.onDataReceived(actor, updateType));
-          emit(EVENTS.UPDATING_REQUEST_HEADERS, actor);
-        });
-        break;
       case "requestCookies":
-        this.requestData(actor, updateType).then(response => {
-          this.onRequestCookies(response)
-            .then(() => this.onDataReceived(actor, updateType));
-          emit(EVENTS.UPDATING_REQUEST_COOKIES, actor);
-        });
-        break;
       case "requestPostData":
-        this.requestData(actor, updateType).then(response => {
-          this.onRequestPostData(response)
-            .then(() => this.onDataReceived(actor, updateType));
-          emit(EVENTS.UPDATING_REQUEST_POST_DATA, actor);
-        });
+      case "responseHeaders":
+      case "responseCookies":
+        this.requestPayloadData(actor, updateType);
         break;
       case "securityInfo":
         this.updateRequest(actor, {
           securityState: networkInfo.securityInfo,
         }).then(() => {
-          this.requestData(actor, updateType).then(response => {
-            this.onSecurityInfo(response)
-              .then(() => this.onDataReceived(actor, updateType));
-            emit(EVENTS.UPDATING_SECURITY_INFO, actor);
-          });
-        });
-        break;
-      case "responseHeaders":
-        this.requestData(actor, updateType).then(response => {
-          this.onResponseHeaders(response)
-            .then(() => this.onDataReceived(actor, updateType));
-          emit(EVENTS.UPDATING_RESPONSE_HEADERS, actor);
-        });
-        break;
-      case "responseCookies":
-        this.requestData(actor, updateType).then(response => {
-          this.onResponseCookies(response)
-            .then(() => this.onDataReceived(actor, updateType));
-          emit(EVENTS.UPDATING_RESPONSE_COOKIES, actor);
+          this.requestPayloadData(actor, updateType);
         });
         break;
       case "responseStart":
         this.updateRequest(actor, {
           httpVersion: networkInfo.response.httpVersion,
           remoteAddress: networkInfo.response.remoteAddress,
           remotePort: networkInfo.response.remotePort,
           status: networkInfo.response.status,
           statusText: networkInfo.response.statusText,
           headersSize: networkInfo.response.headersSize
         }).then(() => {
           emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
         });
         break;
       case "responseContent":
-        this.requestData(actor, updateType).then(response => {
-          this.onResponseContent({
-            contentSize: networkInfo.response.bodySize,
-            transferredSize: networkInfo.response.transferredSize,
-            mimeType: networkInfo.response.content.mimeType
-          }, response).then(() => this.onDataReceived(actor, updateType));
-          emit(EVENTS.UPDATING_RESPONSE_CONTENT, actor);
+        this.updateRequest(actor, {
+          contentSize: networkInfo.response.bodySize,
+          transferredSize: networkInfo.response.transferredSize,
+          mimeType: networkInfo.response.content.mimeType,
+
+          // This field helps knowing when/if responseContent property is available
+          // and can be requested via `requestData`
+          responseContentAvailable: true,
         });
         break;
       case "eventTimings":
         this.updateRequest(actor, { totalTime: networkInfo.totalTime })
           .then(() => {
-            this.requestData(actor, updateType).then(response => {
-              this.onEventTimings(response)
-                .then(() => this.onDataReceived(actor, updateType));
-              emit(EVENTS.UPDATING_EVENT_TIMINGS, actor);
-            });
+            this.requestPayloadData(actor, updateType);
           });
         break;
     }
 
     emit(EVENTS.NETWORK_EVENT_UPDATED, actor);
   }
 
   /**
-   * Wrapper method for requesting HTTP details data from the backend.
+   * Wrapper method for requesting HTTP details data for the payload.
+   *
+   * It is specific to all requests done from `onNetworkEventUpdate`, for data that are
+   * immediately fetched whenever the data is available.
    *
-   * It collects all RDP requests and monitors responses, so it's
-   * possible to determine whether (and when) all requested data
-   * has been fetched from the backend.
+   * All these requests are cached into `rdpRequestMap`. All requests related to a given
+   * actor will be collected in the same record.
    *
-   * It also nicely returns a promise.
+   * Once bug 1404917 is completed, we should no longer use this method.
+   * All request fields should be loaded only on-demand, via `requestData` method.
    *
    * @param {string} actor actor id (used as request id)
    * @param {string} method identifier of the data we want to fetch
-   *
-   * @return {Promise} return a promise resolved when data are received.
    */
-  requestData(actor, method) {
+  requestPayloadData(actor, method) {
     let record = this.rdpRequestMap.get(actor);
 
-    // All RDP requests related to the given actor will be collected
-    // in the same record.
-    if (!record) {
-      record = {};
-    }
-
-    // If data has been already requested return the same promise.
-    if (record.method) {
-      return record.method;
+    // If data has been already requested, do nothing.
+    if (record[method]) {
+      return;
     }
 
-    // Calculate real name of the client getter.
-    let realMethodName = "get" + method.charAt(0).toUpperCase() +
-      method.slice(1);
-
-    // Request data from the backend.
-    let promise = new Promise((resolve, reject) => {
-      if (typeof this.webConsoleClient[realMethodName] == "function") {
-        this.webConsoleClient[realMethodName](actor, response => {
-          // Resolve incoming HTTP details data-promise.
-          resolve(response);
-        });
-      } else {
-        reject(new Error("Error: No such client method!"));
-      }
+    let promise = this._requestData(actor, method);
+    promise.then(() => {
+      // Once we got the data toggle the Map item to `true` in order to
+      // make isRequestPayloadReady return `true` once all the data is fetched.
+      record[method] = true;
+      this.onPayloadDataReceived(actor, method, !record);
     });
-
-    // Store the promise in order to know about RDP requests
-    // in progress.
-    record[method] = promise;
-
-    return promise;
   }
 
   /**
    * Executed when new data are received from the backend.
    */
-  async onDataReceived(actor, type) {
-    let record = this.rdpRequestMap.get(actor);
-    if (record) {
-      record[type] = true;
-    }
-
+  async onPayloadDataReceived(actor, type) {
+    // Notify actions when all the sync request from onNetworkEventUpdate are done,
+    // or, everytime requestData is called for fetching data lazily.
     if (this.isRequestPayloadReady(actor)) {
       let payloadFromQueue = this.getRequestFromQueue(actor).payload;
 
       // Clean up
       this.cleanUpQueue(actor);
       this.rdpRequestMap.delete(actor);
 
       let { updateRequest } = this.actions;
       if (updateRequest) {
         await updateRequest(actor, payloadFromQueue, true);
       }
 
+      // This event is fired only once per request, once all the properties are fetched
+      // from `onNetworkEventUpdate`. There should be no more RDP requests after this.
       emit(EVENTS.PAYLOAD_READY, actor);
     }
   }
 
   /**
+   * Public connector API to lazily request HTTP details from the backend.
+   *
+   * This is internal method that focus on:
+   * - calling the right actor method,
+   * - emitting an event to tell we start fetching some request data,
+   * - call data processing method.
+   *
+   * @param {string} actor actor id (used as request id)
+   * @param {string} method identifier of the data we want to fetch
+   *
+   * @return {Promise} return a promise resolved when data is received.
+   */
+  requestData(actor, method) {
+    // Key string used in `lazyRequestData`. We use this Map to prevent requesting
+    // the same data twice at the same time.
+    let key = actor + "-" + method;
+    let promise = this.lazyRequestData.get(key);
+    // If a request is pending, reuse it.
+    if (promise) {
+      return promise;
+    }
+    // Fetch the data
+    promise = this._requestData(actor, method);
+    this.lazyRequestData.set(key, promise);
+    promise.then(async () => {
+      // Remove the request from the cache, any new call to requestData will fetch the
+      // data again.
+      this.lazyRequestData.delete(key, promise);
+
+      let payloadFromQueue = this.getRequestFromQueue(actor).payload;
+      let { updateRequest } = this.actions;
+      if (updateRequest) {
+        await updateRequest(actor, payloadFromQueue, true);
+      }
+    });
+    return promise;
+  }
+
+  /**
+   * Internal helper used to request HTTP details from the backend.
+   *
+   * This is internal method that focus on:
+   * - calling the right actor method,
+   * - emitting an event to tell we start fetching some request data,
+   * - call data processing method.
+   *
+   * @param {string} actor actor id (used as request id)
+   * @param {string} method identifier of the data we want to fetch
+   *
+   * @return {Promise} return a promise resolved when data is received.
+   */
+  async _requestData(actor, method) {
+    // Calculate real name of the client getter.
+    let clientMethodName = "get" + method.charAt(0).toUpperCase() +
+      method.slice(1);
+    // The name of the callback that processes request response
+    let callbackMethodName = "on" + method.charAt(0).toUpperCase() +
+      method.slice(1);
+    // And the event to fire before updating this data
+    let updatingEventName = "UPDATING_" + method.replace(/([A-Z])/g, "_$1").toUpperCase();
+
+    if (typeof this.webConsoleClient[clientMethodName] == "function") {
+      // Emit event that tell we just start fetching some data
+      emit(EVENTS[updatingEventName], actor);
+
+      // Do a RDP request to fetch data from the actor.
+      let response = await this.webConsoleClient[clientMethodName](actor);
+
+      // Call data processing method.
+      response = await this[callbackMethodName](response);
+      return response;
+    }
+    throw new Error("Error: No such client method '" + clientMethodName + "'!");
+  }
+
+  /**
    * Handles additional information received for a "requestHeaders" packet.
    *
    * @param {object} response the message received from the server.
    */
   onRequestHeaders(response) {
     return this.updateRequest(response.from, {
       requestHeaders: response
     }).then(() => {
@@ -582,26 +635,30 @@ class FirefoxDataProvider {
     return this.updateRequest(response.from, {
       responseCookies: response
     }).then(() => {
       emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
     });
   }
 
   /**
-   * Handles additional information received for a "responseContent" packet.
+   * Handles additional information received via "getResponseContent" request.
    *
-   * @param {object} data the message received from the server event.
    * @param {object} response the message received from the server.
    */
-  onResponseContent(data, response) {
-    let payload = Object.assign({ responseContent: response }, data);
-    return this.updateRequest(response.from, payload).then(() => {
-      emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
+  async onResponseContent(response) {
+    let payload = await this.updateRequest(response.from, {
+      // We have to ensure passing mimeType as fetchResponseContent needs it from
+      // updateRequest. It will convert the LongString in `response.content.text` to a
+      // string.
+      mimeType: response.content.mimeType,
+      responseContent: response,
     });
+    emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
+    return payload.responseContent;
   }
 
   /**
    * Handles additional information received for a "eventTimings" packet.
    *
    * @param {object} response the message received from the server.
    */
   onEventTimings(response) {
--- a/devtools/client/netmonitor/src/connector/index.js
+++ b/devtools/client/netmonitor/src/connector/index.js
@@ -20,16 +20,17 @@ class Connector {
     this.connectFirefox = this.connectFirefox.bind(this);
     this.getLongString = this.getLongString.bind(this);
     this.getNetworkRequest = this.getNetworkRequest.bind(this);
     this.getTabTarget = this.getTabTarget.bind(this);
     this.sendHTTPRequest = this.sendHTTPRequest.bind(this);
     this.setPreferences = this.setPreferences.bind(this);
     this.triggerActivity = this.triggerActivity.bind(this);
     this.viewSourceInDebugger = this.viewSourceInDebugger.bind(this);
+    this.requestData = this.requestData.bind(this);
   }
 
   // Connect/Disconnect API
 
   connect(connection, actions, getState) {
     if (!connection || !connection.tab) {
       return;
     }
@@ -93,11 +94,15 @@ class Connector {
 
   triggerActivity() {
     return this.connector.triggerActivity(...arguments);
   }
 
   viewSourceInDebugger() {
     return this.connector.viewSourceInDebugger(...arguments);
   }
+
+  requestData() {
+    return this.connector.requestData(...arguments);
+  }
 }
 
 module.exports.Connector = Connector;
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -119,17 +119,17 @@ const UPDATE_PROPS = [
   "customQueryValue",
   "requestHeaders",
   "requestHeadersFromUploadStream",
   "requestCookies",
   "requestPostData",
   "responseHeaders",
   "responseCookies",
   "responseContent",
-  "responseContentDataUri",
+  "responseContentAvailable",
   "formDataSections",
 ];
 
 const PANELS = {
   COOKIES: "cookies",
   HEADERS: "headers",
   PARAMS: "params",
   RESPONSE: "response",
--- a/devtools/client/netmonitor/src/har/har-automation.js
+++ b/devtools/client/netmonitor/src/har/har-automation.js
@@ -163,16 +163,17 @@ HarAutomation.prototype = {
    * Network panel (asynchronously) and saves it into a file.
    */
   executeExport: function (data) {
     let items = this.collector.getItems();
     let form = this.toolbox.target.form;
     let title = form.title || form.url;
 
     let options = {
+      requestData: null,
       getString: this.getString.bind(this),
       view: this,
       items: items,
     };
 
     options.defaultFileName = data.fileName;
     options.compress = data.compress;
     options.title = data.title || title;
--- a/devtools/client/netmonitor/src/har/har-builder.js
+++ b/devtools/client/netmonitor/src/har/har-builder.js
@@ -46,30 +46,32 @@ HarBuilder.prototype = {
   /**
    * This is the main method used to build the entire result HAR data.
    * The process is asynchronous since it can involve additional RDP
    * communication (e.g. resolving long strings).
    *
    * @returns {Promise} A promise that resolves to the HAR object when
    * the entire build process is done.
    */
-  build: function () {
+  build: async function () {
     this.promises = [];
 
     // Build basic structure for data.
     let log = this.buildLog();
 
     // Build entries.
     for (let file of this._options.items) {
-      log.entries.push(this.buildEntry(log, file));
+      log.entries.push(await this.buildEntry(log, file));
     }
 
     // Some data needs to be fetched from the backend during the
     // build process, so wait till all is done.
-    return Promise.all(this.promises).then(() => ({ log }));
+    await Promise.all(this.promises);
+
+    return { log };
   },
 
   // Helpers
 
   buildLog: function () {
     return {
       version: HAR_VERSION,
       creator: {
@@ -105,26 +107,26 @@ HarBuilder.prototype = {
     }
 
     this._pageMap[id] = page = this.buildPage(file);
     log.pages.push(page);
 
     return page;
   },
 
-  buildEntry: function (log, file) {
+  buildEntry: async function (log, file) {
     let page = this.getPage(log, file);
 
     let entry = {};
     entry.pageref = page.id;
     entry.startedDateTime = dateToJSON(new Date(file.startedMillis));
     entry.time = file.endedMillis - file.startedMillis;
 
     entry.request = this.buildRequest(file);
-    entry.response = this.buildResponse(file);
+    entry.response = await this.buildResponse(file);
     entry.cache = this.buildCache(file);
     entry.timings = file.eventTimings ? file.eventTimings.timings : {};
 
     if (file.remoteAddress) {
       entry.serverIPAddress = file.remoteAddress;
     }
 
     if (file.remotePort) {
@@ -281,34 +283,34 @@ HarBuilder.prototype = {
           });
         });
       }
     });
 
     return postData;
   },
 
-  buildResponse: function (file) {
+  buildResponse: async function (file) {
     let response = {
       status: 0
     };
 
     // Arbitrary value if it's aborted to make sure status has a number
     if (file.status) {
       response.status = parseInt(file.status, 10);
     }
 
     let responseHeaders = file.responseHeaders;
 
     response.statusText = file.statusText || "";
     response.httpVersion = file.httpVersion || "";
 
     response.headers = this.buildHeaders(responseHeaders);
     response.cookies = this.buildCookies(file.responseCookies);
-    response.content = this.buildContent(file);
+    response.content = await this.buildContent(file);
 
     let headers = responseHeaders ? responseHeaders.headers : null;
     let headersSize = responseHeaders ? responseHeaders.headersSize : -1;
 
     response.redirectURL = findValue(headers, "Location");
     response.headersSize = headersSize;
 
     // 'bodySize' is size of the received response body in bytes.
@@ -318,23 +320,29 @@ HarBuilder.prototype = {
       response.bodySize = (response.status == 304) ? 0 : -1;
     } else {
       response.bodySize = file.transferredSize;
     }
 
     return response;
   },
 
-  buildContent: function (file) {
+  buildContent: async function (file) {
     let content = {
       mimeType: file.mimeType,
       size: -1
     };
 
+    // When using HarAutomation, HarCollector will automatically fetch responseContent,
+    // but when we use it from netmonitor, FirefoxDataProvider should fetch it itself
+    // lazily, via requestData.
     let responseContent = file.responseContent;
+    if (!responseContent && this._options.requestData) {
+      responseContent = await this._options.requestData(file.id, "responseContent");
+    }
     if (responseContent && responseContent.content) {
       content.size = responseContent.content.size;
       content.encoding = responseContent.content.encoding;
     }
 
     let includeBodies = this._options.includeResponseBodies;
     let contentDiscarded = responseContent ?
       responseContent.contentDiscarded : false;
--- a/devtools/client/netmonitor/src/har/test/browser.ini
+++ b/devtools/client/netmonitor/src/har/test/browser.ini
@@ -1,14 +1,16 @@
 [DEFAULT]
 tags = devtools
 subsuite = clipboard
 skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
 support-files =
   head.js
   html_har_post-data-test-page.html
   !/devtools/client/netmonitor/test/head.js
+  !/devtools/client/framework/test/shared-head.js
+  !/devtools/client/netmonitor/test/shared-head.js
   !/devtools/client/netmonitor/test/html_simple-test-page.html
 
 [browser_net_har_copy_all_as_har.js]
 [browser_net_har_post_data.js]
 [browser_net_har_throttle_upload.js]
 [browser_net_har_post_data_on_get.js]
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_copy_all_as_har.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_copy_all_as_har.js
@@ -14,25 +14,26 @@ add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
 
   info("Starting test... ");
 
   let { connector, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let RequestListContextMenu = windowRequire(
     "devtools/client/netmonitor/src/request-list-context-menu");
-  let { getLongString, getTabTarget } = connector;
+  let { getLongString, getTabTarget, requestData } = connector;
 
   store.dispatch(Actions.batchEnable(false));
 
   let wait = waitForNetworkEvents(monitor, 1);
   tab.linkedBrowser.reload();
   yield wait;
 
-  let contextMenu = new RequestListContextMenu({ getTabTarget, getLongString });
+  let contextMenu = new RequestListContextMenu({
+    getTabTarget, getLongString, requestData });
 
   yield contextMenu.copyAllAsHar();
 
   let jsonString = SpecialPowers.getClipboardData("text/unicode");
   let har = JSON.parse(jsonString);
 
   // Check out HAR log
   isnot(har.log, null, "The HAR log must exist");
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_post_data.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_post_data.js
@@ -11,29 +11,30 @@ add_task(function* () {
     HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
 
   info("Starting test... ");
 
   let { connector, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let RequestListContextMenu = windowRequire(
     "devtools/client/netmonitor/src/request-list-context-menu");
-  let { getLongString, getTabTarget } = connector;
+  let { getLongString, getTabTarget, requestData } = connector;
 
   store.dispatch(Actions.batchEnable(false));
 
   // Execute one POST request on the page and wait till its done.
   let wait = waitForNetworkEvents(monitor, 0, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.executeTest();
   });
   yield wait;
 
   // Copy HAR into the clipboard (asynchronous).
-  let contextMenu = new RequestListContextMenu({ getTabTarget, getLongString });
+  let contextMenu = new RequestListContextMenu({
+    getTabTarget, getLongString, requestData });
   let jsonString = yield contextMenu.copyAllAsHar();
   let har = JSON.parse(jsonString);
 
   // Check out the HAR log.
   isnot(har.log, null, "The HAR log must exist");
   is(har.log.pages.length, 1, "There must be one page");
   is(har.log.entries.length, 1, "There must be one request");
 
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_post_data_on_get.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_post_data_on_get.js
@@ -11,29 +11,30 @@ add_task(function* () {
     HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
 
   info("Starting test... ");
 
   let { connector, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let RequestListContextMenu = windowRequire(
     "devtools/client/netmonitor/src/request-list-context-menu");
-  let { getLongString, getTabTarget } = connector;
+  let { getLongString, getTabTarget, requestData } = connector;
 
   store.dispatch(Actions.batchEnable(false));
 
   // Execute one GET request on the page and wait till its done.
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.executeTest3();
   });
   yield wait;
 
   // Copy HAR into the clipboard (asynchronous).
-  let contextMenu = new RequestListContextMenu({ getTabTarget, getLongString });
+  let contextMenu = new RequestListContextMenu({
+    getTabTarget, getLongString, requestData });
   let jsonString = yield contextMenu.copyAllAsHar();
   let har = JSON.parse(jsonString);
 
   // Check out the HAR log.
   isnot(har.log, null, "The HAR log must exist");
   is(har.log.pages.length, 1, "There must be one page");
   is(har.log.entries.length, 1, "There must be one request");
 
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_throttle_upload.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_throttle_upload.js
@@ -15,17 +15,17 @@ function* throttleUploadTest(actuallyThr
     HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
 
   info("Starting test... (actuallyThrottle = " + actuallyThrottle + ")");
 
   let { connector, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let RequestListContextMenu = windowRequire(
     "devtools/client/netmonitor/src/request-list-context-menu");
-  let { getLongString, getTabTarget, setPreferences } = connector;
+  let { getLongString, getTabTarget, setPreferences, requestData } = connector;
 
   store.dispatch(Actions.batchEnable(false));
 
   const size = 4096;
   const uploadSize = actuallyThrottle ? size / 3 : 0;
 
   const request = {
     "NetworkMonitor.throttleData": {
@@ -48,17 +48,18 @@ function* throttleUploadTest(actuallyThr
   // Execute one POST request on the page and wait till its done.
   let wait = waitForNetworkEvents(monitor, 0, 1);
   yield ContentTask.spawn(tab.linkedBrowser, { size }, function* (args) {
     content.wrappedJSObject.executeTest2(args.size);
   });
   yield wait;
 
   // Copy HAR into the clipboard (asynchronous).
-  let contextMenu = new RequestListContextMenu({ getTabTarget, getLongString });
+  let contextMenu = new RequestListContextMenu({
+    getTabTarget, getLongString, requestData });
   let jsonString = yield contextMenu.copyAllAsHar();
   let har = JSON.parse(jsonString);
 
   // Check out the HAR log.
   isnot(har.log, null, "The HAR log must exist");
   is(har.log.pages.length, 1, "There must be one page");
   is(har.log.entries.length, 1, "There must be one request");
 
--- a/devtools/client/netmonitor/src/reducers/requests.js
+++ b/devtools/client/netmonitor/src/reducers/requests.js
@@ -53,16 +53,17 @@ const Request = I.Record({
   customQueryValue: undefined,
   requestHeaders: undefined,
   requestHeadersFromUploadStream: undefined,
   requestCookies: undefined,
   requestPostData: undefined,
   responseHeaders: undefined,
   responseCookies: undefined,
   responseContent: undefined,
+  responseContentAvailable: false,
   responseContentDataUri: undefined,
   formDataSections: undefined,
 });
 
 const Requests = I.Record({
   // The collection of requests (keyed by id)
   requests: I.Map(),
   // Selection state
--- a/devtools/client/netmonitor/src/request-list-context-menu.js
+++ b/devtools/client/netmonitor/src/request-list-context-menu.js
@@ -15,28 +15,31 @@ const {
   getSortedRequests,
 } = require("./selectors/index");
 const { L10N } = require("./utils/l10n");
 const { showMenu } = require("devtools/client/netmonitor/src/utils/menu");
 const {
   getUrlQuery,
   parseQueryString,
   getUrlBaseName,
+  formDataURI,
 } = require("./utils/request-utils");
 
 function RequestListContextMenu({
   cloneSelectedRequest,
   getLongString,
   getTabTarget,
   openStatistics,
+  requestData,
 }) {
   this.cloneSelectedRequest = cloneSelectedRequest;
   this.getLongString = getLongString;
   this.getTabTarget = getTabTarget;
   this.openStatistics = openStatistics;
+  this.requestData = requestData;
 }
 
 RequestListContextMenu.prototype = {
   get selectedRequest() {
     // FIXME: Bug 1336382 - Implement RequestListContextMenu React component
     // Remove window.store
     return getSelectedRequest(window.store.getState());
   },
@@ -109,20 +112,17 @@ RequestListContextMenu.prototype = {
       visible: !!(selectedRequest && selectedRequest.responseHeaders),
       click: () => this.copyResponseHeaders(),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-response",
       label: L10N.getStr("netmonitor.context.copyResponse"),
       accesskey: L10N.getStr("netmonitor.context.copyResponse.accesskey"),
-      visible: !!(selectedRequest &&
-               selectedRequest.responseContent &&
-               selectedRequest.responseContent.content.text &&
-               selectedRequest.responseContent.content.text.length !== 0),
+      visible: !!(selectedRequest && selectedRequest.responseContentAvailable),
       click: () => this.copyResponse(),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-image-as-data-uri",
       label: L10N.getStr("netmonitor.context.copyImageAsDataUri"),
       accesskey: L10N.getStr("netmonitor.context.copyImageAsDataUri.accesskey"),
       visible: !!(selectedRequest &&
@@ -333,25 +333,32 @@ RequestListContextMenu.prototype = {
       rawHeaders = rawHeaders.replace(/\r/g, "");
     }
     copyString(rawHeaders);
   },
 
   /**
    * Copy image as data uri.
    */
-  copyImageAsDataUri() {
-    copyString(this.selectedRequest.responseContentDataUri);
+  async copyImageAsDataUri() {
+    let responseContent = await this.requestData(this.selectedRequest.id,
+      "responseContent");
+    let { mimeType } = this.selectedRequest;
+    let { encoding, text } = responseContent.content;
+    let src = formDataURI(mimeType, encoding, text);
+    copyString(src);
   },
 
   /**
    * Save image as.
    */
-  saveImageAs() {
-    let { encoding, text } = this.selectedRequest.responseContent.content;
+  async saveImageAs() {
+    let responseContent = await this.requestData(this.selectedRequest.id,
+      "responseContent");
+    let { encoding, text } = responseContent.content;
     let fileName = getUrlBaseName(this.selectedRequest.url);
     let data;
     if (encoding === "base64") {
       let decoded = atob(text);
       data = new Uint8Array(decoded.length);
       for (let i = 0; i < decoded.length; ++i) {
         data[i] = decoded.charCodeAt(i);
       }
@@ -359,18 +366,20 @@ RequestListContextMenu.prototype = {
       data = text;
     }
     saveAs(new Blob([data]), fileName, document);
   },
 
   /**
    * Copy response data as a string.
    */
-  copyResponse() {
-    copyString(this.selectedRequest.responseContent.content.text);
+  async copyResponse() {
+    let responseContent = await this.requestData(this.selectedRequest.id,
+      "responseContent");
+    copyString(responseContent.content.text);
   },
 
   /**
    * Copy HAR from the network panel content to the clipboard.
    */
   copyAllAsHar() {
     return HarExporter.copy(this.getDefaultHarOptions());
   },
@@ -386,16 +395,17 @@ RequestListContextMenu.prototype = {
     return HarExporter.save(this.getDefaultHarOptions());
   },
 
   getDefaultHarOptions() {
     let form = this.getTabTarget().form;
     let title = form.title || form.url;
 
     return {
+      requestData: this.requestData,
       getString: this.getLongString,
       items: this.sortedRequests,
       title: title
     };
   }
 };
 
 module.exports = RequestListContextMenu;
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -1,14 +1,15 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
   dropmarker.svg
   head.js
+  shared-head.js
   html_cause-test-page.html
   html_content-type-without-cache-test-page.html
   html_brotli-test-page.html
   html_image-tooltip-test-page.html
   html_cors-test-page.html
   html_custom-get-page.html
   html_cyrillic-test-page.html
   html_frame-test-page.html
--- a/devtools/client/netmonitor/test/browser_net_autoscroll.js
+++ b/devtools/client/netmonitor/test/browser_net_autoscroll.js
@@ -5,17 +5,17 @@
 
 /**
  * Bug 863102 - Automatically scroll down upon new network requests.
  * edited to account for changes made to fix Bug 1360457
  */
 add_task(function* () {
   requestLongerTimeout(4);
 
-  let { monitor } = yield initNetMonitor(INFINITE_GET_URL, true);
+  let { tab, monitor } = yield initNetMonitor(INFINITE_GET_URL, true);
   let { document, windowRequire, store } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
 
   store.dispatch(Actions.batchEnable(false));
 
   // Wait until the first request makes the empty notice disappear
   yield waitForRequestListToAppear();
 
@@ -52,16 +52,21 @@ add_task(function* () {
   // from just below the headers.
   store.dispatch(Actions.selectRequestByIndex(0));
   yield waitForNetworkEvents(monitor, 8);
   yield waitSomeTime();
   let requestsContainerHeaders = requestsContainer.firstChild;
   let headersHeight = requestsContainerHeaders.offsetHeight;
   is(requestsContainer.scrollTop, headersHeight, "Did not scroll.");
 
+  // Stop doing requests.
+  yield ContentTask.spawn(tab.linkedBrowser, {}, function () {
+    content.wrappedJSObject.stopRequests();
+  });
+
   // Done: clean up.
   return teardown(monitor);
 
   function waitForRequestListToAppear() {
     info("Waiting until the empty notice disappears and is replaced with the list");
     return waitUntil(() => !!document.querySelector(".requests-list-contents"));
   }
 
--- a/devtools/client/netmonitor/test/browser_net_brotli.js
+++ b/devtools/client/netmonitor/test/browser_net_brotli.js
@@ -46,21 +46,23 @@ add_task(function* () {
       type: "plain",
       fullMimeType: "text/plain",
       transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 60),
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 64),
       time: true
     });
 
   wait = waitForDOM(document, ".CodeMirror-code");
+  let onResponseContent = monitor.panelWin.once(EVENTS.RECEIVED_RESPONSE_CONTENT);
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector(".network-details-panel-toggle"));
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector("#response-tab"));
   yield wait;
+  yield onResponseContent;
   yield testResponse("br");
   yield teardown(monitor);
 
   function* testResponse(type) {
     switch (type) {
       case "br": {
         is(document.querySelector(".CodeMirror-line").textContent, "X".repeat(64),
           "The text shown in the source editor is incorrect for the brotli request.");
--- a/devtools/client/netmonitor/test/browser_net_clear.js
+++ b/devtools/client/netmonitor/test/browser_net_clear.js
@@ -8,40 +8,39 @@
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
   let { document, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
-  let { EVENTS } = windowRequire("devtools/client/netmonitor/src/constants");
   let detailsPanelToggleButton = document.querySelector(".network-details-panel-toggle");
   let clearButton = document.querySelector(".requests-list-clear-button");
 
   store.dispatch(Actions.batchEnable(false));
 
   // Make sure we start in a sane state
   assertNoRequestState();
 
   // Load one request and assert it shows up in the list
-  let networkEvent = monitor.panelWin.once(EVENTS.NETWORK_EVENT);
+  let onMonitorUpdated = waitForAllRequestsFinished(monitor);
   tab.linkedBrowser.reload();
-  yield networkEvent;
+  yield onMonitorUpdated;
 
   assertSingleRequestState();
 
   // Click clear and make sure the requests are gone
   EventUtils.sendMouseEvent({ type: "click" }, clearButton);
   assertNoRequestState();
 
   // Load a second request and make sure they still show up
-  networkEvent = monitor.panelWin.once(EVENTS.NETWORK_EVENT);
+  onMonitorUpdated = waitForAllRequestsFinished(monitor);
   tab.linkedBrowser.reload();
-  yield networkEvent;
+  yield onMonitorUpdated;
 
   assertSingleRequestState();
 
   // Make sure we can now open the network details panel
   EventUtils.sendMouseEvent({ type: "click" }, detailsPanelToggleButton);
 
   ok(document.querySelector(".network-details-panel") &&
     !detailsPanelToggleButton.classList.contains("pane-collapsed"),
--- a/devtools/client/netmonitor/test/browser_net_content-type.js
+++ b/devtools/client/netmonitor/test/browser_net_content-type.js
@@ -17,17 +17,17 @@ add_task(function* () {
   let {
     getDisplayedRequests,
     getSortedRequests,
   } = windowRequire("devtools/client/netmonitor/src/selectors/index");
 
   store.dispatch(Actions.batchEnable(false));
 
   let wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
-  yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+  yield ContentTask.spawn(tab.linkedBrowser, {}, function () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
   for (let requestItem of document.querySelectorAll(".request-list-item")) {
     let requestsListStatus = requestItem.querySelector(".requests-list-status");
     requestItem.scrollIntoView();
     EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
@@ -137,35 +137,35 @@ add_task(function* () {
       type: "plain",
       fullMimeType: "text/plain",
       transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 324),
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 10.73),
       time: true
     }
   );
 
-  yield selectIndexAndWaitForSourceEditor(0);
+  yield selectIndexAndWaitForSourceEditor(monitor, 0);
   yield testResponseTab("xml");
 
-  yield selectIndexAndWaitForSourceEditor(1);
+  yield selectIndexAndWaitForSourceEditor(monitor, 1);
   yield testResponseTab("css");
 
-  yield selectIndexAndWaitForSourceEditor(2);
+  yield selectIndexAndWaitForSourceEditor(monitor, 2);
   yield testResponseTab("js");
 
   yield selectIndexAndWaitForJSONView(3);
   yield testResponseTab("json");
 
-  yield selectIndexAndWaitForSourceEditor(4);
+  yield selectIndexAndWaitForSourceEditor(monitor, 4);
   yield testResponseTab("html");
 
   yield selectIndexAndWaitForImageView(5);
   yield testResponseTab("png");
 
-  yield selectIndexAndWaitForSourceEditor(6);
+  yield selectIndexAndWaitForSourceEditor(monitor, 6);
   yield testResponseTab("gzip");
 
   yield teardown(monitor);
 
   function* testResponseTab(type) {
     let tabpanel = document.querySelector("#response-panel");
 
     function checkVisibility(box) {
@@ -265,37 +265,31 @@ add_task(function* () {
 
         is(text, new Array(1000).join("Hello gzip!"),
           "The text shown in the source editor is incorrect for the gzip request.");
         break;
       }
     }
   }
 
-  function* selectIndexAndWaitForSourceEditor(index) {
-    let editor = document.querySelector("#response-panel .CodeMirror-code");
-    if (!editor) {
-      let waitDOM = waitForDOM(document, "#response-panel .CodeMirror-code");
-      EventUtils.sendMouseEvent({ type: "mousedown" },
-        document.querySelectorAll(".request-list-item")[index]);
-      document.querySelector("#response-tab").click();
-      yield waitDOM;
-    } else {
-      EventUtils.sendMouseEvent({ type: "mousedown" },
-        document.querySelectorAll(".request-list-item")[index]);
-    }
-  }
-
   function* selectIndexAndWaitForJSONView(index) {
+    let onResponseContent = monitor.panelWin.once(EVENTS.RECEIVED_RESPONSE_CONTENT);
     let tabpanel = document.querySelector("#response-panel");
     let waitDOM = waitForDOM(tabpanel, ".treeTable");
     store.dispatch(Actions.selectRequestByIndex(index));
     yield waitDOM;
+    yield onResponseContent;
+
+    // Waiting for RECEIVED_RESPONSE_CONTENT isn't enough.
+    // DOM may not be fully updated yet and checkVisibility(json) may still fail.
+    yield waitForTick();
   }
 
   function* selectIndexAndWaitForImageView(index) {
+    let onResponseContent = monitor.panelWin.once(EVENTS.RECEIVED_RESPONSE_CONTENT);
     let tabpanel = document.querySelector("#response-panel");
     let waitDOM = waitForDOM(tabpanel, ".response-image");
     store.dispatch(Actions.selectRequestByIndex(index));
     let [imageNode] = yield waitDOM;
     yield once(imageNode, "load");
+    yield onResponseContent;
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_headers-alignment.js
+++ b/devtools/client/netmonitor/test/browser_net_headers-alignment.js
@@ -5,17 +5,17 @@
 
 /**
  * Bug 1360457 - Mis-alignment between headers and columns on overflow
  */
 
 add_task(function* () {
   requestLongerTimeout(4);
 
-  let { monitor } = yield initNetMonitor(INFINITE_GET_URL, true);
+  let { tab, monitor } = yield initNetMonitor(INFINITE_GET_URL, true);
   let { document, windowRequire, store } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
 
   store.dispatch(Actions.batchEnable(false));
 
   // Wait until the first request makes the empty notice disappear
   yield waitForRequestListToAppear();
 
@@ -35,16 +35,21 @@ add_task(function* () {
     let aHeaderColumn = headers.childNodes[columnNumber];
     let aRequestColumn = firstRequestLine.childNodes[columnNumber];
     is(aHeaderColumn.getBoundingClientRect().left,
        aRequestColumn.getBoundingClientRect().left,
        "Headers for columns number " + columnNumber + " are aligned."
     );
   }
 
+  // Stop doing requests.
+  yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+    content.wrappedJSObject.stopRequests();
+  });
+
   // Done: clean up.
   return teardown(monitor);
 
   function waitForRequestListToAppear() {
     info("Waiting until the empty notice disappears and is replaced with the list");
     return waitUntil(() => !!document.querySelector(".requests-list-contents"));
   }
 
--- a/devtools/client/netmonitor/test/browser_net_resend_cors.js
+++ b/devtools/client/netmonitor/test/browser_net_resend_cors.js
@@ -51,25 +51,29 @@ add_task(function* () {
     info("Sending the cloned request (without change)");
     store.dispatch(Actions.sendCustomRequest(connector));
   });
 
   info("Waiting for both resent requests");
   yield onRequests;
 
   // Check the resent requests
-  ITEMS.forEach((item, i) => {
+  for (let i = 0; i < ITEMS.length; i++) {
+    let item = ITEMS[i];
     is(item.method, METHODS[i], `The ${item.method} request has the right method`);
     is(item.url, requestUrl, `The ${item.method} request has the right URL`);
     is(item.status, 200, `The ${item.method} response has the right status`);
 
     if (item.method === "POST") {
+      // Force fetching response content
+      let responseContent = yield connector.requestData(item.id, "responseContent");
+
       is(item.requestPostData.postData.text, "post-data",
         "The POST request has the right POST data");
       // eslint-disable-next-line mozilla/no-cpows-in-tests
-      is(item.responseContent.content.text, "Access-Control-Allow-Origin: *",
+      is(responseContent.content.text, "Access-Control-Allow-Origin: *",
         "The POST response has the right content");
     }
-  });
+  }
 
   info("Finishing the test");
   return teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_security-error.js
+++ b/devtools/client/netmonitor/test/browser_net_security-error.js
@@ -42,19 +42,16 @@ add_task(function* () {
    * completed.
    */
   function waitForSecurityBrokenNetworkEvent() {
     let awaitedEvents = [
       "UPDATING_REQUEST_HEADERS",
       "RECEIVED_REQUEST_HEADERS",
       "UPDATING_REQUEST_COOKIES",
       "RECEIVED_REQUEST_COOKIES",
-      "STARTED_RECEIVING_RESPONSE",
-      "UPDATING_RESPONSE_CONTENT",
-      "RECEIVED_RESPONSE_CONTENT",
       "UPDATING_EVENT_TIMINGS",
       "RECEIVED_EVENT_TIMINGS",
       "UPDATING_SECURITY_INFO",
       "RECEIVED_SECURITY_INFO",
     ];
 
     let promises = awaitedEvents.map((event) => {
       return monitor.panelWin.once(EVENTS[event]);
--- a/devtools/client/netmonitor/test/browser_net_security-state.js
+++ b/devtools/client/netmonitor/test/browser_net_security-state.js
@@ -75,49 +75,40 @@ add_task(function* () {
     yield executeRequests(1, "http://test1.example.com" + CORS_SJS_PATH);
     yield done;
 
     done = waitForNetworkEvents(monitor, 1);
     info("Requesting a resource over HTTPS.");
     yield executeRequests(1, "https://example.com" + CORS_SJS_PATH);
     yield done;
 
-    done = waitForSecurityBrokenNetworkEvent(true);
+    done = waitForSecurityBrokenNetworkEvent();
     info("Requesting a resource over HTTP to localhost.");
     yield executeRequests(1, "http://localhost" + CORS_SJS_PATH);
     yield done;
 
     const expectedCount = Object.keys(EXPECTED_SECURITY_STATES).length;
     is(store.getState().requests.requests.size,
       expectedCount,
       expectedCount + " events logged.");
   }
 
   /**
    * Returns a promise that's resolved once a request with security issues is
    * completed.
    */
-  function waitForSecurityBrokenNetworkEvent(networkError) {
+  function waitForSecurityBrokenNetworkEvent() {
     let awaitedEvents = [
       "UPDATING_REQUEST_HEADERS",
       "RECEIVED_REQUEST_HEADERS",
       "UPDATING_REQUEST_COOKIES",
       "RECEIVED_REQUEST_COOKIES",
-      "STARTED_RECEIVING_RESPONSE",
-      "UPDATING_RESPONSE_CONTENT",
-      "RECEIVED_RESPONSE_CONTENT",
       "UPDATING_EVENT_TIMINGS",
       "RECEIVED_EVENT_TIMINGS",
     ];
 
-    // If the reason for breakage is a network error, then the
-    // STARTED_RECEIVING_RESPONSE event does not fire.
-    if (networkError) {
-      awaitedEvents = awaitedEvents.filter(e => e !== "STARTED_RECEIVING_RESPONSE");
-    }
-
     let promises = awaitedEvents.map((event) => {
       return monitor.panelWin.once(EVENTS[event]);
     });
 
     return Promise.all(promises);
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_security-tab-visibility.js
+++ b/devtools/client/netmonitor/test/browser_net_security-tab-visibility.js
@@ -93,19 +93,16 @@ add_task(function* () {
    * completed.
    */
   function waitForSecurityBrokenNetworkEvent() {
     let awaitedEvents = [
       "UPDATING_REQUEST_HEADERS",
       "RECEIVED_REQUEST_HEADERS",
       "UPDATING_REQUEST_COOKIES",
       "RECEIVED_REQUEST_COOKIES",
-      "STARTED_RECEIVING_RESPONSE",
-      "UPDATING_RESPONSE_CONTENT",
-      "RECEIVED_RESPONSE_CONTENT",
       "UPDATING_EVENT_TIMINGS",
       "RECEIVED_EVENT_TIMINGS",
     ];
 
     let promises = awaitedEvents.map((event) => {
       return monitor.panelWin.once(EVENTS[event]);
     });
 
--- a/devtools/client/netmonitor/test/browser_net_simple-request-data.js
+++ b/devtools/client/netmonitor/test/browser_net_simple-request-data.js
@@ -245,50 +245,34 @@ function test() {
         SIMPLE_SJS,
         {
           status: "200",
           statusText: "Och Aye"
         }
       );
     });
 
-    expectEvent(EVENTS.RECEIVED_RESPONSE_CONTENT, async () => {
+    expectEvent(EVENTS.PAYLOAD_READY, async () => {
       await waitUntil(() => {
         let requestItem = getSortedRequests(store.getState()).get(0);
         return requestItem &&
                requestItem.transferredSize &&
                requestItem.contentSize &&
-               requestItem.mimeType &&
-               requestItem.responseContent;
+               requestItem.mimeType;
       });
 
       let requestItem = getSortedRequests(store.getState()).get(0);
 
       is(requestItem.transferredSize, "342",
         "The transferredSize data has an incorrect value.");
       is(requestItem.contentSize, "12",
         "The contentSize data has an incorrect value.");
       is(requestItem.mimeType, "text/plain; charset=utf-8",
         "The mimeType data has an incorrect value.");
 
-      ok(requestItem.responseContent,
-        "There should be a responseContent data available.");
-      // eslint-disable-next-line mozilla/no-cpows-in-tests
-      is(requestItem.responseContent.content.mimeType,
-        "text/plain; charset=utf-8",
-        "The responseContent data has an incorrect |content.mimeType| property.");
-      // eslint-disable-next-line mozilla/no-cpows-in-tests
-      is(requestItem.responseContent.content.text,
-        "Hello world!",
-        "The responseContent data has an incorrect |content.text| property.");
-      // eslint-disable-next-line mozilla/no-cpows-in-tests
-      is(requestItem.responseContent.content.size,
-        12,
-        "The responseContent data has an incorrect |content.size| property.");
-
       verifyRequestItemTarget(
         document,
         getDisplayedRequests(store.getState()),
         requestItem,
         "GET",
         SIMPLE_SJS,
         {
           type: "plain",
--- a/devtools/client/netmonitor/test/browser_net_streaming-response.js
+++ b/devtools/client/netmonitor/test/browser_net_streaming-response.js
@@ -60,37 +60,23 @@ add_task(function* () {
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector(".network-details-panel-toggle"));
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector("#response-tab"));
   yield wait;
 
   store.dispatch(Actions.selectRequest(null));
 
-  yield selectIndexAndWaitForSourceEditor(0);
+  yield selectIndexAndWaitForSourceEditor(monitor, 0);
   // the hls-m3u8 part
   testEditorContent(REQUESTS[0]);
 
-  yield selectIndexAndWaitForSourceEditor(1);
+  yield selectIndexAndWaitForSourceEditor(monitor, 1);
   // the mpeg-dash part
   testEditorContent(REQUESTS[1]);
 
   return teardown(monitor);
 
-  function* selectIndexAndWaitForSourceEditor(index) {
-    let editor = document.querySelector("#response-panel .CodeMirror-code");
-    if (!editor) {
-      let waitDOM = waitForDOM(document, "#response-panel .CodeMirror-code");
-      EventUtils.sendMouseEvent({ type: "mousedown" },
-        document.querySelectorAll(".request-list-item")[index]);
-      document.querySelector("#response-tab").click();
-      yield waitDOM;
-    } else {
-      EventUtils.sendMouseEvent({ type: "mousedown" },
-        document.querySelectorAll(".request-list-item")[index]);
-    }
-  }
-
   function testEditorContent([ fmt, textRe ]) {
     ok(document.querySelector(".CodeMirror-line").textContent.match(textRe),
       "The text shown in the source editor for " + fmt + " is correct.");
   }
 });
--- a/devtools/client/netmonitor/test/head.js
+++ b/devtools/client/netmonitor/test/head.js
@@ -1,24 +1,28 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /* import-globals-from ../../framework/test/shared-head.js */
+/* import-globals-from shared-head.js */
 /* exported Toolbox, restartNetMonitor, teardown, waitForExplicitFinish,
    verifyRequestItemTarget, waitFor, testFilterButtons, loadCommonFrameScript,
-   performRequestsInContent, waitForNetworkEvents */
+   performRequestsInContent, waitForNetworkEvents, selectIndexAndWaitForSourceEditor */
 
 "use strict";
 
 // shared-head.js handles imports, constants, and utility functions
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
   this);
 
-const { EVENTS } = require("devtools/client/netmonitor/src/constants");
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/netmonitor/test/shared-head.js",
+  this);
+
 const {
   getFormattedIPAndPort,
   getFormattedTime,
 } = require("devtools/client/netmonitor/src/utils/format-utils");
 const {
   decodeUnicodeUrl,
   getFormattedProtocol,
   getUrlBaseName,
@@ -276,16 +280,22 @@ function restartNetMonitor(monitor, newU
 }
 
 function teardown(monitor) {
   info("Destroying the specified network monitor.");
 
   return Task.spawn(function* () {
     let tab = monitor.toolbox.target.tab;
 
+    // Ensure that there is no pending RDP requests related to payload request
+    // done from FirefoxDataProvider.
+    info("Wait for completion of all pending RDP requests...");
+    yield waitForExistingRequests(monitor);
+    info("All pending requests finished.");
+
     let onDestroyed = monitor.once("destroyed");
     yield removeTab(tab);
     yield onDestroyed;
   });
 }
 
 function waitForNetworkEvents(monitor, getRequests, postRequests = 0) {
   return new Promise((resolve) => {
@@ -301,23 +311,24 @@ function waitForNetworkEvents(monitor, g
       ["UPDATING_REQUEST_COOKIES", onGenericEvent],
       ["RECEIVED_REQUEST_COOKIES", onGenericEvent],
       ["UPDATING_REQUEST_POST_DATA", onPostEvent],
       ["RECEIVED_REQUEST_POST_DATA", onPostEvent],
       ["UPDATING_RESPONSE_HEADERS", onGenericEvent],
       ["RECEIVED_RESPONSE_HEADERS", onGenericEvent],
       ["UPDATING_RESPONSE_COOKIES", onGenericEvent],
       ["RECEIVED_RESPONSE_COOKIES", onGenericEvent],
-      ["STARTED_RECEIVING_RESPONSE", onGenericEvent],
-      ["UPDATING_RESPONSE_CONTENT", onGenericEvent],
-      ["RECEIVED_RESPONSE_CONTENT", onGenericEvent],
       ["UPDATING_EVENT_TIMINGS", onGenericEvent],
       ["RECEIVED_EVENT_TIMINGS", onGenericEvent],
       ["PAYLOAD_READY", onPayloadReady]
     ];
+    let expectedGenericEvents = awaitedEventsToListeners
+      .filter(([, listener]) => listener == onGenericEvent).length;
+    let expectedPostEvents = awaitedEventsToListeners
+      .filter(([, listener]) => listener == onPostEvent).length;
 
     function initProgressForURL(url) {
       if (progress[url]) {
         return;
       }
       progress[url] = {};
       awaitedEventsToListeners.forEach(function ([e]) {
         progress[url][e] = 0;
@@ -360,32 +371,34 @@ function waitForNetworkEvents(monitor, g
       }
 
       payloadReady++;
       maybeResolve(event, actor, networkInfo);
     }
 
     function maybeResolve(event, actor, networkInfo) {
       info("> Network events progress: " +
-        genericEvents + "/" + ((getRequests + postRequests) * 13) + ", " +
-        postEvents + "/" + (postRequests * 2) + ", " +
+        "Payload: " + payloadReady + "/" + (getRequests + postRequests) + ", " +
+        "Generic: " + genericEvents + "/" +
+          ((getRequests + postRequests) * expectedGenericEvents) + ", " +
+        "Post: " + postEvents + "/" + (postRequests * expectedPostEvents) + ", " +
         "got " + event + " for " + actor);
 
       let url = networkInfo.request.url;
       updateProgressForURL(url, event);
 
       // Uncomment this to get a detailed progress logging (when debugging a test)
       // info("> Current state: " + JSON.stringify(progress, null, 2));
 
-      // There are 15 updates which need to be fired for a request to be
-      // considered finished. The "requestPostData" packet isn't fired for
-      // non-POST requests.
+      // There are `expectedGenericEvents` updates which need to be fired for a request
+      // to be considered finished. The "requestPostData" packet isn't fired for non-POST
+      // requests.
       if (payloadReady >= (getRequests + postRequests) &&
-        genericEvents >= (getRequests + postRequests) * 13 &&
-        postEvents >= postRequests * 2) {
+        genericEvents >= (getRequests + postRequests) * expectedGenericEvents &&
+        postEvents >= postRequests * expectedPostEvents) {
         awaitedEventsToListeners.forEach(([e, l]) => panel.off(EVENTS[e], l));
         executeSoon(resolve);
       }
     }
 
     awaitedEventsToListeners.forEach(([e, l]) => panel.on(EVENTS[e], l));
   });
 }
@@ -679,8 +692,30 @@ function waitForContentMessage(name) {
 
   return new Promise((resolve) => {
     mm.addMessageListener(name, function onMessage(msg) {
       mm.removeMessageListener(name, onMessage);
       resolve(msg);
     });
   });
 }
+
+/**
+ * Select a request and switch to its response panel.
+ *
+ * @param {Number} index The request index to be selected
+ */
+async function selectIndexAndWaitForSourceEditor(monitor, index) {
+  let document = monitor.panelWin.document;
+  let onResponseContent = monitor.panelWin.once(EVENTS.RECEIVED_RESPONSE_CONTENT);
+  // Select the request first, as it may try to fetch whatever is the current request's
+  // responseContent if we select the ResponseTab first.
+  EventUtils.sendMouseEvent({ type: "mousedown" },
+    document.querySelectorAll(".request-list-item")[index]);
+  // We may already be on the ResponseTab, so only select it if needed.
+  let editor = document.querySelector("#response-panel .CodeMirror-code");
+  if (!editor) {
+    let waitDOM = waitForDOM(document, "#response-panel .CodeMirror-code");
+    document.querySelector("#response-tab").click();
+    await waitDOM;
+  }
+  await onResponseContent;
+}
--- a/devtools/client/netmonitor/test/html_infinite-get-page.html
+++ b/devtools/client/netmonitor/test/html_infinite-get-page.html
@@ -26,18 +26,24 @@
             callback();
           }
         };
         xhr.send(null);
       }
 
       // Use a count parameter to defeat caching.
       let count = 0;
+      let doRequests = true;
+      function stopRequests() { // eslint-disable-line no-unused-vars
+        doRequests = false;
+      }
 
       (function performRequests() {
         get("request_" + (count++), function () {
-          setTimeout(performRequests, 50);
+          if (doRequests) {
+            setTimeout(performRequests, 50);
+          }
         });
       })();
     </script>
   </body>
 
 </html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/shared-head.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* exported EVENTS, waitForExistingRequests */
+
+"use strict";
+
+const { EVENTS } = require("devtools/client/netmonitor/src/constants");
+
+async function waitForExistingRequests(monitor) {
+  let { store } = monitor.panelWin;
+  function getRequests() {
+    return store.getState().requests.requests;
+  }
+  function areAllRequestsFullyLoaded() {
+    let requests = getRequests().valueSeq();
+    for (let request of requests) {
+      // Ignore cloned request as we don't lazily fetch data for them
+      // and have arbitrary number of field set.
+      if (request.id.includes("-clone")) {
+        continue;
+      }
+      // Do same check than FirefoxDataProvider.isRequestPayloadReady,
+      // in order to ensure there is no more pending payload requests to be done.
+      if (!request.requestHeaders || !request.requestCookies ||
+          !request.eventTimings ||
+          (!request.securityInfo && !request.fromServiceWorker) ||
+          ((!request.responseHeaders || !request.responseCookies) &&
+            request.securityState != "broken" &&
+            (!request.responseContentAvailable || request.status))) {
+        return false;
+      }
+    }
+    return true;
+  }
+  // If there is no request, we are good to go.
+  if (getRequests().size == 0) {
+    return;
+  }
+  while (!areAllRequestsFullyLoaded()) {
+    await monitor.panelWin.once(EVENTS.PAYLOAD_READY);
+  }
+}
--- a/devtools/client/styleeditor/test/browser.ini
+++ b/devtools/client/styleeditor/test/browser.ini
@@ -53,16 +53,17 @@ support-files =
   doc_xulpage.xul
   sync.html
   utf-16.css
   !/devtools/client/commandline/test/helpers.js
   !/devtools/client/framework/test/shared-head.js
   !/devtools/client/inspector/shared/test/head.js
   !/devtools/client/inspector/test/head.js
   !/devtools/client/inspector/test/shared-head.js
+  !/devtools/client/netmonitor/test/shared-head.js
   !/devtools/client/responsive.html/test/browser/devices.json
   !/devtools/client/shared/test/test-actor-registry.js
   !/devtools/client/shared/test/test-actor.js
 
 [browser_styleeditor_add_stylesheet.js]
 [browser_styleeditor_autocomplete.js]
 [browser_styleeditor_autocomplete-disabled.js]
 [browser_styleeditor_bom.js]
--- a/devtools/client/styleeditor/test/browser_styleeditor_fetch-from-cache.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_fetch-from-cache.js
@@ -1,16 +1,21 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
+/* import-globals-from ../../netmonitor/test/shared-head.js */
+
 // A test to ensure Style Editor doesn't bybass cache when loading style sheet
 // contents (bug 978688).
 
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/netmonitor/test/shared-head.js", this);
+
 const TEST_URL = TEST_BASE_HTTP + "doc_uncached.html";
 
 add_task(function* () {
   // Disable rcwn to make cache behavior deterministic.
   yield pushPref("network.http.rcwn.enabled", false);
 
   info("Opening netmonitor");
   let tab = yield addTab("about:blank");
@@ -29,16 +34,18 @@ add_task(function* () {
   yield navigateTo(TEST_URL);
 
   info("Opening Style Editor");
   let styleeditor = yield toolbox.selectTool("styleeditor");
 
   info("Waiting for the source to be loaded.");
   yield styleeditor.UI.editors[0].getSourceEditor();
 
+  yield waitForExistingRequests(monitor);
+
   info("Checking Netmonitor contents.");
   let items = [];
   for (let item of getSortedRequests(store.getState())) {
     if (item.url.endsWith("doc_uncached.css")) {
       items.push(item);
     }
   }
 
--- a/devtools/client/webconsole/new-console-output/components/message-types/NetworkEventMessage.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/NetworkEventMessage.js
@@ -111,16 +111,19 @@ function NetworkEventMessage({
     getLongString: (grip) => {
       return serviceContainer.getLongString(grip);
     },
     getTabTarget: () => {},
     getNetworkRequest: () => {},
     sendHTTPRequest: () => {},
     setPreferences: () => {},
     triggerActivity: () => {},
+    requestData: (requestId, dataType) => {
+      return serviceContainer.requestData(requestId, dataType);
+    },
   };
 
   // Only render the attachment if the network-event is
   // actually opened (performance optimization).
   const attachment = open && dom.div({
     className: "network-info network-monitor devtools-monospace"},
     TabboxPanel({
       connector,
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -91,16 +91,19 @@ NewConsoleOutputWrapper.prototype = {
           hud.owner.openLink(url);
         },
         createElement: nodename => {
           return this.document.createElement(nodename);
         },
         getLongString: (grip) => {
           return hud.proxy.webConsoleClient.getString(grip);
         },
+        requestData(id, type) {
+          return hud.proxy.networkDataProvider.requestData(id, type);
+        },
       };
 
       // Set `openContextMenu` this way so, `serviceContainer` variable
       // is available in the current scope and we can pass it into
       // `createContextMenu` method.
       serviceContainer.openContextMenu = (e, message) => {
         let { screenX, screenY, target } = e;
 
--- a/devtools/client/webconsole/new-console-output/store.js
+++ b/devtools/client/webconsole/new-console-output/store.js
@@ -172,16 +172,23 @@ function enableNetProvider(hud) {
       // Data provider implements async logic for fetching
       // data from the backend. It's created the first
       // time it's needed.
       if (!dataProvider) {
         dataProvider = new DataProvider({
           actions,
           webConsoleClient: proxy.webConsoleClient
         });
+
+        // /!\ This is terrible, but it allows ResponsePanel to be able to call
+        // `dataProvider.requestData` to fetch response content lazily.
+        // `proxy.networkDataProvider` is put by NewConsoleOutputWrapper on
+        // `serviceContainer` which allow NetworkEventMessage to expose requestData on
+        // the fake `connector` object it hands over to ResponsePanel.
+        proxy.networkDataProvider = dataProvider;
       }
 
       let type = action.type;
       let newState = reducer(state, action);
 
       // If network message has been opened, fetch all HTTP details
       // from the backend. It can happen (especially in test) that
       // the message is opened before all network event updates are
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_netmonitor_shows_reqs_in_webconsole.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_netmonitor_shows_reqs_in_webconsole.js
@@ -59,9 +59,11 @@ async function testNetmonitor(toolbox) {
   await waitUntil(() => store.getState().requests.requests.size > 0);
 
   is(store.getState().requests.requests.size, 1,
     "Network request appears in the network panel");
 
   let item = getSortedRequests(store.getState()).get(0);
   is(item.method, "GET", "The attached method is correct.");
   is(item.url, TEST_PATH, "The attached url is correct.");
+
+  await waitForExistingRequests(monitor);
 }
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_network_attach.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_network_attach.js
@@ -44,16 +44,18 @@ add_task(async function task() {
   // Expand network log
   urlNode.click();
 
   await consoleReady;
 
   info("network-request-payload-ready received");
 
   await testNetworkMessage(messageNode);
+
+  await waitForExistingRequests(monitor);
 });
 
 async function testNetworkMessage(messageNode) {
   let headersTab = messageNode.querySelector("#headers-tab");
 
   ok(headersTab, "Headers tab is available");
 
   // Headers tab should be selected by default, so just check its content.
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_network_messages_openinnet.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_network_messages_openinnet.js
@@ -70,9 +70,12 @@ async function testNetmonitorLink(toolbo
   store.dispatch(actions.batchEnable(false));
 
   await waitUntil(() => {
     const selected = getSelectedRequest(store.getState());
     return selected && selected.url === url;
   });
 
   ok(true, "The attached url is correct.");
+
+  let monitor = toolbox.getCurrentPanel();
+  await waitForExistingRequests(monitor);
 }
--- a/devtools/client/webconsole/new-console-output/test/mochitest/head.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/head.js
@@ -1,23 +1,27 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 /* import-globals-from ../../../../framework/test/shared-head.js */
+/* import-globals-from ../../../../netmonitor/test/shared-head.js */
 /* eslint no-unused-vars: [2, {"vars": "local"}] */
 
 "use strict";
 
 // shared-head.js handles imports, constants, and utility functions
 // Load the shared-head file first.
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
   this);
 
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/netmonitor/test/shared-head.js", this);
+
 var {HUDService} = require("devtools/client/webconsole/hudservice");
 var WCUL10n = require("devtools/client/webconsole/webconsole-l10n");
 const DOCS_GA_PARAMS = "?utm_source=mozilla" +
                        "&utm_medium=firefox-console-errors" +
                        "&utm_campaign=default";
 
 Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", true);
 registerCleanupFunction(function* () {
--- a/devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js
+++ b/devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js
@@ -1,15 +1,18 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/netmonitor/test/shared-head.js", this);
+
 const TEST_URI = "data:text/html;charset=utf8,Test that the netmonitor " +
                  "displays requests that have been recorded in the " +
                  "web console, even if the netmonitor hadn't opened yet.";
 
 const TEST_FILE = "test-network-request.html";
 const TEST_PATH = "http://example.com/browser/devtools/client/webconsole/" +
                   "test/" + TEST_FILE;
 
@@ -71,9 +74,11 @@ function* testNetmonitor(toolbox) {
 
   yield waitUntil(() => store.getState().requests.requests.size > 0);
 
   is(store.getState().requests.requests.size, 1, "Network request appears in the network panel");
 
   let item = getSortedRequests(store.getState()).get(0);
   is(item.method, "GET", "The attached method is correct.");
   is(item.url, TEST_PATH, "The attached url is correct.");
+
+  yield waitForExistingRequests(monitor);
 }
--- a/devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js
+++ b/devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js
@@ -1,34 +1,41 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+/* import-globals-from ../../netmonitor/test/shared-head.js */
+
 // Tests that network log messages bring up the network panel.
 
 "use strict";
 
 const TEST_NETWORK_REQUEST_URI =
   "http://example.com/browser/devtools/client/webconsole/test/" +
   "test-network-request.html";
 
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/netmonitor/test/shared-head.js", this);
+
 add_task(function* () {
   let finishedRequest = waitForFinishedRequest(({ request }) => {
     return request.url.endsWith("test-network-request.html");
   });
 
   const hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI);
   let request = yield finishedRequest;
 
   yield hud.ui.openNetworkPanel(request.actor);
   let toolbox = gDevTools.getToolbox(hud.target);
   is(toolbox.currentToolId, "netmonitor", "Network panel was opened");
-  let panel = toolbox.getCurrentPanel();
+  let monitor = toolbox.getCurrentPanel();
 
-  let { store, windowRequire } = panel.panelWin;
+  let { store, windowRequire } = monitor.panelWin;
   let { getSelectedRequest } = windowRequire("devtools/client/netmonitor/src/selectors/index");
 
   let selected = getSelectedRequest(store.getState());
   is(selected.method, request.request.method,
      "The correct request is selected");
   is(selected.url, request.request.url,
      "The correct request is definitely selected");
+
+  yield waitForExistingRequests(monitor);
 });
--- a/devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js
+++ b/devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js
@@ -1,23 +1,28 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+/* import-globals-from ../../netmonitor/test/shared-head.js */
+
 // Tests that network log messages bring up the network panel and select the
 // right request even if it was previously filtered off.
 
 "use strict";
 
 const TEST_FILE_URI =
   "http://example.com/browser/devtools/client/webconsole/test/" +
   "test-network.html";
 const TEST_URI = "data:text/html;charset=utf8,<p>test file URI";
 
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/netmonitor/test/shared-head.js", this);
+
 var hud;
 
 add_task(function* () {
   let requests = [];
   let { browser } = yield loadTab(TEST_URI);
 
   yield pushPrefEnv();
   hud = yield openConsole();
@@ -32,18 +37,18 @@ add_task(function* () {
   yield testMessages();
   let htmlRequest = requests.find(e => e.request.url.endsWith("html"));
   ok(htmlRequest, "htmlRequest was a html");
 
   yield hud.ui.openNetworkPanel(htmlRequest.actor);
   let toolbox = gDevTools.getToolbox(hud.target);
   is(toolbox.currentToolId, "netmonitor", "Network panel was opened");
 
-  let panel = toolbox.getCurrentPanel();
-  let { store, windowRequire } = panel.panelWin;
+  let monitor = toolbox.getCurrentPanel();
+  let { store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let { getSelectedRequest } = windowRequire("devtools/client/netmonitor/src/selectors/index");
 
   let selected = getSelectedRequest(store.getState());
   is(selected.method, htmlRequest.request.method,
      "The correct request is selected");
   is(selected.url, htmlRequest.request.url,
      "The correct request is definitely selected");
@@ -59,16 +64,18 @@ add_task(function* () {
   is(selected.method, htmlRequest.request.method,
      "The correct request is selected");
   is(selected.url, htmlRequest.request.url,
      "The correct request is definitely selected");
 
   // All tests are done. Shutdown.
   HUDService.lastFinishedRequest.callback = null;
   htmlRequest = browser = requests = hud = null;
+
+  yield waitForExistingRequests(monitor);
 });
 
 function testMessages() {
   return waitForMessages({
     webconsole: hud,
     messages: [{
       text: "running network console logging tests",
       category: CATEGORY_WEBDEV,