Bug 1418927 - requestHeaders and responseHeaders should be loaded lazily r=Honza,ochameau
authorRicky Chien <ricky060709@gmail.com>
Fri, 01 Dec 2017 18:40:36 +0800
changeset 448232 acc656fd0c97962779b46e11b3500cc3dc7d69f4
parent 448231 9a512e69ff1bc71dafb838410fe413a6a5a4527a
child 448233 d41781d12703d6daaa7687cb75d1a9f9938baa8c
push id8527
push userCallek@gmail.com
push dateThu, 11 Jan 2018 21:05:50 +0000
treeherdermozilla-beta@95342d212a7a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersHonza, ochameau
bugs1418927
milestone59.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 1418927 - requestHeaders and responseHeaders should be loaded lazily r=Honza,ochameau MozReview-Commit-ID: 5ADM5AYdJNI
devtools/client/netmonitor/src/components/RequestListContent.js
devtools/client/netmonitor/src/components/RequestListItem.js
devtools/client/netmonitor/src/components/StackTracePanel.js
devtools/client/netmonitor/src/components/Toolbar.js
devtools/client/netmonitor/src/connector/firefox-connector.js
devtools/client/netmonitor/src/connector/firefox-data-provider.js
devtools/client/netmonitor/src/constants.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_throttle_upload.js
devtools/client/netmonitor/src/utils/filter-autocomplete-provider.js
devtools/client/netmonitor/src/utils/request-utils.js
devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
devtools/client/netmonitor/test/browser.ini
devtools/client/netmonitor/test/browser_net_cause_redirect.js
devtools/client/netmonitor/test/browser_net_copy_headers.js
devtools/client/netmonitor/test/browser_net_curl-utils.js
devtools/client/netmonitor/test/browser_net_filter-01.js
devtools/client/netmonitor/test/browser_net_filter-flags.js
devtools/client/netmonitor/test/browser_net_headers_sorted.js
devtools/client/netmonitor/test/browser_net_persistent_logs.js
devtools/client/netmonitor/test/browser_net_raw_headers.js
devtools/client/netmonitor/test/browser_net_resend.js
devtools/client/netmonitor/test/browser_net_resend_cors.js
devtools/client/netmonitor/test/browser_net_resend_headers.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_status-codes.js
devtools/client/netmonitor/test/browser_net_timing-division.js
devtools/client/netmonitor/test/browser_net_waterfall-click.js
devtools/client/netmonitor/test/head.js
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/test/mochitest/browser.ini
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_expand.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
@@ -237,16 +237,17 @@ class RequestListContent extends Compone
       connector,
       columns,
       displayedRequests,
       firstRequestStartedMillis,
       onCauseBadgeMouseDown,
       onItemMouseDown,
       onSecurityIconMouseDown,
       onWaterfallMouseDown,
+      requestFilterTypes,
       scale,
       selectedRequest,
     } = this.props;
 
     return (
       div({ className: "requests-list-wrapper" },
         div({ className: "requests-list-table" },
           div({
@@ -267,16 +268,17 @@ class RequestListContent extends Compone
               isSelected: item.id === (selectedRequest && selectedRequest.id),
               key: item.id,
               onContextMenu: this.onContextMenu,
               onFocusedNodeChange: this.onFocusedNodeChange,
               onMouseDown: () => onItemMouseDown(item.id),
               onCauseBadgeMouseDown: () => onCauseBadgeMouseDown(item.cause),
               onSecurityIconMouseDown: () => onSecurityIconMouseDown(item.securityState),
               onWaterfallMouseDown: () => onWaterfallMouseDown(),
+              requestFilterTypes,
             }))
           )
         )
       )
     );
   }
 }
 
--- a/devtools/client/netmonitor/src/components/RequestListItem.js
+++ b/devtools/client/netmonitor/src/components/RequestListItem.js
@@ -2,17 +2,20 @@
  * 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 { Component, createFactory } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const { propertiesEqual } = require("../utils/request-utils");
+const {
+  fetchNetworkUpdatePacket,
+  propertiesEqual,
+} = require("../utils/request-utils");
 const { RESPONSE_HEADERS } = require("../constants");
 
 // Components
 const RequestListColumnCause = createFactory(require("./RequestListColumnCause"));
 const RequestListColumnContentSize = createFactory(require("./RequestListColumnContentSize"));
 const RequestListColumnCookies = createFactory(require("./RequestListColumnCookies"));
 const RequestListColumnDomain = createFactory(require("./RequestListColumnDomain"));
 const RequestListColumnDuration = createFactory(require("./RequestListColumnDuration"));
@@ -52,23 +55,26 @@ const UPDATED_REQ_ITEM_PROPS = [
   "url",
   "remoteAddress",
   "cause",
   "contentSize",
   "transferredSize",
   "startedMillis",
   "totalTime",
   "requestCookies",
+  "requestHeaders",
   "responseCookies",
+  "responseHeaders",
 ];
 
 const UPDATED_REQ_PROPS = [
   "firstRequestStartedMillis",
   "index",
   "isSelected",
+  "requestFilterTypes",
   "waterfallWidth",
 ];
 
 /**
  * Render one row in the request list.
  */
 class RequestListItem extends Component {
   static get propTypes() {
@@ -81,24 +87,45 @@ class RequestListItem extends Component 
       firstRequestStartedMillis: PropTypes.number.isRequired,
       fromCache: PropTypes.bool,
       onCauseBadgeMouseDown: PropTypes.func.isRequired,
       onContextMenu: PropTypes.func.isRequired,
       onFocusedNodeChange: PropTypes.func,
       onMouseDown: PropTypes.func.isRequired,
       onSecurityIconMouseDown: PropTypes.func.isRequired,
       onWaterfallMouseDown: PropTypes.func.isRequired,
+      requestFilterTypes: PropTypes.string.isRequired,
       waterfallWidth: PropTypes.number,
     };
   }
 
   componentDidMount() {
     if (this.props.isSelected) {
       this.refs.listItem.focus();
     }
+
+    let { connector, item, requestFilterTypes } = this.props;
+    // Filtering XHR & WS require to lazily fetch requestHeaders & responseHeaders
+    if (requestFilterTypes.get("xhr") || requestFilterTypes.get("ws")) {
+      fetchNetworkUpdatePacket(connector.requestData, item, [
+        "requestHeaders",
+        "responseHeaders",
+      ]);
+    }
+  }
+
+  componentWillReceiveProps(nextProps) {
+    let { connector, item, requestFilterTypes } = nextProps;
+    // Filtering XHR & WS require to lazily fetch requestHeaders & responseHeaders
+    if (requestFilterTypes.get("xhr") || requestFilterTypes.get("ws")) {
+      fetchNetworkUpdatePacket(connector.requestData, item, [
+        "requestHeaders",
+        "responseHeaders",
+      ]);
+    }
   }
 
   shouldComponentUpdate(nextProps) {
     return !propertiesEqual(UPDATED_REQ_ITEM_PROPS, this.props.item, nextProps.item) ||
       !propertiesEqual(UPDATED_REQ_PROPS, this.props, nextProps) ||
       this.props.columns !== nextProps.columns;
   }
 
--- a/devtools/client/netmonitor/src/components/StackTracePanel.js
+++ b/devtools/client/netmonitor/src/components/StackTracePanel.js
@@ -41,18 +41,16 @@ class StackTracePanel extends Component 
    * `componentWillReceiveProps` is the only method called when
    * switching between two requests while this panel is displayed.
    */
   componentWillReceiveProps(nextProps) {
     let { request, connector } = nextProps;
     fetchNetworkUpdatePacket(connector.requestData, request, ["stackTrace"]);
   }
 
-  // Rendering
-
   render() {
     let {
       connector,
       openLink,
       request,
       sourceMapService,
     } = this.props;
 
--- a/devtools/client/netmonitor/src/components/Toolbar.js
+++ b/devtools/client/netmonitor/src/components/Toolbar.js
@@ -13,19 +13,19 @@ const I = require("devtools/client/share
 
 const Actions = require("../actions/index");
 const { FILTER_SEARCH_DELAY, FILTER_TAGS } = require("../constants");
 const {
   getRecordingState,
   getTypeFilteredRequests,
   isNetworkDetailsToggleButtonDisabled,
 } = require("../selectors/index");
-
 const { autocompleteProvider } = require("../utils/filter-autocomplete-provider");
 const { L10N } = require("../utils/l10n");
+const { fetchNetworkUpdatePacket } = require("../utils/request-utils");
 
 // Components
 const SearchBox = createFactory(require("devtools/client/shared/components/SearchBox"));
 
 const { button, div, input, label, span } = dom;
 
 // Localization
 const COLLAPSE_DETAILS_PANE = L10N.getStr("collapseDetailsPane");
@@ -130,19 +130,22 @@ class Toolbar extends Component {
 
   autocompleteProvider(filter) {
     return autocompleteProvider(filter, this.props.filteredRequests);
   }
 
   onSearchBoxFocus() {
     let { connector, filteredRequests } = this.props;
 
-    // Fetch responseCookies for building autocomplete list
+    // Fetch responseCookies & responseHeaders for building autocomplete list
     filteredRequests.forEach((request) => {
-      connector.requestData(request.id, "responseCookies");
+      fetchNetworkUpdatePacket(connector.requestData, request, [
+        "responseCookies",
+        "responseHeaders",
+      ]);
     });
   }
 
   render() {
     let {
       toggleRecording,
       clearRequests,
       requestFilterTypes,
--- a/devtools/client/netmonitor/src/connector/firefox-connector.js
+++ b/devtools/client/netmonitor/src/connector/firefox-connector.js
@@ -66,26 +66,28 @@ class FirefoxConnector {
   }
 
   async disconnect() {
     this.actions.batchReset();
 
     this.removeListeners();
 
     if (this.tabTarget) {
+      // Unregister `will-navigate` needs to be done before `this.timelineFront.destroy()`
+      // since this.tabTarget might be nullified after timelineFront.destroy().
+      this.tabTarget.off("will-navigate");
       // The timeline front wasn't initialized and started if the server wasn't
       // recent enough to emit the markers we were interested in.
       if (this.tabTarget.getTrait("documentLoadingMarkers") && this.timelineFront) {
         this.timelineFront.off("doc-loading", this.onDocLoadingMarker);
         await this.timelineFront.destroy();
       }
-
-      this.tabTarget.off("will-navigate");
       this.tabTarget = null;
     }
+
     this.webConsoleClient = null;
     this.timelineFront = null;
     this.dataProvider = null;
     this.panel = null;
   }
 
   pause() {
     this.removeListeners();
--- a/devtools/client/netmonitor/src/connector/firefox-data-provider.js
+++ b/devtools/client/netmonitor/src/connector/firefox-data-provider.js
@@ -19,26 +19,24 @@ const { fetchHeaders } = require("../uti
  */
 class FirefoxDataProvider {
   constructor({webConsoleClient, actions}) {
     // Options
     this.webConsoleClient = webConsoleClient;
     this.actions = actions;
 
     // Internal properties
-    this.payloadQueue = [];
-    this.rdpRequestMap = new Map();
+    this.payloadQueue = 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);
-    this.getRequestFromQueue = this.getRequestFromQueue.bind(this);
 
     // Event handlers
     this.onNetworkEvent = this.onNetworkEvent.bind(this);
     this.onNetworkEventUpdate = this.onNetworkEventUpdate.bind(this);
   }
 
   /**
    * Add a new network request to application state.
@@ -67,33 +65,31 @@ class FirefoxDataProvider {
         cause,
 
         // Compatibility code to support Firefox 58 and earlier that always
         // send stack-trace immediately on networkEvent message.
         // FF59+ supports fetching the traces lazily via requestData.
         stacktrace: cause.stacktrace,
 
         fromCache,
-        fromServiceWorker},
-        true,
-      );
+        fromServiceWorker,
+      }, true);
     }
 
     emit(EVENTS.REQUEST_ADDED, id);
   }
 
   /**
    * Update a network request if it already exists in application state.
    *
    * @param {string} id request id
    * @param {object} data data payload will be updated to application state
    */
   async updateRequest(id, data) {
     let {
-      mimeType,
       responseContent,
       responseCookies,
       responseHeaders,
       requestCookies,
       requestHeaders,
       requestPostData,
     } = data;
 
@@ -101,17 +97,17 @@ class FirefoxDataProvider {
     let [
       responseContentObj,
       requestHeadersObj,
       responseHeadersObj,
       postDataObj,
       requestCookiesObj,
       responseCookiesObj,
     ] = await Promise.all([
-      this.fetchResponseContent(mimeType, responseContent),
+      this.fetchResponseContent(responseContent),
       this.fetchRequestHeaders(requestHeaders),
       this.fetchResponseHeaders(responseHeaders),
       this.fetchPostData(requestPostData),
       this.fetchRequestCookies(requestCookies),
       this.fetchResponseCookies(responseCookies),
     ]);
 
     let payload = Object.assign({},
@@ -119,50 +115,64 @@ class FirefoxDataProvider {
       responseContentObj,
       requestHeadersObj,
       responseHeadersObj,
       postDataObj,
       requestCookiesObj,
       responseCookiesObj
     );
 
-    this.pushRequestToQueue(id, payload);
+    if (this.actions.updateRequest) {
+      await this.actions.updateRequest(id, payload, true);
+    }
 
     return payload;
   }
 
-  async fetchResponseContent(mimeType, responseContent) {
+  async fetchResponseContent(responseContent) {
     let payload = {};
-    if (mimeType && responseContent && responseContent.content) {
+    if (responseContent && responseContent.content) {
       let { text } = responseContent.content;
       let response = await this.getLongString(text);
       responseContent.content.text = response;
       payload.responseContent = responseContent;
+
+      // Lock down responseContentAvailable once we fetch data from back-end.
+      // Using this as flag to prevent fetching arrived data again.
+      payload.responseContentAvailable = false;
     }
     return payload;
   }
 
   async fetchRequestHeaders(requestHeaders) {
     let payload = {};
     if (requestHeaders && requestHeaders.headers && requestHeaders.headers.length) {
       let headers = await fetchHeaders(requestHeaders, this.getLongString);
       if (headers) {
         payload.requestHeaders = headers;
       }
+
+      // Lock down requestHeadersAvailable once we fetch data from back-end.
+      // Using this as flag to prevent fetching arrived data again.
+      payload.requestHeadersAvailable = false;
     }
     return payload;
   }
 
   async fetchResponseHeaders(responseHeaders) {
     let payload = {};
     if (responseHeaders && responseHeaders.headers && responseHeaders.headers.length) {
       let headers = await fetchHeaders(responseHeaders, this.getLongString);
       if (headers) {
         payload.responseHeaders = headers;
       }
+
+      // Lock down responseHeadersAvailable once we fetch data from back-end.
+      // Using this as flag to prevent fetching arrived data again.
+      payload.responseHeadersAvailable = false;
     }
     return payload;
   }
 
   async fetchPostData(requestPostData) {
     let payload = {};
     if (requestPostData && requestPostData.postData) {
       let { text } = requestPostData.postData;
@@ -181,16 +191,42 @@ class FirefoxDataProvider {
 
       // Lock down requestPostDataAvailable once we fetch data from back-end.
       // Using this as flag to prevent fetching arrived data again.
       payload.requestPostDataAvailable = false;
     }
     return payload;
   }
 
+  async fetchRequestCookies(requestCookies) {
+    let payload = {};
+    if (requestCookies) {
+      let reqCookies = [];
+      // request store cookies in requestCookies or requestCookies.cookies
+      let cookies = requestCookies.cookies ?
+        requestCookies.cookies : requestCookies;
+      // make sure cookies is iterable
+      if (typeof cookies[Symbol.iterator] === "function") {
+        for (let cookie of cookies) {
+          reqCookies.push(Object.assign({}, cookie, {
+            value: await this.getLongString(cookie.value),
+          }));
+        }
+        if (reqCookies.length) {
+          payload.requestCookies = reqCookies;
+        }
+      }
+
+      // Lock down requestCookiesAvailable once we fetch data from back-end.
+      // Using this as flag to prevent fetching arrived data again.
+      payload.requestCookiesAvailable = false;
+    }
+    return payload;
+  }
+
   async fetchResponseCookies(responseCookies) {
     let payload = {};
     if (responseCookies) {
       let resCookies = [];
       // response store cookies in responseCookies or responseCookies.cookies
       let cookies = responseCookies.cookies ?
         responseCookies.cookies : responseCookies;
       // make sure cookies is iterable
@@ -207,103 +243,38 @@ class FirefoxDataProvider {
 
       // Lock down responseCookiesAvailable once we fetch data from back-end.
       // Using this as flag to prevent fetching arrived data again.
       payload.responseCookiesAvailable = false;
     }
     return payload;
   }
 
-  async fetchRequestCookies(requestCookies) {
-    let payload = {};
-    if (requestCookies) {
-      let reqCookies = [];
-      // request store cookies in requestCookies or requestCookies.cookies
-      let cookies = requestCookies.cookies ?
-        requestCookies.cookies : requestCookies;
-      // make sure cookies is iterable
-      if (typeof cookies[Symbol.iterator] === "function") {
-        for (let cookie of cookies) {
-          reqCookies.push(Object.assign({}, cookie, {
-            value: await this.getLongString(cookie.value),
-          }));
-        }
-        if (reqCookies.length) {
-          payload.requestCookies = reqCookies;
-        }
-      }
-
-      // Lock down requestCookiesAvailable once we fetch data from back-end.
-      // Using this as flag to prevent fetching arrived data again.
-      payload.requestCookiesAvailable = false;
-    }
-    return payload;
-  }
-
-  /**
-   * Access a payload item from payload queue.
-   *
-   * @param {string} id request id
-   * @return {boolean} return a queued payload item from queue.
-   */
-  getRequestFromQueue(id) {
-    return this.payloadQueue.find((item) => item.id === id);
-  }
-
   /**
    * Public API used by the Toolbox: Tells if there is still any pending request.
    *
    * @return {boolean} returns true if the payload queue is empty
    */
   isPayloadQueueEmpty() {
-    return this.payloadQueue.length === 0;
-  }
-
-  /**
-   * Return true if payload is ready (all data fetched from the backend)
-   *
-   * @param {string} id request id
-   * @return {boolean} return whether a specific networkEvent has been updated completely.
-   */
-  isRequestPayloadReady(id) {
-    let record = this.rdpRequestMap.get(id);
-    if (!record) {
-      return false;
-    }
-
-    let { payload } = this.getRequestFromQueue(id);
-
-    // The payload is ready when all values in the record are true.
-    // Note that we never fetch response header/cookies for request with security issues.
-    // Bug 1404917 should simplify this heuristic by making all these field be lazily
-    // fetched, only on-demand.
-    return record.requestHeaders && record.eventTimings &&
-      (record.responseHeaders || payload.securityState === "broken" ||
-        (!payload.status && payload.responseContentAvailable));
+    return this.payloadQueue.size === 0;
   }
 
   /**
    * Merge upcoming networkEventUpdate payload into existing one.
    *
-   * @param {string} id request id
+   * @param {string} id request actor id
    * @param {object} payload request data payload
    */
   pushRequestToQueue(id, payload) {
-    let request = this.getRequestFromQueue(id);
-    if (!request) {
-      this.payloadQueue.push({ id, payload });
-    } else {
-      // Merge upcoming networkEventUpdate payload into existing one
-      request.payload = Object.assign({}, request.payload, payload);
+    let payloadFromQueue = this.payloadQueue.get(id);
+    if (!payloadFromQueue) {
+      payloadFromQueue = {};
+      this.payloadQueue.set(id, payloadFromQueue);
     }
-  }
-
-  cleanUpQueue(id) {
-    this.payloadQueue = this.payloadQueue.filter(
-      request => request.id != id);
+    Object.assign(payloadFromQueue, payload);
   }
 
   /**
    * Fetches the network information packet from actor server
    *
    * @param {string} id request id
    * @return {object} networkInfo data packet
    */
@@ -327,38 +298,31 @@ class FirefoxDataProvider {
   }
 
   /**
    * The "networkEvent" message type handler.
    *
    * @param {string} type message type
    * @param {object} networkInfo network request information
    */
-  onNetworkEvent(type, networkInfo) {
+  async onNetworkEvent(type, networkInfo) {
     let {
       actor,
       cause,
       fromCache,
       fromServiceWorker,
       isXHR,
       request: {
         method,
         url,
       },
       startedDateTime,
     } = networkInfo;
 
-    // Create tracking record for this request.
-    this.rdpRequestMap.set(actor, {
-      requestHeaders: false,
-      responseHeaders: false,
-      eventTimings: false,
-    });
-
-    this.addRequest(actor, {
+    await this.addRequest(actor, {
       cause,
       fromCache,
       fromServiceWorker,
       isXHR,
       method,
       startedDateTime,
       url,
     });
@@ -368,126 +332,82 @@ class FirefoxDataProvider {
 
   /**
    * The "networkEventUpdate" message type handler.
    *
    * @param {string} type message type
    * @param {object} packet the message received from the server.
    * @param {object} networkInfo the network request information.
    */
-  onNetworkEventUpdate(type, data) {
+  async 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":
-      case "responseHeaders":
-        this.requestPayloadData(actor, updateType);
-        break;
-      case "requestCookies":
-      case "responseCookies":
-      case "requestPostData":
-        // This field helps knowing when/if updateType property is available
-        // and can be requested via `requestData`
-        this.updateRequest(actor, { [`${updateType}Available`]: true });
-        break;
       case "securityInfo":
-        this.updateRequest(actor, { securityState: networkInfo.securityInfo });
+        this.pushRequestToQueue(actor, { securityState: networkInfo.securityInfo });
         break;
       case "responseStart":
-        this.updateRequest(actor, {
+        this.pushRequestToQueue(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);
         });
+        emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
         break;
       case "responseContent":
-        this.updateRequest(actor, {
+        this.pushRequestToQueue(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.pushRequestToQueue(actor, { totalTime: networkInfo.totalTime });
-        this.requestPayloadData(actor, updateType);
+        await this._requestData(actor, updateType);
         break;
     }
 
+    // This available field helps knowing when/if updateType property is arrived
+    // and can be requested via `requestData`
+    this.pushRequestToQueue(actor, { [`${updateType}Available`]: true });
+
+    this.onPayloadDataReceived(actor);
+
     emit(EVENTS.NETWORK_EVENT_UPDATED, actor);
   }
 
   /**
-   * 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.
-   *
-   * All these requests are cached into `rdpRequestMap`. All requests related to a given
-   * actor will be collected in the same record.
-   *
-   * 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
+   * Notify actions when messages from onNetworkEventUpdate are done, networkEventUpdate
+   * messages contain initial network info for each updateType and then we can invoke
+   * requestData to fetch its corresponded data lazily.
+   * Once all updateTypes of networkEventUpdate message are arrived, we flush merged
+   * request payload from pending queue and then update component.
    */
-  requestPayloadData(actor, method) {
-    let record = this.rdpRequestMap.get(actor);
+  async onPayloadDataReceived(actor) {
+    let payload = this.payloadQueue.get(actor) || {};
 
-    // If data has been already requested, do nothing.
-    if (record[method]) {
+    if (!payload.requestHeadersAvailable || !payload.requestCookiesAvailable ||
+        !payload.eventTimingsAvailable || !payload.responseContentAvailable) {
       return;
     }
 
-    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);
-    });
-  }
+    this.payloadQueue.delete(actor);
 
-  /**
-   * Executed when new data are received from the backend.
-   */
-  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;
+    if (this.actions.updateRequest) {
+      await this.actions.updateRequest(actor, payload, true);
+    }
 
-      // Clean up
-      this.cleanUpQueue(actor);
-      this.rdpRequestMap.delete(actor);
-
-      if (this.actions.updateRequest) {
-        await this.actions.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);
-    }
+    // 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.
    *
    * The method focus on:
    * - calling the right actor method,
    * - emitting an event to tell we start fetching some request data,
@@ -503,31 +423,30 @@ class FirefoxDataProvider {
     // 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 () => {
+    promise = this._requestData(actor, method).then(async (payload) => {
       // Remove the request from the cache, any new call to requestData will fetch the
       // data again.
-      this.lazyRequestData.delete(key, promise);
+      this.lazyRequestData.delete(key);
 
       if (this.actions.updateRequest) {
-        await this.actions.updateRequest(
-          actor,
-          this.getRequestFromQueue(actor).payload,
-          true,
-        );
+        await this.actions.updateRequest(actor, payload, true);
       }
+
+      return payload;
     });
+
+    this.lazyRequestData.set(key, promise);
+
     return promise;
   }
 
   /**
    * Internal helper used to request HTTP details from the backend.
    *
    * This is internal method that focus on:
    * - calling the right actor method,
@@ -552,17 +471,17 @@ class FirefoxDataProvider {
 
     let response = await new Promise((resolve, reject) => {
       // Do a RDP request to fetch data from the actor.
       if (typeof this.webConsoleClient[clientMethodName] === "function") {
         // Make sure we fetch the real actor data instead of cloned actor
         // e.g. CustomRequestPanel will clone a request with additional '-clone' actor id
         this.webConsoleClient[clientMethodName](actor.replace("-clone", ""), (res) => {
           if (res.error) {
-            console.error(res.message);
+            reject(new Error(res.message));
           }
           resolve(res);
         });
       } else {
         reject(new Error(`Error: No such client method '${clientMethodName}'!`));
       }
     });
 
@@ -576,22 +495,35 @@ class FirefoxDataProvider {
     return this[callbackMethodName](response);
   }
 
   /**
    * Handles additional information received for a "requestHeaders" packet.
    *
    * @param {object} response the message received from the server.
    */
-  onRequestHeaders(response) {
-    return this.updateRequest(response.from, {
+  async onRequestHeaders(response) {
+    let payload = await this.updateRequest(response.from, {
       requestHeaders: response
-    }).then(() => {
-      emit(EVENTS.RECEIVED_REQUEST_HEADERS, response.from);
     });
+    emit(EVENTS.RECEIVED_REQUEST_HEADERS, response.from);
+    return payload.requestHeaders;
+  }
+
+  /**
+   * Handles additional information received for a "responseHeaders" packet.
+   *
+   * @param {object} response the message received from the server.
+   */
+  async onResponseHeaders(response) {
+    let payload = await this.updateRequest(response.from, {
+      responseHeaders: response
+    });
+    emit(EVENTS.RECEIVED_RESPONSE_HEADERS, response.from);
+    return payload.responseHeaders;
   }
 
   /**
    * Handles additional information received for a "requestCookies" packet.
    *
    * @param {object} response the message received from the server.
    */
   async onRequestCookies(response) {
@@ -615,35 +547,22 @@ class FirefoxDataProvider {
     return payload;
   }
 
   /**
    * Handles additional information received for a "securityInfo" packet.
    *
    * @param {object} response the message received from the server.
    */
-  onSecurityInfo(response) {
-    return this.updateRequest(response.from, {
+  async onSecurityInfo(response) {
+    let payload = await this.updateRequest(response.from, {
       securityInfo: response.securityInfo
-    }).then(() => {
-      emit(EVENTS.RECEIVED_SECURITY_INFO, response.from);
     });
-  }
-
-  /**
-   * Handles additional information received for a "responseHeaders" packet.
-   *
-   * @param {object} response the message received from the server.
-   */
-  onResponseHeaders(response) {
-    return this.updateRequest(response.from, {
-      responseHeaders: response
-    }).then(() => {
-      emit(EVENTS.RECEIVED_RESPONSE_HEADERS, response.from);
-    });
+    emit(EVENTS.RECEIVED_SECURITY_INFO, response.from);
+    return payload.securityInfo;
   }
 
   /**
    * Handles additional information received for a "responseCookies" packet.
    *
    * @param {object} response the message received from the server.
    */
   async onResponseCookies(response) {
@@ -671,22 +590,22 @@ class FirefoxDataProvider {
     return payload.responseContent;
   }
 
   /**
    * Handles additional information received for a "eventTimings" packet.
    *
    * @param {object} response the message received from the server.
    */
-  onEventTimings(response) {
-    return this.updateRequest(response.from, {
+  async onEventTimings(response) {
+    let payload = await this.updateRequest(response.from, {
       eventTimings: response
-    }).then(() => {
-      emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
     });
+    emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
+    return payload.eventTimings;
   }
 
   /**
    * Handles information received for a "stackTrace" packet.
    *
    * @param {object} response the message received from the server.
    */
   async onStackTrace(response) {
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -68,18 +68,18 @@ const EVENTS = {
   UPDATING_REQUEST_COOKIES: "NetMonitor:NetworkEventUpdating:RequestCookies",
   RECEIVED_REQUEST_COOKIES: "NetMonitor:NetworkEventUpdated:RequestCookies",
 
   // When request post data begins and finishes receiving.
   UPDATING_REQUEST_POST_DATA: "NetMonitor:NetworkEventUpdating:RequestPostData",
   RECEIVED_REQUEST_POST_DATA: "NetMonitor:NetworkEventUpdated:RequestPostData",
 
   // When security information begins and finishes receiving.
-  UPDATING_SECURITY_INFO: "NetMonitor::NetworkEventUpdating:SecurityInfo",
-  RECEIVED_SECURITY_INFO: "NetMonitor::NetworkEventUpdated:SecurityInfo",
+  UPDATING_SECURITY_INFO: "NetMonitor:NetworkEventUpdating:SecurityInfo",
+  RECEIVED_SECURITY_INFO: "NetMonitor:NetworkEventUpdated:SecurityInfo",
 
   // When response headers begin and finish receiving.
   UPDATING_RESPONSE_HEADERS: "NetMonitor:NetworkEventUpdating:ResponseHeaders",
   RECEIVED_RESPONSE_HEADERS: "NetMonitor:NetworkEventUpdated:ResponseHeaders",
 
   // When response cookies begin and finish receiving.
   UPDATING_RESPONSE_COOKIES: "NetMonitor:NetworkEventUpdating:ResponseCookies",
   RECEIVED_RESPONSE_COOKIES: "NetMonitor:NetworkEventUpdated:ResponseCookies",
@@ -108,30 +108,33 @@ const UPDATE_PROPS = [
   "url",
   "remotePort",
   "remoteAddress",
   "status",
   "statusText",
   "httpVersion",
   "securityState",
   "securityInfo",
+  "securityInfoAvailable",
   "mimeType",
   "contentSize",
   "transferredSize",
   "totalTime",
   "eventTimings",
   "headersSize",
   "customQueryValue",
   "requestHeaders",
+  "requestHeadersAvailable",
   "requestHeadersFromUploadStream",
   "requestCookies",
   "requestCookiesAvailable",
   "requestPostData",
   "requestPostDataAvailable",
   "responseHeaders",
+  "responseHeadersAvailable",
   "responseCookies",
   "responseCookiesAvailable",
   "responseContent",
   "responseContentAvailable",
   "formDataSections",
   "stacktrace",
 ];
 
--- a/devtools/client/netmonitor/src/har/har-builder.js
+++ b/devtools/client/netmonitor/src/har/har-builder.js
@@ -148,28 +148,41 @@ HarBuilder.prototype = {
       onContentLoad: -1,
       onLoad: -1
     };
 
     return timings;
   },
 
   buildRequest: async function (file) {
+    // When using HarAutomation, HarCollector will automatically fetch requestHeaders
+    // and requestCookies, but when we use it from netmonitor, FirefoxDataProvider
+    // should fetch it itself lazily, via requestData.
+
+    let requestHeaders = file.requestHeaders;
+    if (!requestHeaders && this._options.requestData) {
+      requestHeaders = await this._options.requestData(file.id, "requestHeaders");
+    }
+
+    let requestCookies = file.requestCookies;
+    if (!requestCookies && this._options.requestData) {
+      requestCookies = await this._options.requestData(file.id, "requestCookies");
+    }
+
     let request = {
       bodySize: 0
     };
-
     request.method = file.method;
     request.url = file.url;
     request.httpVersion = file.httpVersion || "";
-    request.headers = this.buildHeaders(file.requestHeaders);
+    request.headers = this.buildHeaders(requestHeaders);
     request.headers = this.appendHeadersPostData(request.headers, file);
-    request.cookies = this.buildCookies(file.requestCookies);
+    request.cookies = this.buildCookies(requestCookies);
     request.queryString = parseQueryString(getUrlQuery(file.url)) || [];
-    request.headersSize = file.requestHeaders.headersSize;
+    request.headersSize = requestHeaders.headersSize;
     request.postData = await this.buildPostData(file);
 
     if (request.postData && request.postData.text) {
       request.bodySize = request.postData.text.length;
     }
 
     return request;
   },
@@ -229,33 +242,37 @@ HarBuilder.prototype = {
         });
       });
     });
 
     return result;
   },
 
   buildPostData: async function (file) {
-    // When using HarAutomation, HarCollector will automatically fetch requestPostData,
-    // but when we use it from netmonitor, FirefoxDataProvider should fetch it itself
-    // lazily, via requestData.
+    // When using HarAutomation, HarCollector will automatically fetch requestPostData
+    // and requestHeaders, but when we use it from netmonitor, FirefoxDataProvider
+    // should fetch it itself lazily, via requestData.
     let requestPostData = file.requestPostData;
     let requestHeaders = file.requestHeaders;
     let requestHeadersFromUploadStream;
 
     if (!requestPostData && this._options.requestData) {
       let payload = await this._options.requestData(file.id, "requestPostData");
       requestPostData = payload.requestPostData;
       requestHeadersFromUploadStream = payload.requestHeadersFromUploadStream;
     }
 
     if (!requestPostData.postData.text) {
       return undefined;
     }
 
+    if (!requestHeaders && this._options.requestData) {
+      requestHeaders = await this._options.requestData(file.id, "requestHeaders");
+    }
+
     let postData = {
       mimeType: findValue(requestHeaders.headers, "content-type"),
       params: [],
       text: requestPostData.postData.text,
     };
 
     if (requestPostData.postDataDiscarded) {
       postData.comment = L10N.getStr("har.requestBodyNotIncluded");
@@ -283,32 +300,43 @@ HarBuilder.prototype = {
         }
       });
     }
 
     return postData;
   },
 
   buildResponse: async function (file) {
+    // When using HarAutomation, HarCollector will automatically fetch responseHeaders
+    // and responseCookies, but when we use it from netmonitor, FirefoxDataProvider
+    // should fetch it itself lazily, via requestData.
+
+    let responseHeaders = file.responseHeaders;
+    if (!responseHeaders && this._options.requestData) {
+      responseHeaders = await this._options.requestData(file.id, "responseHeaders");
+    }
+
+    let responseCookies = file.responseCookies;
+    if (!responseCookies && this._options.requestData) {
+      responseCookies = await this._options.requestData(file.id, "responseCookies");
+    }
+
     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.cookies = this.buildCookies(responseCookies);
     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;
 
--- a/devtools/client/netmonitor/src/har/test/browser.ini
+++ b/devtools/client/netmonitor/src/har/test/browser.ini
@@ -2,15 +2,14 @@
 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_throttle_upload.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_throttle_upload.js
@@ -42,21 +42,23 @@ function* throttleUploadTest(actuallyThr
   info("sending throttle request");
   yield new Promise((resolve) => {
     connector.setPreferences(request, (response) => {
       resolve(response);
     });
   });
 
   // Execute one POST request on the page and wait till its done.
+  let onEventTimings = monitor.panelWin.once(EVENTS.RECEIVED_EVENT_TIMINGS);
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, { size }, function* (args) {
     content.wrappedJSObject.executeTest2(args.size);
   });
   yield wait;
+  yield onEventTimings;
 
   // Copy HAR into the clipboard (asynchronous).
   let contextMenu = new RequestListContextMenu({ connector });
   let jsonString = yield contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
   let har = JSON.parse(jsonString);
 
   // Check out the HAR log.
   isnot(har.log, null, "The HAR log must exist");
--- a/devtools/client/netmonitor/src/utils/filter-autocomplete-provider.js
+++ b/devtools/client/netmonitor/src/utils/filter-autocomplete-provider.js
@@ -51,18 +51,18 @@ function getAutocompleteValuesForFlag(fl
       values = responseCookies.map(c => c.hasOwnProperty("domain") ?
           c.domain : request.urlDetails.host);
       break;
     case "is":
       values = ["cached", "from-cache", "running"];
       break;
     case "has-response-header":
       // Some requests not having responseHeaders..?
-      values = request.responseHeaders &&
-        request.responseHeaders.headers.map(h => h.name);
+      values = request.responseHeaders ?
+        request.responseHeaders.headers.map(h => h.name) : [];
       break;
     case "protocol":
       values.push(request.httpVersion);
       break;
     case "method":
     default:
       values.push(request[flag]);
   }
@@ -103,20 +103,21 @@ function getLastTokenFlagValues(lastToke
 
   let values = [];
   for (let request of requests) {
     values.push(...getAutocompleteValuesForFlag(flag, request));
   }
   values = [...new Set(values)];
 
   return values
+    .filter(value => value)
     .filter(value => {
-      if (typedFlagValue) {
-        let lowerTyped = typedFlagValue.toLowerCase(),
-          lowerValue = value.toLowerCase();
+      if (typedFlagValue && value) {
+        let lowerTyped = typedFlagValue.toLowerCase();
+        let lowerValue = value.toLowerCase();
         return lowerValue.includes(lowerTyped) && lowerValue !== lowerTyped;
       }
       return typeof value !== "undefined" && value !== "" && value !== "undefined";
     })
     .sort()
     .map(value => isNegativeFlag ? `-${flag}:${value}` : `${flag}:${value}`);
 }
 
--- a/devtools/client/netmonitor/src/utils/request-utils.js
+++ b/devtools/client/netmonitor/src/utils/request-utils.js
@@ -415,52 +415,59 @@ function getResponseHeader(item, header)
     }
   }
   return null;
 }
 
 /**
  * Extracts any urlencoded form data sections from a POST request.
  */
-function updateFormDataSections(props) {
+async function updateFormDataSections(props) {
   let {
     connector,
     request = {},
     updateRequest,
   } = props;
   let {
+    id,
     formDataSections,
     requestHeaders,
+    requestHeadersAvailable,
     requestHeadersFromUploadStream,
     requestPostData,
+    requestPostDataAvailable,
   } = request;
 
-  if (!formDataSections && requestHeaders &&
-      requestHeadersFromUploadStream && requestPostData) {
-    getFormDataSections(
+  if (requestHeadersAvailable && !requestHeaders) {
+    requestHeaders = await connector.requestData(id, "requestHeaders");
+  }
+
+  if (requestPostDataAvailable && !requestPostData) {
+    requestPostData = await connector.requestData(id, "requestPostData");
+  }
+
+  if (!formDataSections && requestHeaders && requestPostData &&
+      requestHeadersFromUploadStream) {
+    formDataSections = await getFormDataSections(
       requestHeaders,
       requestHeadersFromUploadStream,
       requestPostData,
       connector.getLongString,
-    ).then((newFormDataSections) => {
-      updateRequest(
-        request.id,
-        { formDataSections: newFormDataSections },
-        true,
-      );
-    });
+    );
+
+    updateRequest(request.id, { formDataSections }, true);
   }
 }
 
 /**
  * This helper function is used for additional processing of
  * incoming network update packets. It's used by Network and
  * Console panel reducers.
  */
-function processNetworkUpdates(request) {
+function processNetworkUpdates(request = {}) {
   let result = {};
   for (let [key, value] of Object.entries(request)) {
     if (UPDATE_PROPS.includes(key)) {
       result[key] = value;
 
       switch (key) {
         case "securityInfo":
           result.securityState = value.state;
--- a/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
+++ b/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
@@ -29,19 +29,21 @@ class RequestListContextMenu {
     let {
       id,
       isCustom,
       formDataSections,
       method,
       mimeType,
       httpVersion,
       requestHeaders,
+      requestHeadersAvailable,
       requestPostData,
       requestPostDataAvailable,
       responseHeaders,
+      responseHeadersAvailable,
       responseContentAvailable,
       url,
     } = selectedRequest;
     let {
       cloneSelectedRequest,
       openStatistics,
     } = this.props;
     let menu = [];
@@ -62,53 +64,63 @@ class RequestListContextMenu {
       visible: !!(selectedRequest && getUrlQuery(url)),
       click: () => this.copyUrlParams(url),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-post-data",
       label: L10N.getStr("netmonitor.context.copyPostData"),
       accesskey: L10N.getStr("netmonitor.context.copyPostData.accesskey"),
+      // Menu item will be visible even if data hasn't arrived, so we need to check
+      // *Available property and then fetch data lazily once user triggers the action.
       visible: !!(selectedRequest && (requestPostDataAvailable || requestPostData)),
       click: () => this.copyPostData(id, formDataSections),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-as-curl",
       label: L10N.getStr("netmonitor.context.copyAsCurl"),
       accesskey: L10N.getStr("netmonitor.context.copyAsCurl.accesskey"),
-      visible: !!selectedRequest,
+      // Menu item will be visible even if data hasn't arrived, so we need to check
+      // *Available property and then fetch data lazily once user triggers the action.
+      visible: !!(selectedRequest && (requestHeadersAvailable || requestHeaders)),
       click: () => this.copyAsCurl(id, url, method, requestHeaders, httpVersion),
     });
 
     copySubmenu.push({
       type: "separator",
       visible: copySubmenu.slice(0, 4).some((subMenu) => subMenu.visible),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-request-headers",
       label: L10N.getStr("netmonitor.context.copyRequestHeaders"),
       accesskey: L10N.getStr("netmonitor.context.copyRequestHeaders.accesskey"),
-      visible: !!(selectedRequest && requestHeaders && requestHeaders.rawHeaders),
-      click: () => this.copyRequestHeaders(requestHeaders.rawHeaders.trim()),
+      // Menu item will be visible even if data hasn't arrived, so we need to check
+      // *Available property and then fetch data lazily once user triggers the action.
+      visible: !!(selectedRequest && (requestHeadersAvailable || requestHeaders)),
+      click: () => this.copyRequestHeaders(id, requestHeaders),
     });
 
     copySubmenu.push({
       id: "response-list-context-copy-response-headers",
       label: L10N.getStr("netmonitor.context.copyResponseHeaders"),
       accesskey: L10N.getStr("netmonitor.context.copyResponseHeaders.accesskey"),
-      visible: !!(selectedRequest && responseHeaders && responseHeaders.rawHeaders),
-      click: () => this.copyResponseHeaders(responseHeaders.rawHeaders.trim()),
+      // Menu item will be visible even if data hasn't arrived, so we need to check
+      // *Available property and then fetch data lazily once user triggers the action.
+      visible: !!(selectedRequest && (responseHeadersAvailable || responseHeaders)),
+      click: () => this.copyResponseHeaders(id, responseHeaders),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-response",
       label: L10N.getStr("netmonitor.context.copyResponse"),
       accesskey: L10N.getStr("netmonitor.context.copyResponse.accesskey"),
+      // Menu item will be visible even if data hasn't arrived, so we need to check
+      // *Available property and then fetch data lazily once user triggers the action.
       visible: !!(selectedRequest && responseContentAvailable),
       click: () => this.copyResponse(id),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-image-as-data-uri",
       label: L10N.getStr("netmonitor.context.copyImageAsDataUri"),
       accesskey: L10N.getStr("netmonitor.context.copyImageAsDataUri.accesskey"),
@@ -275,43 +287,56 @@ class RequestListContextMenu {
     }
     copyString(string);
   }
 
   /**
    * Copy a cURL command from the currently selected item.
    */
   async copyAsCurl(id, url, method, requestHeaders, httpVersion) {
+    if (!requestHeaders) {
+      requestHeaders = await this.props.connector.requestData(id, "requestHeaders");
+    }
     let { requestPostData } = await this.props.connector
       .requestData(id, "requestPostData");
     // Create a sanitized object for the Curl command generator.
     let data = {
       url,
       method,
       headers: requestHeaders.headers,
       httpVersion: httpVersion,
       postDataText: requestPostData ? requestPostData.postData.text : "",
     };
     copyString(Curl.generateCommand(data));
   }
 
   /**
    * Copy the raw request headers from the currently selected item.
    */
-  copyRequestHeaders(rawHeaders) {
+  async copyRequestHeaders(id, requestHeaders) {
+    if (!requestHeaders) {
+      requestHeaders = await this.props.connector.requestData(id, "requestHeaders");
+    }
+    let rawHeaders = requestHeaders.rawHeaders.trim();
+
     if (Services.appinfo.OS !== "WINNT") {
       rawHeaders = rawHeaders.replace(/\r/g, "");
     }
     copyString(rawHeaders);
   }
 
   /**
    * Copy the raw response headers from the currently selected item.
    */
-  copyResponseHeaders(rawHeaders) {
+  async copyResponseHeaders(id, responseHeaders) {
+    if (!responseHeaders) {
+      responseHeaders = await this.props.connector.requestData(id, "responseHeaders");
+    }
+    let rawHeaders = responseHeaders.rawHeaders.trim();
+
     if (Services.appinfo.OS !== "WINNT") {
       rawHeaders = rawHeaders.replace(/\r/g, "");
     }
     copyString(rawHeaders);
   }
 
   /**
    * Copy image as data uri.
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -1,15 +1,14 @@
 [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_cause_redirect.js
+++ b/devtools/client/netmonitor/test/browser_net_cause_redirect.js
@@ -28,19 +28,21 @@ add_task(function* () {
   store.dispatch(Actions.batchEnable(false));
 
   let wait = waitForNetworkEvents(monitor, EXPECTED_REQUESTS.length);
   yield performRequests(2, HSTS_SJS);
   yield wait;
 
   // Fetch stack-trace data from the backend and wait till
   // all packets are received.
-  let requests = getSortedRequests(store.getState());
-  yield Promise.all(requests.map(requestItem =>
-    connector.requestData(requestItem.id, "stackTrace")));
+  let requests = getSortedRequests(store.getState())
+    .filter((req) => !req.stacktrace)
+    .map((req) => connector.requestData(req.id, "stackTrace"));
+
+  yield Promise.all(requests);
 
   EXPECTED_REQUESTS.forEach(({status, hasStack}, i) => {
     let item = getSortedRequests(store.getState()).get(i);
 
     is(item.status, status, `Request #${i} has the expected status`);
 
     let { stacktrace } = item;
     let stackLen = stacktrace ? stacktrace.length : 0;
--- a/devtools/client/netmonitor/test/browser_net_copy_headers.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_headers.js
@@ -27,18 +27,16 @@ add_task(function* () {
   let requestItem = getSortedRequests(store.getState()).get(0);
   let { method, httpVersion, status, statusText } = requestItem;
 
   EventUtils.sendMouseEvent({ type: "contextmenu" },
     document.querySelectorAll(".request-list-item")[0]);
 
   let selectedRequest = getSelectedRequest(store.getState());
   is(selectedRequest, requestItem, "Proper request is selected");
-  ok(selectedRequest.requestHeaders, "Selected request should have request headers");
-  ok(selectedRequest.responseHeaders, "Selected request should have response headers");
 
   const EXPECTED_REQUEST_HEADERS = [
     `${method} ${SIMPLE_URL} ${httpVersion}`,
     "Host: example.com",
     "User-Agent: " + navigator.userAgent + "",
     "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
     "Accept-Language: " + navigator.languages.join(",") + ";q=0.5",
     "Accept-Encoding: gzip, deflate",
--- a/devtools/client/netmonitor/test/browser_net_curl-utils.js
+++ b/devtools/client/netmonitor/test/browser_net_curl-utils.js
@@ -230,34 +230,35 @@ function testEscapeStringWin() {
 
   let newLines = "line1\r\nline2\r\nline3";
   is(CurlUtils.escapeStringWin(newLines),
     '"line1"^\u000d\u000A"line2"^\u000d\u000A"line3"',
     "Newlines should be escaped.");
 }
 
 function* createCurlData(selected, getLongString, requestData) {
-  let { url, method, httpVersion } = selected;
+  let { id, url, method, httpVersion } = selected;
 
   // Create a sanitized object for the Curl command generator.
   let data = {
     url,
     method,
     headers: [],
     httpVersion,
     postDataText: null
   };
 
+  let requestHeaders = yield requestData(id, "requestHeaders");
   // Fetch header values.
-  for (let { name, value } of selected.requestHeaders.headers) {
+  for (let { name, value } of requestHeaders.headers) {
     let text = yield getLongString(value);
     data.headers.push({ name: name, value: text });
   }
 
-  let { requestPostData } = yield requestData(selected.id, "requestPostData");
+  let { requestPostData } = yield requestData(id, "requestPostData");
   // Fetch the request payload.
   if (requestPostData) {
     let postData = requestPostData.postData.text;
     data.postDataText = yield getLongString(postData);
   }
 
   return data;
 }
--- a/devtools/client/netmonitor/test/browser_net_filter-01.js
+++ b/devtools/client/netmonitor/test/browser_net_filter-01.js
@@ -312,18 +312,25 @@ add_task(function* () {
       yield waitUntil(() => requestsListStatus.title);
     }
 
     isnot(getSelectedRequest(store.getState()), undefined,
       "There should still be a selected item after filtering.");
     is(getSelectedIndex(store.getState()), 0,
       "The first item should be still selected after filtering.");
 
-    const items = getSortedRequests(store.getState());
-    const visibleItems = getDisplayedRequests(store.getState());
+    let items = getSortedRequests(store.getState());
+    let visibleItems;
+
+    // Filter results will be updated asynchronously, so we should wait until
+    // displayed requests reach final state.
+    yield waitUntil(() => {
+      visibleItems = getDisplayedRequests(store.getState());
+      return visibleItems.size === visibility.filter(e => e).length;
+    });
 
     is(items.size, visibility.length,
        "There should be a specific amount of items in the requests menu.");
     is(visibleItems.size, visibility.filter(e => e).length,
        "There should be a specific amount of visible items in the requests menu.");
 
     for (let i = 0; i < visibility.length; i++) {
       let itemId = items.get(i).id;
--- a/devtools/client/netmonitor/test/browser_net_filter-flags.js
+++ b/devtools/client/netmonitor/test/browser_net_filter-flags.js
@@ -140,18 +140,30 @@ add_task(function* () {
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let {
     getDisplayedRequests,
     getSortedRequests,
   } = windowRequire("devtools/client/netmonitor/src/selectors/index");
 
   store.dispatch(Actions.batchEnable(false));
 
+  function type(string) {
+    for (let ch of string) {
+      EventUtils.synthesizeKey(ch, {}, monitor.panelWin);
+    }
+  }
+
+  // Filtering network request will start fetching data lazily
+  // (fetching requestHeaders & responseHeaders for filtering WS & XHR)
+  // Lazy fetching will be executed when user focuses on filter box.
   function setFreetextFilter(value) {
-    store.dispatch(Actions.setRequestFilterText(value));
+    let filterBox = document.querySelector(".devtools-filterinput");
+    filterBox.focus();
+    filterBox.value = "";
+    type(value);
   }
 
   info("Starting test... ");
 
   let waitNetwork = waitForNetworkEvents(monitor, REQUESTS.length);
   loadCommonFrameScript();
   yield performRequestsInContent(REQUESTS);
   yield waitNetwork;
@@ -345,28 +357,44 @@ add_task(function* () {
     let requestItems = document.querySelectorAll(".request-list-item");
     for (let requestItem of requestItems) {
       requestItem.scrollIntoView();
       let requestsListStatus = requestItem.querySelector(".requests-list-status");
       EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
       yield waitUntil(() => requestsListStatus.title);
     }
 
-    const items = getSortedRequests(store.getState());
-    const visibleItems = getDisplayedRequests(store.getState());
+    let items = getSortedRequests(store.getState());
+    let visibleItems = getDisplayedRequests(store.getState());
+
+    // Filter results will be updated asynchronously, so we should wait until
+    // displayed requests reach final state.
+    yield waitUntil(() => {
+      visibleItems = getDisplayedRequests(store.getState());
+      return visibleItems.size === visibility.filter(e => e).length;
+    });
 
     is(items.size, visibility.length,
       "There should be a specific amount of items in the requests menu.");
     is(visibleItems.size, visibility.filter(e => e).length,
       "There should be a specific amount of visible items in the requests menu.");
 
     for (let i = 0; i < visibility.length; i++) {
       let itemId = items.get(i).id;
       let shouldBeVisible = !!visibility[i];
       let isThere = visibleItems.some(r => r.id == itemId);
+
+      // Filter results will be updated asynchronously, so we should wait until
+      // displayed requests reach final state.
+      yield waitUntil(() => {
+        visibleItems = getDisplayedRequests(store.getState());
+        isThere = visibleItems.some(r => r.id == itemId);
+        return isThere === shouldBeVisible;
+      });
+
       is(isThere, shouldBeVisible,
         `The item at index ${i} has visibility=${shouldBeVisible}`);
 
       if (shouldBeVisible) {
         let { method, url, data } = EXPECTED_REQUESTS[i];
         verifyRequestItemTarget(
           document,
           getDisplayedRequests(store.getState()),
--- a/devtools/client/netmonitor/test/browser_net_headers_sorted.js
+++ b/devtools/client/netmonitor/test/browser_net_headers_sorted.js
@@ -7,29 +7,36 @@
  * Tests if Request-Headers and Response-Headers are sorted in Headers tab.
  */
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(SIMPLE_SJS);
   info("Starting test... ");
 
   let { document, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
+  let {
+    getSortedRequests,
+  } = windowRequire("devtools/client/netmonitor/src/selectors/index");
 
   store.dispatch(Actions.batchEnable(false));
 
+  let wait = waitForNetworkEvents(monitor, 1);
   tab.linkedBrowser.reload();
-
-  let wait = waitForNetworkEvents(monitor, 1);
   yield wait;
 
   wait = waitForDOM(document, ".headers-overview");
   EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelectorAll(".request-list-item")[0]);
   yield wait;
 
+  yield waitUntil(() => {
+    let request = getSortedRequests(store.getState()).get(0);
+    return request.requestHeaders && request.responseHeaders;
+  });
+
   info("Check if Request-Headers and Response-Headers are sorted");
   let expectedResponseHeaders = ["cache-control", "connection", "content-length",
                                  "content-type", "date", "expires", "foo-bar",
                                  "pragma", "server", "set-cookie"];
   let expectedRequestHeaders = ["Accept", "Accept-Encoding", "Accept-Language",
                                 "Cache-Control", "Connection", "Cookie", "Host",
                                 "Pragma", "Upgrade-Insecure-Requests", "User-Agent"];
 
--- a/devtools/client/netmonitor/test/browser_net_persistent_logs.js
+++ b/devtools/client/netmonitor/test/browser_net_persistent_logs.js
@@ -7,36 +7,46 @@
  * Tests if the network monitor leaks on initialization and sudden destruction.
  * You can also use this initialization format as a template for other tests.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(SINGLE_GET_URL);
   info("Starting test... ");
 
-  let { document } = monitor.panelWin;
+  let { document, store, windowRequire } = monitor.panelWin;
+  let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
+
+  store.dispatch(Actions.batchEnable(false));
 
   Services.prefs.setBoolPref("devtools.netmonitor.persistlog", false);
 
   yield reloadAndWait();
 
+  // Using waitUntil in the test is necessary to ensure all requests are added correctly.
+  // Because reloadAndWait call may catch early uncaught requests from initNetMonitor, so
+  // the actual number of requests after reloadAndWait could be wrong since all requests
+  // haven't finished.
+  yield waitUntil(() => document.querySelectorAll(".request-list-item").length === 2);
   is(document.querySelectorAll(".request-list-item").length, 2,
     "The request list should have two items at this point.");
 
   yield reloadAndWait();
 
+  yield waitUntil(() => document.querySelectorAll(".request-list-item").length === 2);
   // Since the reload clears the log, we still expect two requests in the log
   is(document.querySelectorAll(".request-list-item").length, 2,
     "The request list should still have two items at this point.");
 
   // Now we toggle the persistence logs on
   Services.prefs.setBoolPref("devtools.netmonitor.persistlog", true);
 
   yield reloadAndWait();
 
+  yield waitUntil(() => document.querySelectorAll(".request-list-item").length === 4);
   // Since we togged the persistence logs, we expect four items after the reload
   is(document.querySelectorAll(".request-list-item").length, 4,
     "The request list should now have four items at this point.");
 
   Services.prefs.setBoolPref("devtools.netmonitor.persistlog", false);
   return teardown(monitor);
 
   /**
--- a/devtools/client/netmonitor/test/browser_net_raw_headers.js
+++ b/devtools/client/netmonitor/test/browser_net_raw_headers.js
@@ -20,17 +20,17 @@ add_task(function* () {
   store.dispatch(Actions.batchEnable(false));
 
   let wait = waitForNetworkEvents(monitor, 2);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  wait = waitForDOM(document, ".headers-overview");
+  wait = waitForDOM(document, "#headers-panel .tree-section", 2);
   EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelectorAll(".request-list-item")[0]);
   yield wait;
 
   wait = waitForDOM(document, ".raw-headers-container textarea", 2);
   EventUtils.sendMouseEvent({ type: "click" }, getRawHeadersButton());
   yield wait;
 
--- a/devtools/client/netmonitor/test/browser_net_resend.js
+++ b/devtools/client/netmonitor/test/browser_net_resend.js
@@ -12,17 +12,16 @@ const ADD_HEADER = "Test-header: true";
 const ADD_UA_HEADER = "User-Agent: Custom-Agent";
 const ADD_POSTDATA = "&t3=t4";
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(POST_DATA_URL);
   info("Starting test... ");
 
   let { document, store, windowRequire, connector } = monitor.panelWin;
-  let { requestData } = connector;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let {
     getSelectedRequest,
     getSortedRequests,
   } = windowRequire("devtools/client/netmonitor/src/selectors/index");
 
   store.dispatch(Actions.batchEnable(false));
 
@@ -51,17 +50,25 @@ add_task(function* () {
   customItem = getSelectedRequest(store.getState());
   testCustomItemChanged(customItem, origItem);
 
   // send the new request
   wait = waitForNetworkEvents(monitor, 1);
   store.dispatch(Actions.sendCustomRequest(connector));
   yield wait;
 
-  let sentItem = getSelectedRequest(store.getState());
+  let sentItem;
+  // Testing sent request will require updated requestHeaders and requestPostData,
+  // we must wait for both properties get updated before starting test.
+  yield waitUntil(() => {
+    sentItem = getSelectedRequest(store.getState());
+    origItem = getSortedRequests(store.getState()).get(0);
+    return sentItem.requestHeaders && sentItem.requestPostData &&
+      origItem.requestHeaders && origItem.requestPostData;
+  });
 
   yield testSentRequest(sentItem, origItem);
 
   // Ensure the UI shows the new request, selected, and that the detail panel was closed.
   is(getSortedRequests(store.getState()).length, 3, "There are 3 requests shown");
   is(document.querySelector(".request-list-item.selected").getAttribute("data-id"),
     sentItem.id, "The sent request is selected");
   is(document.querySelector(".network-details-panel"), null,
@@ -159,23 +166,18 @@ add_task(function* () {
 
     let { headers } = data.requestHeaders;
     let hasHeader = headers.some(h => `${h.name}: ${h.value}` == ADD_HEADER);
     ok(hasHeader, "new header added to sent request");
 
     let hasUAHeader = headers.some(h => `${h.name}: ${h.value}` == ADD_UA_HEADER);
     ok(hasUAHeader, "User-Agent header added to sent request");
 
-    let { requestPostData: clonedRequestPostData } = yield requestData(data.id,
-      "requestPostData");
-    let { requestPostData: origRequestPostData } = yield requestData(origData.id,
-      "requestPostData");
-
-    is(clonedRequestPostData.postData.text,
-      origRequestPostData.postData.text + ADD_POSTDATA,
+    is(data.requestPostData.postData.text,
+      origData.requestPostData.postData.text + ADD_POSTDATA,
       "post data added to sent request");
   }
 
   function type(string) {
     for (let ch of string) {
       EventUtils.synthesizeKey(ch, {}, monitor.panelWin);
     }
   }
--- a/devtools/client/netmonitor/test/browser_net_resend_cors.js
+++ b/devtools/client/netmonitor/test/browser_net_resend_cors.js
@@ -10,16 +10,17 @@
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CORS_URL);
   info("Starting test... ");
 
   let { store, windowRequire, connector } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let {
+    getRequestById,
     getSortedRequests,
   } = windowRequire("devtools/client/netmonitor/src/selectors/index");
 
   store.dispatch(Actions.batchEnable(false));
 
   let requestUrl = "http://test1.example.com" + CORS_SJS_PATH;
 
   info("Waiting for OPTIONS, then POST");
@@ -36,45 +37,68 @@ add_task(function* () {
   ITEMS.forEach((item, 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`);
   });
 
   // Resend both requests without modification. Wait for resent OPTIONS, then POST.
   // POST is supposed to have no preflight OPTIONS request this time (CORS is disabled)
   let onRequests = waitForNetworkEvents(monitor, 1);
-  ITEMS.forEach((item) => {
+  for (let item of ITEMS) {
     info(`Selecting the ${item.method} request`);
     store.dispatch(Actions.selectRequest(item.id));
 
+    // Wait for requestHeaders and responseHeaders are required when fetching data
+    // from back-end.
+    yield waitUntil(() => {
+      item = getRequestById(store.getState(), item.id);
+      return item.requestHeaders && item.responseHeaders;
+    });
+
+    let { size } = getSortedRequests(store.getState());
+
     info("Cloning the selected request into a custom clone");
     store.dispatch(Actions.cloneSelectedRequest());
 
     info("Sending the cloned request (without change)");
     store.dispatch(Actions.sendCustomRequest(connector));
-  });
+
+    yield waitUntil(() => getSortedRequests(store.getState()).size === size + 1);
+  }
 
   info("Waiting for both resent requests");
   yield onRequests;
 
   // Check the resent requests
   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 lazy load data
-      let responseContent = yield connector.requestData(item.id, "responseContent");
-      let { requestPostData } = yield connector.requestData(item.id, "requestPostData");
+      is(item.method, "POST", `The ${item.method} request has the right method`);
 
-      is(requestPostData.postData.text, "post-data",
+      // Trigger responseContent update requires to wait until
+      // responseContentAvailable set true
+      yield waitUntil(() => {
+        item = getRequestById(store.getState(), item.id);
+        return item.responseContentAvailable;
+      });
+      yield connector.requestData(item.id, "responseContent");
+
+      // Wait for both requestPostData & responseContent payloads arrived.
+      yield waitUntil(() => {
+        item = getRequestById(store.getState(), item.id);
+        return item.responseContent && item.requestPostData;
+      });
+
+      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(responseContent.content.text, "Access-Control-Allow-Origin: *",
+      is(item.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_resend_headers.js
+++ b/devtools/client/netmonitor/test/browser_net_resend_headers.js
@@ -8,17 +8,17 @@
  */
 
 add_task(function* () {
   let { monitor } = yield initNetMonitor(SIMPLE_SJS);
   info("Starting test... ");
 
   let { store, windowRequire, connector } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
-  let { sendHTTPRequest } = connector;
+  let { requestData, sendHTTPRequest } = connector;
   let {
     getSortedRequests,
   } = windowRequire("devtools/client/netmonitor/src/selectors/index");
 
   store.dispatch(Actions.batchEnable(false));
 
   let requestUrl = SIMPLE_SJS;
   let requestHeaders = [
@@ -35,16 +35,29 @@ add_task(function* () {
     url: requestUrl,
     method: "POST",
     headers: requestHeaders,
     body: "Hello"
   });
   yield wait;
 
   let item = getSortedRequests(store.getState()).get(0);
+
+  ok(item.requestHeadersAvailable, "headers are available for lazily fetching");
+
+  if (item.requestHeadersAvailable && !item.requestHeaders) {
+    requestData(item.id, "requestHeaders");
+  }
+
+  // Wait until requestHeaders packet gets updated.
+  yield waitUntil(() => {
+    item = getSortedRequests(store.getState()).get(0);
+    return item.requestHeaders;
+  });
+
   is(item.method, "POST", "The request has the right method");
   is(item.url, requestUrl, "The request has the right URL");
 
   for (let { name, value } of item.requestHeaders.headers) {
     info(`Request header: ${name}: ${value}`);
   }
 
   function hasRequestHeader(name, value) {
--- a/devtools/client/netmonitor/test/browser_net_security-error.js
+++ b/devtools/client/netmonitor/test/browser_net_security-error.js
@@ -6,23 +6,22 @@
 /**
  * Test that Security details tab shows an error message with broken connections.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
   let { document, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
-  let { EVENTS } = windowRequire("devtools/client/netmonitor/src/constants");
 
   store.dispatch(Actions.batchEnable(false));
 
   info("Requesting a resource that has a certificate problem.");
 
-  let requestsDone = waitForSecurityBrokenNetworkEvent();
+  let requestsDone = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests(1, "https://nocert.example.com");
   });
   yield requestsDone;
 
   let securityInfoLoaded = waitForDOM(document, ".security-info-value");
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector(".network-details-panel-toggle"));
@@ -31,30 +30,9 @@ add_task(function* () {
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector("#security-tab"));
   yield securityInfoLoaded;
 
   let errormsg = document.querySelector(".security-info-value");
   isnot(errormsg.textContent, "", "Error message is not empty.");
 
   return teardown(monitor);
-
-  /**
-   * Returns a promise that's resolved once a request with security issues is
-   * completed.
-   */
-  function waitForSecurityBrokenNetworkEvent() {
-    let awaitedEvents = [
-      "UPDATING_REQUEST_HEADERS",
-      "RECEIVED_REQUEST_HEADERS",
-      "UPDATING_REQUEST_COOKIES",
-      "RECEIVED_REQUEST_COOKIES",
-      "UPDATING_EVENT_TIMINGS",
-      "RECEIVED_EVENT_TIMINGS",
-    ];
-
-    let promises = awaitedEvents.map((event) => {
-      return monitor.panelWin.once(EVENTS[event]);
-    });
-
-    return Promise.all(promises);
-  }
 });
--- a/devtools/client/netmonitor/test/browser_net_security-state.js
+++ b/devtools/client/netmonitor/test/browser_net_security-state.js
@@ -51,20 +51,17 @@ add_task(function* () {
    */
   function* performRequests() {
     function executeRequests(count, url) {
       return ContentTask.spawn(tab.linkedBrowser, {count, url}, function* (args) {
         content.wrappedJSObject.performRequests(args.count, args.url);
       });
     }
 
-    // waitForNetworkEvents does not work for requests with security errors as
-    // those only emit 9/13 events of a successful request.
-    let done = waitForSecurityBrokenNetworkEvent();
-
+    let done = waitForNetworkEvents(monitor, 1);
     info("Requesting a resource that has a certificate problem.");
     yield executeRequests(1, "https://nocert.example.com");
 
     // Wait for the request to complete before firing another request. Otherwise
     // the request with security issues interfere with waitForNetworkEvents.
     info("Waiting for request to complete.");
     yield done;
 
@@ -75,40 +72,19 @@ 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();
+    done = waitForNetworkEvents(monitor, 1);
     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() {
-    let awaitedEvents = [
-      "UPDATING_REQUEST_HEADERS",
-      "RECEIVED_REQUEST_HEADERS",
-      "UPDATING_REQUEST_COOKIES",
-      "RECEIVED_REQUEST_COOKIES",
-      "UPDATING_EVENT_TIMINGS",
-      "RECEIVED_EVENT_TIMINGS",
-    ];
-
-    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
@@ -96,20 +96,16 @@ add_task(function* () {
   return teardown(monitor);
 
   /**
    * Returns a promise that's resolved once a request with security issues is
    * completed.
    */
   function waitForSecurityBrokenNetworkEvent() {
     let awaitedEvents = [
-      "UPDATING_REQUEST_HEADERS",
-      "RECEIVED_REQUEST_HEADERS",
-      "UPDATING_REQUEST_COOKIES",
-      "RECEIVED_REQUEST_COOKIES",
       "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
@@ -22,17 +22,17 @@ function test() {
   // number of response headers will vary depending on the platform.
   Services.prefs.setBoolPref("network.tcp.tcp_fastopen_enable", false);
 
   let { L10N } = require("devtools/client/netmonitor/src/utils/l10n");
 
   initNetMonitor(SIMPLE_SJS).then(async ({ tab, monitor }) => {
     info("Starting test... ");
 
-    let { document, store, windowRequire } = monitor.panelWin;
+    let { document, store, windowRequire, connector } = monitor.panelWin;
     let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
     let { EVENTS } = windowRequire("devtools/client/netmonitor/src/constants");
     let {
       getDisplayedRequests,
       getSelectedRequest,
       getSortedRequests,
     } = windowRequire("devtools/client/netmonitor/src/selectors/index");
 
@@ -342,15 +342,26 @@ function test() {
         "GET",
         SIMPLE_SJS,
         {
           time: true
         }
       );
     });
 
+    let wait = waitForNetworkEvents(monitor, 1);
     tab.linkedBrowser.reload();
+    await wait;
+
+    let requestItem = getSortedRequests(store.getState()).get(0);
+
+    if (!requestItem.requestHeaders) {
+      connector.requestData(requestItem.id, "requestHeaders");
+    }
+    if (!requestItem.responseHeaders) {
+      connector.requestData(requestItem.id, "responseHeaders");
+    }
 
     await Promise.all(promiseList);
     await teardown(monitor);
     finish();
   });
 }
--- a/devtools/client/netmonitor/test/browser_net_status-codes.js
+++ b/devtools/client/netmonitor/test/browser_net_status-codes.js
@@ -167,16 +167,19 @@ add_task(function* () {
 
   /**
    * A function that tests "Headers" panel contains correct information.
    */
   function* testHeaders(data, index) {
     EventUtils.sendMouseEvent({ type: "mousedown" },
       document.querySelectorAll(".request-list-item")[index]);
 
+    yield waitUntil(() => document.querySelector(
+      "#headers-panel .tabpanel-summary-value.textbox-input"));
+
     let panel = document.querySelector("#headers-panel");
     let summaryValues = panel.querySelectorAll(".tabpanel-summary-value.textbox-input");
     let { method, correctUri, details: { status, statusText } } = data;
 
     is(summaryValues[0].value, correctUri,
       "The url summary value is incorrect.");
     is(summaryValues[1].value, method, "The method summary value is incorrect.");
     is(panel.querySelector(".requests-list-status-icon").dataset.code, status,
--- a/devtools/client/netmonitor/test/browser_net_timing-division.js
+++ b/devtools/client/netmonitor/test/browser_net_timing-division.js
@@ -8,20 +8,16 @@
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
   info("Starting test... ");
 
   let { document, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
-  let {
-    getSortedRequests,
-  } = windowRequire("devtools/client/netmonitor/src/selectors/index");
-
   store.dispatch(Actions.batchEnable(false));
 
   let wait = waitForNetworkEvents(monitor, 2);
   // Timeout needed for having enough divisions on the time scale.
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests(2, null, 3000);
   });
   yield wait;
@@ -39,23 +35,15 @@ add_task(function* () {
 
   milDivs.forEach(div => info(`Millisecond division: ${div.textContent}`));
   secDivs.forEach(div => info(`Second division: ${div.textContent}`));
   minDivs.forEach(div => info(`Minute division: ${div.textContent}`));
 
   is(store.getState().requests.requests.size, 2,
      "There should be only two requests made.");
 
-  let firstRequest = getSortedRequests(store.getState()).get(0);
-  let lastRequest = getSortedRequests(store.getState()).get(1);
-
-  info("First request happened at: " +
-       firstRequest.responseHeaders.headers.find(e => e.name == "date").value);
-  info("Last request happened at: " +
-       lastRequest.responseHeaders.headers.find(e => e.name == "date").value);
-
   ok(secDivs.length,
      "There should be at least one division on the seconds time scale.");
   ok(secDivs[0].textContent.match(/\d+\.\d{2}\s\w+/),
      "The division on the seconds time scale looks legit.");
 
   return teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_waterfall-click.js
+++ b/devtools/client/netmonitor/test/browser_net_waterfall-click.js
@@ -6,31 +6,27 @@
 /**
  * Test that clicking on the waterfall opens the timing sidebar panel.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
   let { document } = monitor.panelWin;
 
-  yield performRequestsAndWait();
-
-  let wait = waitForDOM(document, "#timings-panel");
-  let timing = document.querySelectorAll(".requests-list-timings")[0];
+  let onAllEvents = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
+  yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+    content.wrappedJSObject.performRequests();
+  });
+  yield onAllEvents;
 
   info("Clicking waterfall and waiting for panel update.");
-  EventUtils.synthesizeMouseAtCenter(timing, {}, monitor.panelWin);
+  let wait = waitForDOM(document, "#timings-panel");
+
+  EventUtils.sendMouseEvent({ type: "mousedown" },
+    document.querySelectorAll(".requests-list-timings")[0]);
 
   yield wait;
 
   ok(document.querySelector("#timings-tab[aria-selected=true]"),
      "Timings tab is selected.");
 
   return teardown(monitor);
-
-  function* performRequestsAndWait() {
-    let onAllEvents = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
-    yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
-      content.wrappedJSObject.performRequests();
-    });
-    yield onAllEvents;
-  }
 });
--- a/devtools/client/netmonitor/test/head.js
+++ b/devtools/client/netmonitor/test/head.js
@@ -1,40 +1,36 @@
 /* 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, 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);
 
-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,
   getUrlHost,
   getUrlQuery,
   getUrlScheme,
 } = require("devtools/client/netmonitor/src/utils/request-utils");
+const { EVENTS } = require("devtools/client/netmonitor/src/constants");
 
 /* eslint-disable no-unused-vars, max-len */
 const EXAMPLE_URL = "http://example.com/browser/devtools/client/netmonitor/test/";
 const HTTPS_EXAMPLE_URL = "https://example.com/browser/devtools/client/netmonitor/test/";
 
 const API_CALLS_URL = EXAMPLE_URL + "html_api-calls-test-page.html";
 const SIMPLE_URL = EXAMPLE_URL + "html_simple-test-page.html";
 const NAVIGATE_URL = EXAMPLE_URL + "html_navigate-test-page.html";
@@ -220,33 +216,45 @@ function waitForAllRequestsFinished(moni
     window.on(EVENTS.PAYLOAD_READY, onTimings);
   });
 }
 
 let finishedQueue = {};
 let updatingTypes = [
   "NetMonitor:NetworkEventUpdating:RequestCookies",
   "NetMonitor:NetworkEventUpdating:ResponseCookies",
+  "NetMonitor:NetworkEventUpdating:RequestHeaders",
+  "NetMonitor:NetworkEventUpdating:ResponseHeaders",
+  "NetMonitor:NetworkEventUpdating:RequestPostData",
+  "NetMonitor:NetworkEventUpdating:ResponseContent",
+  "NetMonitor:NetworkEventUpdating:SecurityInfo",
+  "NetMonitor:NetworkEventUpdating:EventTimings",
 ];
 let updatedTypes = [
   "NetMonitor:NetworkEventUpdated:RequestCookies",
   "NetMonitor:NetworkEventUpdated:ResponseCookies",
+  "NetMonitor:NetworkEventUpdated:RequestHeaders",
+  "NetMonitor:NetworkEventUpdated:ResponseHeaders",
+  "NetMonitor:NetworkEventUpdated:RequestPostData",
+  "NetMonitor:NetworkEventUpdated:ResponseContent",
+  "NetMonitor:NetworkEventUpdated:SecurityInfo",
+  "NetMonitor:NetworkEventUpdated:EventTimings",
 ];
 
 // Start collecting all networkEventUpdate event when panel is opened.
 // removeTab() should be called once all corresponded RECEIVED_* events finished.
 function startNetworkEventUpdateObserver(panelWin) {
   updatingTypes.forEach((type) => panelWin.on(type, (event, actor) => {
-    let key = actor + "-" + event.replace("NetMonitor:NetworkEventUpdating:", "");
+    let key = actor + "-" + updatedTypes[updatingTypes.indexOf(event)];
     finishedQueue[key] = finishedQueue[key] ? finishedQueue[key] + 1 : 1;
   }));
 
   updatedTypes.forEach((type) => panelWin.on(type, (event, actor) => {
-    let key = actor + "-" + event.replace("NetMonitor:NetworkEventUpdated:", "");
-    finishedQueue[key]--;
+    let key = actor + "-" + event;
+    finishedQueue[key] = finishedQueue[key] ? finishedQueue[key] - 1 : -1;
   }));
 }
 
 function* waitForAllNetworkUpdateEvents() {
   function checkNetworkEventUpdateState() {
     for (let key in finishedQueue) {
       if (finishedQueue[key] > 0) {
         return false;
@@ -324,113 +332,74 @@ 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);
     yield waitForAllNetworkUpdateEvents();
     info("All pending requests finished.");
 
     let onDestroyed = monitor.once("destroyed");
     yield removeTab(tab);
     yield onDestroyed;
   });
 }
 
 function waitForNetworkEvents(monitor, getRequests) {
   return new Promise((resolve) => {
     let panel = monitor.panelWin;
     let { getNetworkRequest } = panel.connector;
-    let progress = {};
-    let genericEvents = 0;
+    let networkEvent = 0;
     let payloadReady = 0;
-    let awaitedEventsToListeners = [
-      ["UPDATING_REQUEST_HEADERS", onGenericEvent],
-      ["RECEIVED_REQUEST_HEADERS", onGenericEvent],
-      ["UPDATING_RESPONSE_HEADERS", onGenericEvent],
-      ["RECEIVED_RESPONSE_HEADERS", onGenericEvent],
-      ["UPDATING_EVENT_TIMINGS", onGenericEvent],
-      ["RECEIVED_EVENT_TIMINGS", onGenericEvent],
-      ["PAYLOAD_READY", onPayloadReady]
-    ];
-    let expectedGenericEvents = awaitedEventsToListeners
-      .filter(([, listener]) => listener == onGenericEvent).length;
 
-    function initProgressForURL(url) {
-      if (progress[url]) {
-        return;
-      }
-      progress[url] = {};
-      awaitedEventsToListeners.forEach(function ([e]) {
-        progress[url][e] = 0;
-      });
-    }
-
-    function updateProgressForURL(url, event) {
-      initProgressForURL(url);
-      progress[url][Object.keys(EVENTS).find(e => EVENTS[e] == event)] = 1;
-    }
-
-    function onGenericEvent(event, actor) {
+    function onNetworkEvent(event, actor) {
       let networkInfo = getNetworkRequest(actor);
       if (!networkInfo) {
         // Must have been related to reloading document to disable cache.
         // Ignore the event.
         return;
       }
-      genericEvents++;
+      networkEvent++;
       maybeResolve(event, actor, networkInfo);
     }
 
     function onPayloadReady(event, actor) {
       let networkInfo = getNetworkRequest(actor);
       if (!networkInfo) {
         // Must have been related to reloading document to disable cache.
         // Ignore the event.
         return;
       }
-
       payloadReady++;
       maybeResolve(event, actor, networkInfo);
     }
 
     function maybeResolve(event, actor, networkInfo) {
       info("> Network event progress: " +
-        "Payload: " + payloadReady + "/" + getRequests + ", " +
-        "Generic: " + genericEvents + "/" + (getRequests * expectedGenericEvents) + ", " +
+        "NetworkEvent: " + networkEvent + "/" + getRequests + ", " +
+        "PayloadReady: " + payloadReady + "/" + getRequests + ", " +
         "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 `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 &&
-        genericEvents >= getRequests * expectedGenericEvents) {
-        awaitedEventsToListeners.forEach(([e, l]) => panel.off(EVENTS[e], l));
+      // Wait until networkEvent & payloadReady finish for each request.
+      if (networkEvent >= getRequests && payloadReady >= getRequests) {
+        panel.off(EVENTS.NETWORK_EVENT, onNetworkEvent);
+        panel.off(EVENTS.PAYLOAD_READY, onPayloadReady);
         executeSoon(resolve);
       }
     }
 
-    awaitedEventsToListeners.forEach(([e, l]) => panel.on(EVENTS[e], l));
+    panel.on(EVENTS.NETWORK_EVENT, onNetworkEvent);
+    panel.on(EVENTS.PAYLOAD_READY, onPayloadReady);
   });
 }
 
-function verifyRequestItemTarget(document, requestList, requestItem, method,
+function* verifyRequestItemTarget(document, requestList, requestItem, method,
                                  url, data = {}) {
   info("> Verifying: " + method + " " + url + " " + data.toSource());
 
   let visibleIndex = requestList.indexOf(requestItem);
 
   info("Visible index of item: " + visibleIndex);
 
   let { fuzzyUrl, status, statusText, cause, type, fullMimeType,
deleted file mode 100644
--- a/devtools/client/netmonitor/test/shared-head.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/* 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.eventTimings ||
-          (!request.responseHeaders && 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
@@ -56,17 +56,16 @@ 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,21 +1,16 @@
 /* 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");
@@ -34,18 +29,16 @@ 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/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
@@ -160,17 +160,16 @@ support-files =
   test-subresource-security-error.js
   test-subresource-security-error.js^headers^
   test-trackingprotection-securityerrors.html
   test-webconsole-error-observer.html
   testscript.js
   !/devtools/client/netmonitor/test/sjs_cors-test-server.sjs
   !/image/test/mochitest/blue.png
   !/devtools/client/framework/test/shared-head.js
-  !/devtools/client/netmonitor/test/shared-head.js
 [browser_console.js]
 skip-if = true # Bug 1406060
 [browser_console_addonsdk_loader_exception.js]
 skip-if = true # Bug 1406060
 [browser_console_clear_method.js]
 skip-if = true # Bug 1406060
 [browser_console_consolejsm_output.js]
 skip-if = true # Bug 1406060
--- 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,11 +59,9 @@ 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
@@ -42,24 +42,39 @@ add_task(async function task() {
   let consoleReady = ui.jsterm.hud.once("network-request-payload-ready");
 
   // Expand network log
   urlNode.click();
 
   await consoleReady;
 
   info("network-request-payload-ready received");
-
   await testNetworkMessage(messageNode);
-
-  await waitForExistingRequests(monitor);
+  await waitForLazyRequests(toolbox);
 });
 
 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.
-  let headersContent = messageNode.querySelector(
-    "#headers-panel .headers-overview");
+  let headersContent;
+  await waitUntil(() => {
+    headersContent = messageNode.querySelector(
+      "#headers-panel .headers-overview");
+    return headersContent;
+  });
+
   ok(headersContent, "Headers content is available");
 }
+
+/**
+ * Wait until all lazily fetch requests in netmonitor get finsished.
+ * Otherwise test will be shutdown too early and cause failure.
+ */
+async function waitForLazyRequests(toolbox) {
+  let { ui } = toolbox.getCurrentPanel().hud;
+  let proxy = ui.jsterm.hud.proxy;
+  return waitUntil(() => {
+    return !proxy.networkDataProvider.lazyRequestData.size;
+  });
+}
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_network_messages_expand.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_network_messages_expand.js
@@ -242,16 +242,20 @@ function testEmptyTimings(messageNode) {
 }
 
 async function testTimings(messageNode) {
   let timingsTab = messageNode.querySelector("#timings-tab");
   ok(timingsTab, "Timings tab is available");
 
   // Select Timings tab and check the content.
   timingsTab.click();
+  await waitUntil(() => {
+    return !!messageNode.querySelector(
+      "#timings-panel .timings-container .timings-label");
+  });
   let timingsContent = messageNode.querySelector(
     "#timings-panel .timings-container .timings-label");
   ok(timingsContent, "Timings content is available");
   ok(timingsContent.textContent, "Timings text is available");
 }
 
 // Stack Trace
 
@@ -294,15 +298,19 @@ async function waitForRequestUpdates(too
   return new Promise(resolve => {
     ui.jsterm.hud.on("network-message-updated", () => {
       info("network-message-updated received");
       resolve();
     });
   });
 }
 
+/**
+ * Wait until all lazily fetch requests in netmonitor get finsished.
+ * Otherwise test will be shutdown too early and cause failure.
+ */
 async function waitForLazyRequests(toolbox) {
   let {ui} = toolbox.getCurrentPanel().hud;
   let proxy = ui.jsterm.hud.proxy;
   return waitUntil(() => {
     return !proxy.networkDataProvider.lazyRequestData.size;
   });
 }
--- 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,12 +70,9 @@ 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,27 +1,23 @@
 /* -*- 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,18 +1,15 @@
 /* -*- 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;
 
@@ -63,22 +60,22 @@ function loadDocument(browser) {
   return deferred.promise;
 }
 
 function* testNetmonitor(toolbox) {
   let monitor = toolbox.getCurrentPanel();
 
   let { store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
-  let { getSortedRequests } = windowRequire("devtools/client/netmonitor/src/selectors/index");
+  let { getSortedRequests } =
+    windowRequire("devtools/client/netmonitor/src/selectors/index");
 
   store.dispatch(Actions.batchEnable(false));
 
   yield waitUntil(() => store.getState().requests.requests.size > 0);
 
-  is(store.getState().requests.requests.size, 1, "Network request appears in the network panel");
+  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,41 +1,35 @@
 /* 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 monitor = toolbox.getCurrentPanel();
 
   let { store, windowRequire } = monitor.panelWin;
-  let { getSelectedRequest } = windowRequire("devtools/client/netmonitor/src/selectors/index");
+  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,28 +1,23 @@
 /* -*- 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();
@@ -64,18 +59,16 @@ 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,