Bug 1418927 - requestHeaders and responseHeaders should be loaded lazily r?honza,ochameau draft
authorRicky Chien <ricky060709@gmail.com>
Fri, 01 Dec 2017 18:40:36 +0800
changeset 711271 7b15fac001fc4175b5312895111e4f72e7e4a299
parent 711268 4add05ca4e0e33631efb0143f376f99930c3cfa7
child 743783 a32c77d8ab3b80a04605d00ba1c63b25a279609c
push id93042
push userbmo:rchien@mozilla.com
push dateWed, 13 Dec 2017 17:57:51 +0000
reviewershonza, ochameau
bugs1418927
milestone59.0a1
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,