Bug 1404928 - Request Post DATA should be loaded lazily r?honza,ochameau draft
authorRicky Chien <ricky060709@gmail.com>
Thu, 16 Nov 2017 15:54:11 +0800
changeset 705451 a4e1d60960d91f3352a4241ceb7d8fae1ef4b4ea
parent 705442 38f49346a200cc25492236c7b3c536fc835fe031
child 705551 9509a99bafa15eeee65bd6e85dfeda5da222ab52
child 705562 4d8b495c822f02deac36914d63735d57d0a2bd26
child 705634 f4016348fe3bd524491f1fffc0335a1862ee831f
child 705704 c9173748ca0201c11562b259e27e971f32b09339
child 705713 30e0175bc263f1048bd1885b2673506dfd89faa8
child 705767 7a78f67d8c1dbeef047a3fd0a2f3522211abd822
push id91468
push userbmo:rchien@mozilla.com
push dateThu, 30 Nov 2017 02:49:49 +0000
reviewershonza, ochameau
bugs1404928
milestone59.0a1
Bug 1404928 - Request Post DATA should be loaded lazily r?honza,ochameau MozReview-Commit-ID: EQo2OUg9hMD
devtools/client/netmonitor/src/components/CustomRequestPanel.js
devtools/client/netmonitor/src/components/HeadersPanel.js
devtools/client/netmonitor/src/components/ParamsPanel.js
devtools/client/netmonitor/src/components/RequestListContent.js
devtools/client/netmonitor/src/components/ResponsePanel.js
devtools/client/netmonitor/src/components/SecurityPanel.js
devtools/client/netmonitor/src/components/TabboxPanel.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_net_har_copy_all_as_har.js
devtools/client/netmonitor/src/har/test/browser_net_har_post_data.js
devtools/client/netmonitor/src/har/test/browser_net_har_post_data_on_get.js
devtools/client/netmonitor/src/har/test/browser_net_har_throttle_upload.js
devtools/client/netmonitor/src/reducers/requests.js
devtools/client/netmonitor/src/request-list-context-menu.js
devtools/client/netmonitor/test/browser_net_curl-utils.js
devtools/client/netmonitor/test/browser_net_open_in_debugger.js
devtools/client/netmonitor/test/browser_net_open_in_style_editor.js
devtools/client/netmonitor/test/browser_net_open_request_in_tab.js
devtools/client/netmonitor/test/browser_net_post-data-03.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/head.js
--- a/devtools/client/netmonitor/src/components/CustomRequestPanel.js
+++ b/devtools/client/netmonitor/src/components/CustomRequestPanel.js
@@ -1,14 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+const { Component } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { L10N } = require("../utils/l10n");
 const Actions = require("../actions/index");
 const { getSelectedRequest } = require("../selectors/index");
 const {
   getUrlQuery,
@@ -25,229 +26,265 @@ const {
 
 const CUSTOM_CANCEL = L10N.getStr("netmonitor.custom.cancel");
 const CUSTOM_HEADERS = L10N.getStr("netmonitor.custom.headers");
 const CUSTOM_NEW_REQUEST = L10N.getStr("netmonitor.custom.newRequest");
 const CUSTOM_POSTDATA = L10N.getStr("netmonitor.custom.postData");
 const CUSTOM_QUERY = L10N.getStr("netmonitor.custom.query");
 const CUSTOM_SEND = L10N.getStr("netmonitor.custom.send");
 
-function CustomRequestPanel({
-  removeSelectedCustomRequest,
-  request = {},
-  sendCustomRequest,
-  updateRequest,
-}) {
-  let {
-    method,
-    customQueryValue,
-    requestHeaders,
-    requestPostData,
-    url,
-  } = request;
-
-  let headers = "";
-  if (requestHeaders) {
-    headers = requestHeaders.customHeadersValue ?
-      requestHeaders.customHeadersValue : writeHeaderText(requestHeaders.headers);
+/*
+ * Custom request panel component
+ * A network request editor which simply provide edit and resend interface
+ * for netowrk development.
+ */
+class CustomRequestPanel extends Component {
+  static get propTypes() {
+    return {
+      connector: PropTypes.object.isRequired,
+      removeSelectedCustomRequest: PropTypes.func.isRequired,
+      request: PropTypes.object,
+      sendCustomRequest: PropTypes.func.isRequired,
+      updateRequest: PropTypes.func.isRequired,
+    };
   }
-  let queryArray = url ? parseQueryString(getUrlQuery(url)) : [];
-  let params = customQueryValue;
-  if (!params) {
-    params = queryArray ?
-      queryArray.map(({ name, value }) => name + "=" + value).join("\n") : "";
-  }
-  let postData = requestPostData && requestPostData.postData.text ?
-    requestPostData.postData.text : "";
 
-  return (
-    div({ className: "custom-request-panel" },
-      div({ className: "tabpanel-summary-container custom-request" },
-        div({ className: "custom-request-label custom-header" },
-          CUSTOM_NEW_REQUEST
-        ),
-        button({
-          className: "devtools-button",
-          id: "custom-request-send-button",
-          onClick: sendCustomRequest,
-        },
-          CUSTOM_SEND
-        ),
-        button({
-          className: "devtools-button",
-          id: "custom-request-close-button",
-          onClick: removeSelectedCustomRequest,
-        },
-          CUSTOM_CANCEL
-        ),
-      ),
-      div({
-        className: "tabpanel-summary-container custom-method-and-url",
-        id: "custom-method-and-url",
-      },
-        input({
-          className: "custom-method-value",
-          id: "custom-method-value",
-          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
-          value: method || "GET",
-        }),
-        input({
-          className: "custom-url-value",
-          id: "custom-url-value",
-          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
-          value: url || "http://",
-        }),
-      ),
-      // Hide query field when there is no params
-      params ? div({
-        className: "tabpanel-summary-container custom-section",
-        id: "custom-query",
-      },
-        div({ className: "custom-request-label" }, CUSTOM_QUERY),
-        textarea({
-          className: "tabpanel-summary-input",
-          id: "custom-query-value",
-          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
-          rows: 4,
-          value: params,
-          wrap: "off",
-        })
-      ) : null,
-      div({
-        id: "custom-headers",
-        className: "tabpanel-summary-container custom-section",
-      },
-        div({ className: "custom-request-label" }, CUSTOM_HEADERS),
-        textarea({
-          className: "tabpanel-summary-input",
-          id: "custom-headers-value",
-          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
-          rows: 8,
-          value: headers,
-          wrap: "off",
-        })
-      ),
-      div({
-        id: "custom-postdata",
-        className: "tabpanel-summary-container custom-section",
-      },
-        div({ className: "custom-request-label" }, CUSTOM_POSTDATA),
-        textarea({
-          className: "tabpanel-summary-input",
-          id: "custom-postdata-value",
-          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
-          rows: 6,
-          value: postData,
-          wrap: "off",
-        })
-      ),
-    )
-  );
-}
+  componentDidMount() {
+    this.maybeFetchPostData(this.props);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    this.maybeFetchPostData(nextProps);
+  }
 
-CustomRequestPanel.displayName = "CustomRequestPanel";
-
-CustomRequestPanel.propTypes = {
-  connector: PropTypes.object.isRequired,
-  removeSelectedCustomRequest: PropTypes.func.isRequired,
-  request: PropTypes.object,
-  sendCustomRequest: PropTypes.func.isRequired,
-  updateRequest: PropTypes.func.isRequired,
-};
-
-/**
- * Parse a text representation of a name[divider]value list with
- * the given name regex and divider character.
- *
- * @param {string} text - Text of list
- * @return {array} array of headers info {name, value}
- */
-function parseRequestText(text, namereg, divider) {
-  let regex = new RegExp(`(${namereg})\\${divider}\\s*(.+)`);
-  let pairs = [];
-
-  for (let line of text.split("\n")) {
-    let matches = regex.exec(line);
-    if (matches) {
-      let [, name, value] = matches;
-      pairs.push({ name, value });
+  /**
+   * When switching to another request, lazily fetch request post data
+   * from the backend. The panel will first be empty and then display the content.
+   */
+  maybeFetchPostData(props) {
+    if (props.request.requestPostDataAvailable &&
+      (!props.request.requestPostData ||
+        !props.request.requestPostData.postData.text)) {
+      // This method will set `props.request.requestPostData`
+      // asynchronously and force another render.
+      props.connector.requestData(props.request.id, "requestPostData");
     }
   }
-  return pairs;
-}
+
+  /**
+   * Parse a text representation of a name[divider]value list with
+   * the given name regex and divider character.
+   *
+   * @param {string} text - Text of list
+   * @return {array} array of headers info {name, value}
+   */
+  parseRequestText(text, namereg, divider) {
+    let regex = new RegExp(`(${namereg})\\${divider}\\s*(.+)`);
+    let pairs = [];
+
+    for (let line of text.split("\n")) {
+      let matches = regex.exec(line);
+      if (matches) {
+        let [, name, value] = matches;
+        pairs.push({ name, value });
+      }
+    }
+    return pairs;
+  }
+
+  /**
+   * Update Custom Request Fields
+   *
+   * @param {Object} evt click event
+   * @param {Object} request current request
+   * @param {updateRequest} updateRequest action
+   */
+  updateCustomRequestFields(evt, request, updateRequest) {
+    const val = evt.target.value;
+    let data;
 
-/**
- * Update Custom Request Fields
- *
- * @param {Object} evt click event
- * @param {Object} request current request
- * @param {updateRequest} updateRequest action
- */
-function updateCustomRequestFields(evt, request, updateRequest) {
-  const val = evt.target.value;
-  let data;
-  switch (evt.target.id) {
-    case "custom-headers-value":
-      let customHeadersValue = val || "";
-      // Parse text representation of multiple HTTP headers
-      let headersArray = parseRequestText(customHeadersValue, "\\S+?", ":");
-      // Remove temp customHeadersValue while query string is parsable
-      if (customHeadersValue === "" ||
+    switch (evt.target.id) {
+      case "custom-headers-value":
+        let customHeadersValue = val || "";
+        // Parse text representation of multiple HTTP headers
+        let headersArray = this.parseRequestText(customHeadersValue, "\\S+?", ":");
+        // Remove temp customHeadersValue while query string is parsable
+        if (customHeadersValue === "" ||
           headersArray.length === customHeadersValue.split("\n").length) {
-        customHeadersValue = null;
-      }
-      data = {
-        requestHeaders: {
-          customHeadersValue,
-          headers: headersArray,
+          customHeadersValue = null;
+        }
+        data = {
+          requestHeaders: {
+            customHeadersValue,
+            headers: headersArray,
+          },
+        };
+        break;
+      case "custom-method-value":
+        data = { method: val.trim() };
+        break;
+      case "custom-postdata-value":
+        data = {
+          requestPostData: {
+            postData: { text: val },
+          }
+        };
+        break;
+      case "custom-query-value":
+        let customQueryValue = val || "";
+        // Parse readable text list of a query string
+        let queryArray = customQueryValue ?
+          this.parseRequestText(customQueryValue, ".+?", "=") : [];
+        // Write out a list of query params into a query string
+        let queryString = queryArray.map(
+          ({ name, value }) => name + "=" + value).join("&");
+        let url = queryString ? [request.url.split("?")[0], queryString].join("?") :
+          request.url.split("?")[0];
+        // Remove temp customQueryValue while query string is parsable
+        if (customQueryValue === "" ||
+          queryArray.length === customQueryValue.split("\n").length) {
+          customQueryValue = null;
+        }
+        data = {
+          customQueryValue,
+          url,
+        };
+        break;
+      case "custom-url-value":
+        data = {
+          customQueryValue: null,
+          url: val
+        };
+        break;
+      default:
+        break;
+    }
+    if (data) {
+      // All updateRequest batch mode should be disabled to make UI editing in sync
+      updateRequest(request.id, data, false);
+    }
+  }
+
+  render() {
+    let {
+      removeSelectedCustomRequest,
+      request = {},
+      sendCustomRequest,
+      updateRequest,
+    } = this.props;
+    let {
+      method,
+      customQueryValue,
+      requestHeaders,
+      requestPostData,
+      url,
+    } = request;
+
+    let headers = "";
+    if (requestHeaders) {
+      headers = requestHeaders.customHeadersValue ?
+        requestHeaders.customHeadersValue : writeHeaderText(requestHeaders.headers);
+    }
+    let queryArray = url ? parseQueryString(getUrlQuery(url)) : [];
+    let params = customQueryValue;
+    if (!params) {
+      params = queryArray ?
+        queryArray.map(({ name, value }) => name + "=" + value).join("\n") : "";
+    }
+    let postData = requestPostData && requestPostData.postData.text ?
+      requestPostData.postData.text : "";
+
+    return (
+      div({ className: "custom-request-panel" },
+        div({ className: "tabpanel-summary-container custom-request" },
+          div({ className: "custom-request-label custom-header" },
+            CUSTOM_NEW_REQUEST,
+          ),
+          button({
+            className: "devtools-button",
+            id: "custom-request-send-button",
+            onClick: sendCustomRequest,
+          },
+            CUSTOM_SEND,
+          ),
+          button({
+            className: "devtools-button",
+            id: "custom-request-close-button",
+            onClick: removeSelectedCustomRequest,
+          },
+            CUSTOM_CANCEL,
+          ),
+        ),
+        div({
+          className: "tabpanel-summary-container custom-method-and-url",
+          id: "custom-method-and-url",
         },
-      };
-      break;
-    case "custom-method-value":
-      data = { method: val.trim() };
-      break;
-    case "custom-postdata-value":
-      data = {
-        requestPostData: {
-          postData: { text: val },
-        }
-      };
-      break;
-    case "custom-query-value":
-      let customQueryValue = val || "";
-      // Parse readable text list of a query string
-      let queryArray = customQueryValue ?
-        parseRequestText(customQueryValue, ".+?", "=") : [];
-      // Write out a list of query params into a query string
-      let queryString = queryArray.map(
-        ({ name, value }) => name + "=" + value).join("&");
-      let url = queryString ? [request.url.split("?")[0], queryString].join("?") :
-        request.url.split("?")[0];
-      // Remove temp customQueryValue while query string is parsable
-      if (customQueryValue === "" ||
-          queryArray.length === customQueryValue.split("\n").length) {
-        customQueryValue = null;
-      }
-      data = {
-        customQueryValue,
-        url,
-      };
-      break;
-    case "custom-url-value":
-      data = {
-        customQueryValue: null,
-        url: val
-      };
-      break;
-    default:
-      break;
-  }
-  if (data) {
-    // All updateRequest batch mode should be disabled to make UI editing in sync
-    updateRequest(request.id, data, false);
+          input({
+            className: "custom-method-value",
+            id: "custom-method-value",
+            onChange: (evt) =>
+              this.updateCustomRequestFields(evt, request, updateRequest),
+            value: method || "GET",
+          }),
+          input({
+            className: "custom-url-value",
+            id: "custom-url-value",
+            onChange: (evt) =>
+              this.updateCustomRequestFields(evt, request, updateRequest),
+            value: url || "http://",
+          }),
+        ),
+        // Hide query field when there is no params
+        params ? div({
+          className: "tabpanel-summary-container custom-section",
+          id: "custom-query",
+        },
+          div({ className: "custom-request-label" }, CUSTOM_QUERY),
+          textarea({
+            className: "tabpanel-summary-input",
+            id: "custom-query-value",
+            onChange: (evt) =>
+              this.updateCustomRequestFields(evt, request, updateRequest),
+            rows: 4,
+            value: params,
+            wrap: "off",
+          }),
+        ) : null,
+        div({
+          id: "custom-headers",
+          className: "tabpanel-summary-container custom-section",
+        },
+          div({ className: "custom-request-label" }, CUSTOM_HEADERS),
+          textarea({
+            className: "tabpanel-summary-input",
+            id: "custom-headers-value",
+            onChange: (evt) =>
+              this.updateCustomRequestFields(evt, request, updateRequest),
+            rows: 8,
+            value: headers,
+            wrap: "off",
+          }),
+        ),
+        div({
+          id: "custom-postdata",
+          className: "tabpanel-summary-container custom-section",
+        },
+          div({ className: "custom-request-label" }, CUSTOM_POSTDATA),
+          textarea({
+            className: "tabpanel-summary-input",
+            id: "custom-postdata-value",
+            onChange: (evt) =>
+              this.updateCustomRequestFields(evt, request, updateRequest),
+            rows: 6,
+            value: postData,
+            wrap: "off",
+          }),
+        ),
+      )
+    );
   }
 }
 
 module.exports = connect(
   (state) => ({ request: getSelectedRequest(state) }),
   (dispatch, props) => ({
     removeSelectedCustomRequest: () => dispatch(Actions.removeSelectedCustomRequest()),
     sendCustomRequest: () => dispatch(Actions.sendCustomRequest(props.connector)),
--- a/devtools/client/netmonitor/src/components/HeadersPanel.js
+++ b/devtools/client/netmonitor/src/components/HeadersPanel.js
@@ -44,16 +44,17 @@ const SUMMARY_VERSION = L10N.getStr("net
 
 /*
  * Headers panel component
  * Lists basic information about the request
  */
 class HeadersPanel extends Component {
   static get propTypes() {
     return {
+      connector: PropTypes.object.isRequired,
       cloneSelectedRequest: PropTypes.func.isRequired,
       request: PropTypes.object.isRequired,
       renderValue: PropTypes.func,
       openLink: PropTypes.func,
     };
   }
 
   constructor(props) {
@@ -62,16 +63,40 @@ class HeadersPanel extends Component {
     this.state = {
       rawHeadersOpened: false,
     };
 
     this.getProperties = this.getProperties.bind(this);
     this.toggleRawHeaders = this.toggleRawHeaders.bind(this);
     this.renderSummary = this.renderSummary.bind(this);
     this.renderValue = this.renderValue.bind(this);
+    this.maybeFetchPostData = this.maybeFetchPostData.bind(this);
+  }
+
+  componentDidMount() {
+    this.maybeFetchPostData(this.props);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    this.maybeFetchPostData(nextProps);
+  }
+
+  /**
+   * When switching to another request, lazily fetch request post data
+   * from the backend. The panel will first be empty and then display the content.
+   * Fetching post data is used for updating requestHeadersFromUploadStream section,
+   */
+  maybeFetchPostData(props) {
+    if (props.request.requestPostDataAvailable &&
+        (!props.request.requestPostData ||
+        !props.request.requestPostData.postData.text)) {
+      // This method will set `props.request.requestPostData`
+      // asynchronously and force another render.
+      props.connector.requestData(props.request.id, "requestPostData");
+    }
   }
 
   getProperties(headers, title) {
     if (headers && headers.headers.length) {
       let headerKey = `${title} (${getFormattedSize(headers.headersSize, 3)})`;
       let propertiesResult = {
         [headerKey]:
           headers.headers.reduce((acc, { name, value }) =>
--- a/devtools/client/netmonitor/src/components/ParamsPanel.js
+++ b/devtools/client/netmonitor/src/components/ParamsPanel.js
@@ -41,56 +41,100 @@ class ParamsPanel extends Component {
     return {
       connector: PropTypes.object.isRequired,
       openLink: PropTypes.func,
       request: PropTypes.object.isRequired,
       updateRequest: PropTypes.func.isRequired,
     };
   }
 
+  constructor(props) {
+    super(props);
+  }
+
   componentDidMount() {
+    this.maybeFetchPostData(this.props);
     updateFormDataSections(this.props);
   }
 
   componentWillReceiveProps(nextProps) {
+    this.maybeFetchPostData(nextProps);
     updateFormDataSections(nextProps);
   }
 
+  /**
+   * When switching to another request, lazily fetch request post data
+   * from the backend. The panel will first be empty and then display the content.
+   */
+  maybeFetchPostData(props) {
+    if (props.request.requestPostDataAvailable &&
+        (!props.request.requestPostData ||
+        !props.request.requestPostData.postData.text)) {
+      // This method will set `props.request.requestPostData`
+      // asynchronously and force another render.
+      props.connector.requestData(props.request.id, "requestPostData");
+    }
+  }
+
+  /**
+   * Mapping array to dict for TreeView usage.
+   * Since TreeView only support Object(dict) format.
+   * This function also deal with duplicate key case
+   * (for multiple selection and query params with same keys)
+   *
+   * @param {Object[]} arr - key-value pair array like query or form params
+   * @returns {Object} Rep compatible object
+   */
+  getProperties(arr) {
+    return sortObjectKeys(arr.reduce((map, obj) => {
+      let value = map[obj.name];
+      if (value) {
+        if (typeof value !== "object") {
+          map[obj.name] = [value];
+        }
+        map[obj.name].push(obj.value);
+      } else {
+        map[obj.name] = obj.value;
+      }
+      return map;
+    }, {}));
+  }
+
   render() {
     let {
       openLink,
       request
     } = this.props;
     let {
       formDataSections,
       mimeType,
       requestPostData,
       url,
     } = request;
     let postData = requestPostData ? requestPostData.postData.text : null;
     let query = getUrlQuery(url);
 
-    if (!formDataSections && !postData && !query) {
+    if ((!formDataSections || formDataSections.length === 0) && !postData && !query) {
       return div({ className: "empty-notice" },
         PARAMS_EMPTY_TEXT
       );
     }
 
     let object = {};
     let json;
 
     // Query String section
     if (query) {
-      object[PARAMS_QUERY_STRING] = getProperties(parseQueryString(query));
+      object[PARAMS_QUERY_STRING] = this.getProperties(parseQueryString(query));
     }
 
     // Form Data section
     if (formDataSections && formDataSections.length > 0) {
       let sections = formDataSections.filter((str) => /\S/.test(str)).join("&");
-      object[PARAMS_FORM_DATA] = getProperties(parseFormData(sections));
+      object[PARAMS_FORM_DATA] = this.getProperties(parseFormData(sections));
     }
 
     // Request payload section
     if (formDataSections && formDataSections.length === 0 && postData) {
       try {
         json = JSON.parse(postData);
       } catch (error) {
         // Continue regardless of parsing error
@@ -118,37 +162,13 @@ class ParamsPanel extends Component {
           sectionNames: SECTION_NAMES,
           openLink,
         })
       )
     );
   }
 }
 
-/**
- * Mapping array to dict for TreeView usage.
- * Since TreeView only support Object(dict) format.
- * This function also deal with duplicate key case
- * (for multiple selection and query params with same keys)
- *
- * @param {Object[]} arr - key-value pair array like query or form params
- * @returns {Object} Rep compatible object
- */
-function getProperties(arr) {
-  return sortObjectKeys(arr.reduce((map, obj) => {
-    let value = map[obj.name];
-    if (value) {
-      if (typeof value !== "object") {
-        map[obj.name] = [value];
-      }
-      map[obj.name].push(obj.value);
-    } else {
-      map[obj.name] = obj.value;
-    }
-    return map;
-  }, {}));
-}
-
 module.exports = connect(null,
   (dispatch) => ({
     updateRequest: (id, data, batch) => dispatch(Actions.updateRequest(id, data, batch)),
   }),
 )(ParamsPanel);
--- a/devtools/client/netmonitor/src/components/RequestListContent.js
+++ b/devtools/client/netmonitor/src/components/RequestListContent.js
@@ -8,16 +8,17 @@ const { Component, createFactory } = req
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
 const Actions = require("../actions/index");
 const { setTooltipImageContent } = require("../request-list-tooltip");
 const {
   getDisplayedRequests,
+  getSelectedRequest,
   getWaterfallScale,
 } = require("../selectors/index");
 
 // Components
 const RequestListHeader = createFactory(require("./RequestListHeader"));
 const RequestListItem = createFactory(require("./RequestListItem"));
 const RequestListContextMenu = require("../request-list-context-menu");
 
@@ -31,59 +32,57 @@ const MAX_SCROLL_HEIGHT = 2147483647;
 /**
  * Renders the actual contents of the request list.
  */
 class RequestListContent extends Component {
   static get propTypes() {
     return {
       connector: PropTypes.object.isRequired,
       columns: PropTypes.object.isRequired,
-      dispatch: PropTypes.func.isRequired,
+      cloneSelectedRequest: PropTypes.func.isRequired,
       displayedRequests: PropTypes.array.isRequired,
       firstRequestStartedMillis: PropTypes.number.isRequired,
       fromCache: PropTypes.bool,
       onCauseBadgeMouseDown: PropTypes.func.isRequired,
       onItemMouseDown: PropTypes.func.isRequired,
       onSecurityIconMouseDown: PropTypes.func.isRequired,
       onSelectDelta: PropTypes.func.isRequired,
       onWaterfallMouseDown: PropTypes.func.isRequired,
+      openStatistics: PropTypes.func.isRequired,
       scale: PropTypes.number,
-      selectedRequestId: PropTypes.string,
+      selectedRequest: PropTypes.object,
     };
   }
 
   constructor(props) {
     super(props);
     this.isScrolledToBottom = this.isScrolledToBottom.bind(this);
     this.onHover = this.onHover.bind(this);
     this.onScroll = this.onScroll.bind(this);
     this.onKeyDown = this.onKeyDown.bind(this);
     this.onContextMenu = this.onContextMenu.bind(this);
     this.onFocusedNodeChange = this.onFocusedNodeChange.bind(this);
   }
 
   componentWillMount() {
-    const { dispatch, connector } = this.props;
+    const { connector, cloneSelectedRequest, openStatistics } = this.props;
     this.contextMenu = new RequestListContextMenu({
-      cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
-      getTabTarget: connector.getTabTarget,
-      getLongString: connector.getLongString,
-      openStatistics: (open) => dispatch(Actions.openStatistics(connector, open)),
-      requestData: connector.requestData,
+      connector,
+      cloneSelectedRequest,
+      openStatistics,
     });
     this.tooltip = new HTMLTooltip(window.parent.document, { type: "arrow" });
   }
 
   componentDidMount() {
     // Install event handler for displaying a tooltip
     this.tooltip.startTogglingOnHover(this.refs.contentEl, this.onHover, {
       toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
       interactive: true
     });
-
     // Install event handler to hide the tooltip on scroll
     this.refs.contentEl.addEventListener("scroll", this.onScroll, true);
   }
 
   componentWillUpdate(nextProps) {
     // Check if the list is scrolled to bottom before the UI update.
     // The scroll is ever needed only if new rows are added to the list.
     const delta = nextProps.displayedRequests.size - this.props.displayedRequests.size;
@@ -215,37 +214,37 @@ class RequestListContent extends Compone
       columns,
       displayedRequests,
       firstRequestStartedMillis,
       onCauseBadgeMouseDown,
       onItemMouseDown,
       onSecurityIconMouseDown,
       onWaterfallMouseDown,
       scale,
-      selectedRequestId,
+      selectedRequest,
     } = this.props;
 
     return (
-      div({ className: "requests-list-wrapper"},
-        div({ className: "requests-list-table"},
+      div({ className: "requests-list-wrapper" },
+        div({ className: "requests-list-table" },
           div({
             ref: "contentEl",
             className: "requests-list-contents",
             tabIndex: 0,
             onKeyDown: this.onKeyDown,
-            style: {"--timings-scale": scale, "--timings-rev-scale": 1 / scale}
+            style: { "--timings-scale": scale, "--timings-rev-scale": 1 / scale }
           },
             RequestListHeader(),
             displayedRequests.map((item, index) => RequestListItem({
               firstRequestStartedMillis,
               fromCache: item.status === "304" || item.fromCache,
               columns,
               item,
               index,
-              isSelected: item.id === selectedRequestId,
+              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(),
             }))
@@ -256,21 +255,22 @@ class RequestListContent extends Compone
   }
 }
 
 module.exports = connect(
   (state) => ({
     columns: state.ui.columns,
     displayedRequests: getDisplayedRequests(state),
     firstRequestStartedMillis: state.requests.firstStartedMillis,
-    selectedRequestId: state.requests.selectedId,
+    selectedRequest: getSelectedRequest(state),
     scale: getWaterfallScale(state),
   }),
-  (dispatch) => ({
-    dispatch,
+  (dispatch, props) => ({
+    cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
+    openStatistics: (open) => dispatch(Actions.openStatistics(props.connector, open)),
     /**
      * A handler that opens the stack trace tab when a stack trace is available
      */
     onCauseBadgeMouseDown: (cause) => {
       if (cause.stacktrace && cause.stacktrace.length > 0) {
         dispatch(Actions.selectDetailsPanelTab("stack-trace"));
       }
     },
--- a/devtools/client/netmonitor/src/components/ResponsePanel.js
+++ b/devtools/client/netmonitor/src/components/ResponsePanel.js
@@ -51,27 +51,20 @@ class ResponsePanel extends Component {
         height: 0,
       },
     };
 
     this.updateImageDimemsions = this.updateImageDimemsions.bind(this);
     this.isJSON = this.isJSON.bind(this);
   }
 
-  /**
-   * `componentDidMount` is called when opening the ResponsePanel for the first time
-   */
   componentDidMount() {
     this.maybeFetchResponseContent(this.props);
   }
 
-  /**
-   * `componentWillReceiveProps` is the only method called when switching between two
-   * requests while the response panel is displayed.
-   */
   componentWillReceiveProps(nextProps) {
     this.maybeFetchResponseContent(nextProps);
   }
 
   /**
    * When switching to another request, lazily fetch response content
    * from the backend. The Response Panel will first be empty and then
    * display the content.
--- a/devtools/client/netmonitor/src/components/SecurityPanel.js
+++ b/devtools/client/netmonitor/src/components/SecurityPanel.js
@@ -54,27 +54,20 @@ class SecurityPanel extends Component {
   static get propTypes() {
     return {
       connector: PropTypes.object.isRequired,
       openLink: PropTypes.func,
       request: PropTypes.object.isRequired,
     };
   }
 
-  /**
-   * `componentDidMount` is called when opening the SecurityPanel for the first time
-   */
   componentDidMount() {
     this.maybeFetchSecurityInfo(this.props);
   }
 
-  /**
-   * `componentWillReceiveProps` is the only method called when switching between two
-   * requests while the security panel is displayed.
-   */
   componentWillReceiveProps(nextProps) {
     this.maybeFetchSecurityInfo(nextProps);
   }
 
   /**
    * When switching to another request, lazily fetch securityInfo
    * from the backend. The Security Panel will first be empty and then
    * display the content.
--- a/devtools/client/netmonitor/src/components/TabboxPanel.js
+++ b/devtools/client/netmonitor/src/components/TabboxPanel.js
@@ -1,17 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { createFactory } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { L10N } = require("../utils/l10n");
 const { PANELS } = require("../constants");
 
 // Components
 const Tabbar = createFactory(require("devtools/client/shared/components/tabs/TabBar"));
 const TabPanel = createFactory(require("devtools/client/shared/components/tabs/Tabs").TabPanel);
 const CookiesPanel = createFactory(require("./CookiesPanel"));
 const HeadersPanel = createFactory(require("./HeadersPanel"));
@@ -53,17 +52,22 @@ function TabboxPanel({
       onSelect: selectTab,
       renderOnlySelected: true,
       showAllTabsMenu: true,
     },
       TabPanel({
         id: PANELS.HEADERS,
         title: HEADERS_TITLE,
       },
-        HeadersPanel({ request, cloneSelectedRequest, openLink }),
+        HeadersPanel({
+          cloneSelectedRequest,
+          connector,
+          openLink,
+          request,
+        }),
       ),
       TabPanel({
         id: PANELS.COOKIES,
         title: COOKIES_TITLE,
       },
         CookiesPanel({ request, openLink }),
       ),
       TabPanel({
@@ -113,9 +117,9 @@ TabboxPanel.propTypes = {
   cloneSelectedRequest: PropTypes.func,
   connector: PropTypes.object.isRequired,
   openLink: PropTypes.func,
   request: PropTypes.object,
   selectTab: PropTypes.func.isRequired,
   sourceMapService: PropTypes.object,
 };
 
-module.exports = connect()(TabboxPanel);
+module.exports = TabboxPanel;
--- a/devtools/client/netmonitor/src/connector/firefox-data-provider.js
+++ b/devtools/client/netmonitor/src/connector/firefox-data-provider.js
@@ -30,16 +30,17 @@ class FirefoxDataProvider {
     this.rdpRequestMap = new Map();
 
     // Map[key string => Promise] used by `requestData` to prevent requesting the same
     // request data twice.
     this.lazyRequestData = new Map();
 
     // Fetching data from the backend
     this.getLongString = this.getLongString.bind(this);
+    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.
@@ -255,28 +256,25 @@ class FirefoxDataProvider {
   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. (i.e. all data
-    // received, but the lazy one. responseContent is the only one for now).
+    // 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.requestCookies &&
-      record.eventTimings &&
+    return record.requestHeaders && record.requestCookies && record.eventTimings &&
       (
-       (record.responseHeaders && record.responseCookies) ||
-       payload.securityState == "broken" ||
-       (payload.responseContentAvailable && !payload.status)
+        (record.responseHeaders && record.responseCookies) ||
+        payload.securityState === "broken" ||
+        (!payload.status && payload.responseContentAvailable)
       );
   }
 
   /**
    * Merge upcoming networkEventUpdate payload into existing one.
    *
    * @param {string} id request id
    * @param {object} payload request data payload
@@ -379,23 +377,27 @@ class FirefoxDataProvider {
     // that started during the pause and we missed its `networkEvent`.
     if (!this.rdpRequestMap.has(actor)) {
       return;
     }
 
     switch (updateType) {
       case "requestHeaders":
       case "requestCookies":
-      case "requestPostData":
       case "responseHeaders":
       case "responseCookies":
         this.requestPayloadData(actor, updateType);
         break;
-      // (Be careful, securityState can be undefined, for example for WebSocket requests)
-      // Also note that service worker don't have security info set.
+      case "requestPostData":
+        this.updateRequest(actor, {
+          // This field helps knowing when/if requestPostData property is available
+          // and can be requested via `requestData`
+          requestPostDataAvailable: true
+        });
+        break;
       case "securityInfo":
         this.updateRequest(actor, { securityState: networkInfo.securityInfo });
         break;
       case "responseStart":
         this.updateRequest(actor, {
           httpVersion: networkInfo.response.httpVersion,
           remoteAddress: networkInfo.response.remoteAddress,
           remotePort: networkInfo.response.remotePort,
@@ -406,17 +408,16 @@ class FirefoxDataProvider {
           emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
         });
         break;
       case "responseContent":
         this.updateRequest(actor, {
           contentSize: networkInfo.response.bodySize,
           transferredSize: networkInfo.response.transferredSize,
           mimeType: networkInfo.response.content.mimeType,
-
           // This field helps knowing when/if responseContent property is available
           // and can be requested via `requestData`
           responseContentAvailable: true,
         });
         break;
       case "eventTimings":
         this.pushRequestToQueue(actor, { totalTime: networkInfo.totalTime });
         this.requestPayloadData(actor, updateType);
@@ -466,19 +467,18 @@ class FirefoxDataProvider {
     // or, everytime requestData is called for fetching data lazily.
     if (this.isRequestPayloadReady(actor)) {
       let payloadFromQueue = this.getRequestFromQueue(actor).payload;
 
       // Clean up
       this.cleanUpQueue(actor);
       this.rdpRequestMap.delete(actor);
 
-      let { updateRequest } = this.actions;
-      if (updateRequest) {
-        await updateRequest(actor, payloadFromQueue, true);
+      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);
     }
   }
 
@@ -493,34 +493,36 @@ class FirefoxDataProvider {
    * @param {string} actor actor id (used as request id)
    * @param {string} method identifier of the data we want to fetch
    *
    * @return {Promise} return a promise resolved when data is received.
    */
   requestData(actor, method) {
     // Key string used in `lazyRequestData`. We use this Map to prevent requesting
     // the same data twice at the same time.
-    let key = actor + "-" + method;
+    let key = `${actor}-${method}`;
     let promise = this.lazyRequestData.get(key);
     // If a request is pending, reuse it.
     if (promise) {
       return promise;
     }
     // Fetch the data
     promise = this._requestData(actor, method);
     this.lazyRequestData.set(key, promise);
     promise.then(async () => {
       // Remove the request from the cache, any new call to requestData will fetch the
       // data again.
       this.lazyRequestData.delete(key, promise);
 
-      let payloadFromQueue = this.getRequestFromQueue(actor).payload;
-      let { updateRequest } = this.actions;
-      if (updateRequest) {
-        await updateRequest(actor, payloadFromQueue, true);
+      if (this.actions.updateRequest) {
+        await this.actions.updateRequest(
+          actor,
+          this.getRequestFromQueue(actor).payload,
+          true,
+        );
       }
     });
     return promise;
   }
 
   /**
    * Internal helper used to request HTTP details from the backend.
    *
@@ -543,24 +545,35 @@ class FirefoxDataProvider {
     let updatingEventName = `UPDATING_${method.replace(/([A-Z])/g, "_$1").toUpperCase()}`;
 
     // Emit event that tell we just start fetching some data
     emit(EVENTS[updatingEventName], actor);
 
     let response = await new Promise((resolve, reject) => {
       // Do a RDP request to fetch data from the actor.
       if (typeof this.webConsoleClient[clientMethodName] === "function") {
-        this.webConsoleClient[clientMethodName](actor, (res) => {
+        // 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.error);
+          }
           resolve(res);
         });
       } else {
         reject(new Error(`Error: No such client method '${clientMethodName}'!`));
       }
     });
 
+    // Restore clone actor id
+    if (actor.includes("-clone")) {
+      // Because response's properties are read-only, we create a new response
+      response = { ...response, from: `${response.from}-clone` };
+    }
+
     // Call data processing method.
     return this[callbackMethodName](response);
   }
 
   /**
    * Handles additional information received for a "requestHeaders" packet.
    *
    * @param {object} response the message received from the server.
@@ -586,22 +599,22 @@ class FirefoxDataProvider {
     });
   }
 
   /**
    * Handles additional information received for a "requestPostData" packet.
    *
    * @param {object} response the message received from the server.
    */
-  onRequestPostData(response) {
-    return this.updateRequest(response.from, {
+  async onRequestPostData(response) {
+    let payload = await this.updateRequest(response.from, {
       requestPostData: response
-    }).then(() => {
-      emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
     });
+    emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
+    return payload;
   }
 
   /**
    * Handles additional information received for a "securityInfo" packet.
    *
    * @param {object} response the message received from the server.
    */
   onSecurityInfo(response) {
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -119,16 +119,17 @@ const UPDATE_PROPS = [
   "totalTime",
   "eventTimings",
   "headersSize",
   "customQueryValue",
   "requestHeaders",
   "requestHeadersFromUploadStream",
   "requestCookies",
   "requestPostData",
+  "requestPostDataAvailable",
   "responseHeaders",
   "responseCookies",
   "responseContent",
   "responseContentAvailable",
   "formDataSections",
   "stacktrace",
 ];
 
--- a/devtools/client/netmonitor/src/har/har-builder.js
+++ b/devtools/client/netmonitor/src/har/har-builder.js
@@ -115,17 +115,17 @@ HarBuilder.prototype = {
   buildEntry: async function (log, file) {
     let page = this.getPage(log, file);
 
     let entry = {};
     entry.pageref = page.id;
     entry.startedDateTime = dateToJSON(new Date(file.startedMillis));
     entry.time = file.endedMillis - file.startedMillis;
 
-    entry.request = this.buildRequest(file);
+    entry.request = await this.buildRequest(file);
     entry.response = await this.buildResponse(file);
     entry.cache = this.buildCache(file);
     entry.timings = file.eventTimings ? file.eventTimings.timings : {};
 
     if (file.remoteAddress) {
       entry.serverIPAddress = file.remoteAddress;
     }
 
@@ -147,43 +147,33 @@ HarBuilder.prototype = {
     let timings = {
       onContentLoad: -1,
       onLoad: -1
     };
 
     return timings;
   },
 
-  buildRequest: function (file) {
+  buildRequest: async function (file) {
     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.appendHeadersPostData(request.headers, file);
     request.cookies = this.buildCookies(file.requestCookies);
-
     request.queryString = parseQueryString(getUrlQuery(file.url)) || [];
-
-    if (file.requestPostData) {
-      request.postData = this.buildPostData(file);
-    }
-
     request.headersSize = file.requestHeaders.headersSize;
+    request.postData = await this.buildPostData(file);
 
-    // Set request body size, but make sure the body is fetched
-    // from the backend.
-    if (file.requestPostData) {
-      this.fetchData(file.requestPostData.postData.text).then(value => {
-        request.bodySize = value.length;
-      });
+    if (request.postData && request.postData.text) {
+      request.bodySize = request.postData.text.length;
     }
 
     return request;
   },
 
   /**
    * Fetch all header values from the backend (if necessary) and
    * build the result HAR structure.
@@ -238,57 +228,67 @@ HarBuilder.prototype = {
           value: value
         });
       });
     });
 
     return result;
   },
 
-  buildPostData: function (file) {
+  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.
+    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;
+    }
+
     let postData = {
-      mimeType: findValue(file.requestHeaders.headers, "content-type"),
+      mimeType: findValue(requestHeaders.headers, "content-type"),
       params: [],
-      text: ""
+      text: requestPostData.postData.text,
     };
 
-    if (!file.requestPostData) {
-      return postData;
-    }
-
-    if (file.requestPostData.postDataDiscarded) {
+    if (requestPostData.postDataDiscarded) {
       postData.comment = L10N.getStr("har.requestBodyNotIncluded");
       return postData;
     }
 
-    // Load request body from the backend.
-    this.fetchData(file.requestPostData.postData.text).then(postDataText => {
-      postData.text = postDataText;
-
-      // If we are dealing with URL encoded body, parse parameters.
-      let { headers } = file.requestHeaders;
-      if (CurlUtils.isUrlEncodedRequest({ headers, postDataText })) {
-        postData.mimeType = "application/x-www-form-urlencoded";
+    // If we are dealing with URL encoded body, parse parameters.
+    if (CurlUtils.isUrlEncodedRequest({
+      headers: requestHeaders.headers,
+      postDataText: postData.text,
+    })) {
+      postData.mimeType = "application/x-www-form-urlencoded";
 
-        // Extract form parameters and produce nice HAR array.
-        getFormDataSections(
-          file.requestHeaders,
-          file.requestHeadersFromUploadStream,
-          file.requestPostData,
-          this._options.getString,
-        ).then(formDataSections => {
-          formDataSections.forEach(section => {
-            let paramsArray = parseQueryString(section);
-            if (paramsArray) {
-              postData.params = [...postData.params, ...paramsArray];
-            }
-          });
-        });
-      }
-    });
+      // Extract form parameters and produce nice HAR array.
+      let formDataSections = await getFormDataSections(
+        requestHeaders,
+        requestHeadersFromUploadStream,
+        requestPostData,
+        this._options.getString,
+      );
+
+      formDataSections.forEach((section) => {
+        let paramsArray = parseQueryString(section);
+        if (paramsArray) {
+          postData.params = [...postData.params, ...paramsArray];
+        }
+      });
+    }
 
     return postData;
   },
 
   buildResponse: async function (file) {
     let response = {
       status: 0
     };
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_copy_all_as_har.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_copy_all_as_har.js
@@ -14,28 +14,28 @@ add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
 
   info("Starting test... ");
 
   let { connector, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let RequestListContextMenu = windowRequire(
     "devtools/client/netmonitor/src/request-list-context-menu");
-  let { getLongString, getTabTarget, requestData } = connector;
+  let { getSortedRequests } = windowRequire(
+    "devtools/client/netmonitor/src/selectors/index");
 
   store.dispatch(Actions.batchEnable(false));
 
   let wait = waitForNetworkEvents(monitor, 1);
   tab.linkedBrowser.reload();
   yield wait;
 
-  let contextMenu = new RequestListContextMenu({
-    getTabTarget, getLongString, requestData });
+  let contextMenu = new RequestListContextMenu({ connector });
 
-  yield contextMenu.copyAllAsHar();
+  yield contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
 
   let jsonString = SpecialPowers.getClipboardData("text/unicode");
   let har = JSON.parse(jsonString);
 
   // Check out HAR log
   isnot(har.log, null, "The HAR log must exist");
   is(har.log.creator.name, "Firefox", "The creator field must be set");
   is(har.log.browser.name, "Firefox", "The browser field must be set");
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_post_data.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_post_data.js
@@ -11,31 +11,31 @@ add_task(function* () {
     HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
 
   info("Starting test... ");
 
   let { connector, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let RequestListContextMenu = windowRequire(
     "devtools/client/netmonitor/src/request-list-context-menu");
-  let { getLongString, getTabTarget, requestData } = connector;
+  let { getSortedRequests } = windowRequire(
+    "devtools/client/netmonitor/src/selectors/index");
 
   store.dispatch(Actions.batchEnable(false));
 
   // Execute one POST request on the page and wait till its done.
   let wait = waitForNetworkEvents(monitor, 0, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.executeTest();
   });
   yield wait;
 
   // Copy HAR into the clipboard (asynchronous).
-  let contextMenu = new RequestListContextMenu({
-    getTabTarget, getLongString, requestData });
-  let jsonString = yield contextMenu.copyAllAsHar();
+  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");
   is(har.log.pages.length, 1, "There must be one page");
   is(har.log.entries.length, 1, "There must be one request");
 
   let entry = har.log.entries[0];
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_post_data_on_get.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_post_data_on_get.js
@@ -11,37 +11,37 @@ add_task(function* () {
     HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
 
   info("Starting test... ");
 
   let { connector, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let RequestListContextMenu = windowRequire(
     "devtools/client/netmonitor/src/request-list-context-menu");
-  let { getLongString, getTabTarget, requestData } = connector;
+  let { getSortedRequests } = windowRequire(
+    "devtools/client/netmonitor/src/selectors/index");
 
   store.dispatch(Actions.batchEnable(false));
 
   // Execute one GET request on the page and wait till its done.
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.executeTest3();
   });
   yield wait;
 
   // Copy HAR into the clipboard (asynchronous).
-  let contextMenu = new RequestListContextMenu({
-    getTabTarget, getLongString, requestData });
-  let jsonString = yield contextMenu.copyAllAsHar();
+  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");
   is(har.log.pages.length, 1, "There must be one page");
   is(har.log.entries.length, 1, "There must be one request");
 
   let entry = har.log.entries[0];
-  is(entry.request.postData, undefined,
-    "Check post data is not present");
+
+  is(entry.request.postData, undefined, "Check post data is not present");
 
   // Clean up
   return teardown(monitor);
 });
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_throttle_upload.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_throttle_upload.js
@@ -15,17 +15,18 @@ function* throttleUploadTest(actuallyThr
     HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
 
   info("Starting test... (actuallyThrottle = " + actuallyThrottle + ")");
 
   let { connector, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let RequestListContextMenu = windowRequire(
     "devtools/client/netmonitor/src/request-list-context-menu");
-  let { getLongString, getTabTarget, setPreferences, requestData } = connector;
+  let { getSortedRequests } = windowRequire(
+    "devtools/client/netmonitor/src/selectors/index");
 
   store.dispatch(Actions.batchEnable(false));
 
   const size = 4096;
   const uploadSize = actuallyThrottle ? size / 3 : 0;
 
   const request = {
     "NetworkMonitor.throttleData": {
@@ -35,32 +36,31 @@ function* throttleUploadTest(actuallyThr
       downloadBPSMax: 200000,
       uploadBPSMean: uploadSize,
       uploadBPSMax: uploadSize,
     },
   };
 
   info("sending throttle request");
   yield new Promise((resolve) => {
-    setPreferences(request, (response) => {
+    connector.setPreferences(request, (response) => {
       resolve(response);
     });
   });
 
   // Execute one POST request on the page and wait till its done.
   let wait = waitForNetworkEvents(monitor, 0, 1);
   yield ContentTask.spawn(tab.linkedBrowser, { size }, function* (args) {
     content.wrappedJSObject.executeTest2(args.size);
   });
   yield wait;
 
   // Copy HAR into the clipboard (asynchronous).
-  let contextMenu = new RequestListContextMenu({
-    getTabTarget, getLongString, requestData });
-  let jsonString = yield contextMenu.copyAllAsHar();
+  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");
   is(har.log.pages.length, 1, "There must be one page");
   is(har.log.entries.length, 1, "There must be one request");
 
   let entry = har.log.entries[0];
--- a/devtools/client/netmonitor/src/reducers/requests.js
+++ b/devtools/client/netmonitor/src/reducers/requests.js
@@ -129,16 +129,17 @@ function requestsReducer(state = Request
 
       let newRequest = {
         id: clonedRequest.id + "-clone",
         method: clonedRequest.method,
         url: clonedRequest.url,
         urlDetails: clonedRequest.urlDetails,
         requestHeaders: clonedRequest.requestHeaders,
         requestPostData: clonedRequest.requestPostData,
+        requestPostDataAvailable: clonedRequest.requestPostDataAvailable,
         isCustom: true
       };
 
       return {
         ...state,
         requests: mapSet(requests, newRequest.id, newRequest),
         selectedId: newRequest.id,
       };
--- a/devtools/client/netmonitor/src/request-list-context-menu.js
+++ b/devtools/client/netmonitor/src/request-list-context-menu.js
@@ -4,408 +4,391 @@
 
 "use strict";
 
 const Services = require("Services");
 const { Curl } = require("devtools/client/shared/curl");
 const { gDevTools } = require("devtools/client/framework/devtools");
 const { saveAs } = require("devtools/client/shared/file-saver");
 const { copyString } = require("devtools/shared/platform/clipboard");
+const { showMenu } = require("devtools/client/netmonitor/src/utils/menu");
 const { HarExporter } = require("./har/har-exporter");
 const {
   getSelectedRequest,
   getSortedRequests,
 } = require("./selectors/index");
 const { L10N } = require("./utils/l10n");
-const { showMenu } = require("devtools/client/netmonitor/src/utils/menu");
 const {
+  formDataURI,
   getUrlQuery,
+  getUrlBaseName,
   parseQueryString,
-  getUrlBaseName,
-  formDataURI,
 } = require("./utils/request-utils");
 
-function RequestListContextMenu({
-  cloneSelectedRequest,
-  getLongString,
-  getTabTarget,
-  openStatistics,
-  requestData,
-}) {
-  this.cloneSelectedRequest = cloneSelectedRequest;
-  this.getLongString = getLongString;
-  this.getTabTarget = getTabTarget;
-  this.openStatistics = openStatistics;
-  this.requestData = requestData;
-}
+class RequestListContextMenu {
+  constructor(props) {
+    this.props = props;
+  }
 
-RequestListContextMenu.prototype = {
-  get selectedRequest() {
+  open(event) {
     // FIXME: Bug 1336382 - Implement RequestListContextMenu React component
-    // Remove window.store
-    return getSelectedRequest(window.store.getState());
-  },
+    // Remove window.store.getState()
+    let selectedRequest = getSelectedRequest(window.store.getState());
+    let sortedRequests = getSortedRequests(window.store.getState());
 
-  get sortedRequests() {
-    // FIXME: Bug 1336382 - Implement RequestListContextMenu React component
-    // Remove window.store
-    return getSortedRequests(window.store.getState());
-  },
-
-  /**
-   * Handle the context menu opening. Hide items if no request is selected.
-   * Since visible attribute only accept boolean value but the method call may
-   * return undefined, we use !! to force convert any object to boolean
-   */
-  open(event = {}) {
-    let selectedRequest = this.selectedRequest;
     let menu = [];
     let copySubmenu = [];
+    let {
+      id,
+      isCustom,
+      method,
+      mimeType,
+      httpVersion,
+      requestHeaders,
+      requestPostDataAvailable,
+      responseHeaders,
+      responseContentAvailable,
+      url,
+    } = selectedRequest || {};
+    let {
+      cloneSelectedRequest,
+      openStatistics,
+    } = this.props;
 
     copySubmenu.push({
       id: "request-list-context-copy-url",
       label: L10N.getStr("netmonitor.context.copyUrl"),
       accesskey: L10N.getStr("netmonitor.context.copyUrl.accesskey"),
       visible: !!selectedRequest,
-      click: () => this.copyUrl(),
+      click: () => this.copyUrl(url),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-url-params",
       label: L10N.getStr("netmonitor.context.copyUrlParams"),
       accesskey: L10N.getStr("netmonitor.context.copyUrlParams.accesskey"),
-      visible: !!(selectedRequest && getUrlQuery(selectedRequest.url)),
-      click: () => this.copyUrlParams(),
+      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"),
-      visible: !!(selectedRequest && selectedRequest.requestPostData),
-      click: () => this.copyPostData(),
+      visible: !!(selectedRequest && requestPostDataAvailable),
+      click: () => this.copyPostData(id),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-as-curl",
       label: L10N.getStr("netmonitor.context.copyAsCurl"),
       accesskey: L10N.getStr("netmonitor.context.copyAsCurl.accesskey"),
       visible: !!selectedRequest,
-      click: () => this.copyAsCurl(),
+      click: () => this.copyAsCurl(id, url, method, requestHeaders, httpVersion),
     });
 
     copySubmenu.push({
       type: "separator",
-      visible: !!selectedRequest,
+      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 && selectedRequest.requestHeaders),
-      click: () => this.copyRequestHeaders(),
+      visible: !!(selectedRequest && requestHeaders && requestHeaders.rawHeaders),
+      click: () => this.copyRequestHeaders(requestHeaders.rawHeaders.trim()),
     });
 
     copySubmenu.push({
       id: "response-list-context-copy-response-headers",
       label: L10N.getStr("netmonitor.context.copyResponseHeaders"),
       accesskey: L10N.getStr("netmonitor.context.copyResponseHeaders.accesskey"),
-      visible: !!(selectedRequest && selectedRequest.responseHeaders),
-      click: () => this.copyResponseHeaders(),
+      visible: !!(selectedRequest && responseHeaders && responseHeaders.rawHeaders),
+      click: () => this.copyResponseHeaders(responseHeaders.rawHeaders.trim()),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-response",
       label: L10N.getStr("netmonitor.context.copyResponse"),
       accesskey: L10N.getStr("netmonitor.context.copyResponse.accesskey"),
-      visible: !!(selectedRequest && selectedRequest.responseContentAvailable),
-      click: () => this.copyResponse(),
+      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"),
-      visible: !!(selectedRequest &&
-               selectedRequest.mimeType &&
-               selectedRequest.mimeType.includes("image/")),
-      click: () => this.copyImageAsDataUri(),
+      visible: !!(selectedRequest && mimeType && mimeType.includes("image/")),
+      click: () => this.copyImageAsDataUri(id, mimeType),
     });
 
     copySubmenu.push({
       type: "separator",
-      visible: !!selectedRequest,
+      visible: copySubmenu.slice(5, 9).some((subMenu) => subMenu.visible),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-all-as-har",
       label: L10N.getStr("netmonitor.context.copyAllAsHar"),
       accesskey: L10N.getStr("netmonitor.context.copyAllAsHar.accesskey"),
-      visible: this.sortedRequests.size > 0,
-      click: () => this.copyAllAsHar(),
+      visible: sortedRequests.size > 0,
+      click: () => this.copyAllAsHar(sortedRequests),
     });
 
     menu.push({
       label: L10N.getStr("netmonitor.context.copy"),
       accesskey: L10N.getStr("netmonitor.context.copy.accesskey"),
       visible: !!selectedRequest,
       submenu: copySubmenu,
     });
 
     menu.push({
       id: "request-list-context-save-all-as-har",
       label: L10N.getStr("netmonitor.context.saveAllAsHar"),
       accesskey: L10N.getStr("netmonitor.context.saveAllAsHar.accesskey"),
-      visible: this.sortedRequests.size > 0,
-      click: () => this.saveAllAsHar(),
+      visible: sortedRequests.size > 0,
+      click: () => this.saveAllAsHar(sortedRequests),
     });
 
     menu.push({
       id: "request-list-context-save-image-as",
       label: L10N.getStr("netmonitor.context.saveImageAs"),
       accesskey: L10N.getStr("netmonitor.context.saveImageAs.accesskey"),
-      visible: !!(selectedRequest &&
-               selectedRequest.mimeType &&
-               selectedRequest.mimeType.includes("image/")),
-      click: () => this.saveImageAs(),
+      visible: !!(selectedRequest && mimeType && mimeType.includes("image/")),
+      click: () => this.saveImageAs(id, url),
     });
 
     menu.push({
       type: "separator",
-      visible: !!(selectedRequest && !selectedRequest.isCustom),
+      visible: copySubmenu.slice(10, 14).some((subMenu) => subMenu.visible),
     });
 
     menu.push({
       id: "request-list-context-resend",
       label: L10N.getStr("netmonitor.context.editAndResend"),
       accesskey: L10N.getStr("netmonitor.context.editAndResend.accesskey"),
-      visible: !!(selectedRequest && !selectedRequest.isCustom),
-      click: this.cloneSelectedRequest,
+      visible: !!(selectedRequest && !isCustom),
+      click: cloneSelectedRequest,
     });
 
     menu.push({
       type: "separator",
-      visible: !!selectedRequest,
+      visible: copySubmenu.slice(15, 16).some((subMenu) => subMenu.visible),
     });
 
     menu.push({
       id: "request-list-context-newtab",
       label: L10N.getStr("netmonitor.context.newTab"),
       accesskey: L10N.getStr("netmonitor.context.newTab.accesskey"),
       visible: !!selectedRequest,
-      click: () => this.openRequestInTab()
+      click: () => this.openRequestInTab(url),
     });
 
     menu.push({
       id: "request-list-context-open-in-debugger",
       label: L10N.getStr("netmonitor.context.openInDebugger"),
       accesskey: L10N.getStr("netmonitor.context.openInDebugger.accesskey"),
-      visible: !!(selectedRequest &&
-               selectedRequest.mimeType &&
-               selectedRequest.mimeType.includes("javascript")),
-      click: () => this.openInDebugger()
+      visible: !!(selectedRequest && mimeType && mimeType.includes("javascript")),
+      click: () => this.openInDebugger(url),
     });
 
     menu.push({
       id: "request-list-context-open-in-style-editor",
       label: L10N.getStr("netmonitor.context.openInStyleEditor"),
       accesskey: L10N.getStr("netmonitor.context.openInStyleEditor.accesskey"),
       visible: !!(selectedRequest &&
-               Services.prefs.getBoolPref("devtools.styleeditor.enabled") &&
-               selectedRequest.mimeType &&
-               selectedRequest.mimeType.includes("css")),
-      click: () => this.openInStyleEditor()
+        Services.prefs.getBoolPref("devtools.styleeditor.enabled") &&
+        mimeType && mimeType.includes("css")),
+      click: () => this.openInStyleEditor(url),
     });
 
     menu.push({
       id: "request-list-context-perf",
       label: L10N.getStr("netmonitor.context.perfTools"),
       accesskey: L10N.getStr("netmonitor.context.perfTools.accesskey"),
-      visible: this.sortedRequests.size > 0,
-      click: () => this.openStatistics(true)
+      visible: sortedRequests.size > 0,
+      click: () => openStatistics(true),
     });
 
-    return showMenu(event, menu);
-  },
+    showMenu(event, menu);
+  }
 
   /**
    * Opens selected item in a new tab.
    */
-  openRequestInTab() {
+  openRequestInTab(url) {
     let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
-    win.openUILinkIn(this.selectedRequest.url, "tab", { relatedToCurrent: true });
-  },
+    win.openUILinkIn(url, "tab", { relatedToCurrent: true });
+  }
 
   /**
    * Opens selected item in the debugger
    */
-  openInDebugger() {
-    let toolbox = gDevTools.getToolbox(this.getTabTarget());
-    toolbox.viewSourceInDebugger(this.selectedRequest.url, 0);
-  },
+  openInDebugger(url) {
+    let toolbox = gDevTools.getToolbox(this.props.connector.getTabTarget());
+    toolbox.viewSourceInDebugger(url, 0);
+  }
 
   /**
    * Opens selected item in the style editor
    */
-  openInStyleEditor() {
-    let toolbox = gDevTools.getToolbox(this.getTabTarget());
-    toolbox.viewSourceInStyleEditor(this.selectedRequest.url, 0);
-  },
+  openInStyleEditor(url) {
+    let toolbox = gDevTools.getToolbox(this.props.connector.getTabTarget());
+    toolbox.viewSourceInStyleEditor(url, 0);
+  }
 
   /**
    * Copy the request url from the currently selected item.
    */
-  copyUrl() {
-    copyString(this.selectedRequest.url);
-  },
+  copyUrl(url) {
+    copyString(url);
+  }
 
   /**
    * Copy the request url query string parameters from the currently
    * selected item.
    */
-  copyUrlParams() {
-    let params = getUrlQuery(this.selectedRequest.url).split("&");
+  copyUrlParams(url) {
+    let params = getUrlQuery(url).split("&");
     copyString(params.join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n"));
-  },
+  }
 
   /**
    * Copy the request form data parameters (or raw payload) from
    * the currently selected item.
    */
-  copyPostData() {
-    let { formDataSections, requestPostData } = this.selectedRequest;
+  async copyPostData(id) {
+    // FIXME: Bug 1336382 - Implement RequestListContextMenu React component
+    // Remove window.store.getState()
+    let { formDataSections } = getSelectedRequest(window.store.getState());
     let params = [];
-
     // Try to extract any form data parameters.
     formDataSections.forEach(section => {
       let paramsArray = parseQueryString(section);
       if (paramsArray) {
         params = [...params, ...paramsArray];
       }
     });
 
     let string = params
       .map(param => param.name + (param.value ? "=" + param.value : ""))
       .join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n");
 
     // Fall back to raw payload.
     if (!string) {
+      let { requestPostData } = await this.props.connector
+        .requestData(id, "requestPostData");
       string = requestPostData.postData.text;
       if (Services.appinfo.OS !== "WINNT") {
         string = string.replace(/\r/g, "");
       }
     }
     copyString(string);
-  },
+  }
 
   /**
    * Copy a cURL command from the currently selected item.
    */
-  copyAsCurl() {
-    let selected = this.selectedRequest;
+  async copyAsCurl(id, url, method, requestHeaders, httpVersion) {
+    let { requestPostData } = await this.props.connector
+      .requestData(id, "requestPostData");
     // Create a sanitized object for the Curl command generator.
     let data = {
-      url: selected.url,
-      method: selected.method,
-      headers: selected.requestHeaders.headers,
-      httpVersion: selected.httpVersion,
-      postDataText: selected.requestPostData && selected.requestPostData.postData.text,
+      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() {
-    let rawHeaders = this.selectedRequest.requestHeaders.rawHeaders.trim();
+  copyRequestHeaders(rawHeaders) {
     if (Services.appinfo.OS !== "WINNT") {
       rawHeaders = rawHeaders.replace(/\r/g, "");
     }
     copyString(rawHeaders);
-  },
+  }
 
   /**
    * Copy the raw response headers from the currently selected item.
    */
-  copyResponseHeaders() {
-    let rawHeaders = this.selectedRequest.responseHeaders.rawHeaders.trim();
+  copyResponseHeaders(rawHeaders) {
     if (Services.appinfo.OS !== "WINNT") {
       rawHeaders = rawHeaders.replace(/\r/g, "");
     }
     copyString(rawHeaders);
-  },
+  }
 
   /**
    * Copy image as data uri.
    */
-  async copyImageAsDataUri() {
-    let responseContent = await this.requestData(this.selectedRequest.id,
-      "responseContent");
-    let { mimeType } = this.selectedRequest;
+  async copyImageAsDataUri(id, mimeType) {
+    let responseContent = await this.props.connector.requestData(id, "responseContent");
     let { encoding, text } = responseContent.content;
-    let src = formDataURI(mimeType, encoding, text);
-    copyString(src);
-  },
+    copyString(formDataURI(mimeType, encoding, text));
+  }
 
   /**
    * Save image as.
    */
-  async saveImageAs() {
-    let responseContent = await this.requestData(this.selectedRequest.id,
-      "responseContent");
+  async saveImageAs(id, url) {
+    let responseContent = await this.props.connector.requestData(id, "responseContent");
     let { encoding, text } = responseContent.content;
-    let fileName = getUrlBaseName(this.selectedRequest.url);
+    let fileName = getUrlBaseName(url);
     let data;
     if (encoding === "base64") {
       let decoded = atob(text);
       data = new Uint8Array(decoded.length);
       for (let i = 0; i < decoded.length; ++i) {
         data[i] = decoded.charCodeAt(i);
       }
     } else {
       data = text;
     }
     saveAs(new Blob([data]), fileName, document);
-  },
+  }
 
   /**
    * Copy response data as a string.
    */
-  async copyResponse() {
-    let responseContent = await this.requestData(this.selectedRequest.id,
-      "responseContent");
+  async copyResponse(id) {
+    let responseContent = await this.props.connector.requestData(id, "responseContent");
     copyString(responseContent.content.text);
-  },
+  }
 
   /**
    * Copy HAR from the network panel content to the clipboard.
    */
-  copyAllAsHar() {
-    return HarExporter.copy(this.getDefaultHarOptions());
-  },
+  copyAllAsHar(sortedRequests) {
+    return HarExporter.copy(this.getDefaultHarOptions(sortedRequests));
+  }
 
   /**
    * Save HAR from the network panel content to a file.
    */
-  saveAllAsHar() {
-    // FIXME: This will not work in launchpad
+  saveAllAsHar(sortedRequests) {
+    // This will not work in launchpad
     // document.execCommand(‘cut’/‘copy’) was denied because it was not called from
     // inside a short running user-generated event handler.
     // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard
-    return HarExporter.save(this.getDefaultHarOptions());
-  },
+    return HarExporter.save(this.getDefaultHarOptions(sortedRequests));
+  }
 
-  getDefaultHarOptions() {
-    let form = this.getTabTarget().form;
-    let title = form.title || form.url;
+  getDefaultHarOptions(sortedRequests) {
+    let { getLongString, getTabTarget, requestData } = this.props.connector;
+    let { form: { title, url } } = getTabTarget();
 
     return {
-      requestData: this.requestData,
-      getString: this.getLongString,
-      items: this.sortedRequests,
-      title: title
+      getString: getLongString,
+      items: sortedRequests,
+      requestData,
+      title: title || url,
     };
   }
-};
+}
 
 module.exports = RequestListContextMenu;
--- a/devtools/client/netmonitor/test/browser_net_curl-utils.js
+++ b/devtools/client/netmonitor/test/browser_net_curl-utils.js
@@ -13,49 +13,52 @@ add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CURL_UTILS_URL);
   info("Starting test... ");
 
   let { store, windowRequire, connector } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let {
     getSortedRequests,
   } = windowRequire("devtools/client/netmonitor/src/selectors/index");
-  let { getLongString } = connector;
+  let {
+    getLongString,
+    requestData,
+  } = connector;
 
   store.dispatch(Actions.batchEnable(false));
 
   let wait = waitForNetworkEvents(monitor, 1, 3);
   yield ContentTask.spawn(tab.linkedBrowser, SIMPLE_SJS, function* (url) {
     content.wrappedJSObject.performRequests(url);
   });
   yield wait;
 
   let requests = {
     get: getSortedRequests(store.getState()).get(0),
     post: getSortedRequests(store.getState()).get(1),
     multipart: getSortedRequests(store.getState()).get(2),
     multipartForm: getSortedRequests(store.getState()).get(3),
   };
 
-  let data = yield createCurlData(requests.get, getLongString);
+  let data = yield createCurlData(requests.get, getLongString, requestData);
   testFindHeader(data);
 
-  data = yield createCurlData(requests.post, getLongString);
+  data = yield createCurlData(requests.post, getLongString, requestData);
   testIsUrlEncodedRequest(data);
   testWritePostDataTextParams(data);
   testWriteEmptyPostDataTextParams(data);
   testDataArgumentOnGeneratedCommand(data);
 
-  data = yield createCurlData(requests.multipart, getLongString);
+  data = yield createCurlData(requests.multipart, getLongString, requestData);
   testIsMultipartRequest(data);
   testGetMultipartBoundary(data);
   testMultiPartHeaders(data);
   testRemoveBinaryDataFromMultipartText(data);
 
-  data = yield createCurlData(requests.multipartForm, getLongString);
+  data = yield createCurlData(requests.multipartForm, getLongString, requestData);
   testMultiPartHeaders(data);
 
   testGetHeadersFromMultipartText({
     postDataText: "Content-Type: text/plain\r\n\r\n",
   });
 
   if (Services.appinfo.OS != "WINNT") {
     testEscapeStringPosix();
@@ -226,17 +229,17 @@ function testEscapeStringWin() {
     "Backslashes should be escaped.");
 
   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) {
+function* createCurlData(selected, getLongString, requestData) {
   let { url, method, httpVersion } = selected;
 
   // Create a sanitized object for the Curl command generator.
   let data = {
     url,
     method,
     headers: [],
     httpVersion,
@@ -244,16 +247,17 @@ function* createCurlData(selected, getLo
   };
 
   // Fetch header values.
   for (let { name, value } of selected.requestHeaders.headers) {
     let text = yield getLongString(value);
     data.headers.push({ name: name, value: text });
   }
 
+  let { requestPostData } = yield requestData(selected.id, "requestPostData");
   // Fetch the request payload.
-  if (selected.requestPostData) {
-    let postData = selected.requestPostData.postData.text;
+  if (requestPostData) {
+    let postData = requestPostData.postData.text;
     data.postDataText = yield getLongString(postData);
   }
 
   return data;
 }
--- a/devtools/client/netmonitor/test/browser_net_open_in_debugger.js
+++ b/devtools/client/netmonitor/test/browser_net_open_in_debugger.js
@@ -7,33 +7,34 @@
  * Test the 'Open in debugger' feature
  */
 
 add_task(function* () {
   let { tab, monitor, toolbox} = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
   info("Starting test... ");
 
   let { document, store, windowRequire } = monitor.panelWin;
-
+  let contextMenuDoc = monitor.panelWin.parent.document;
   // Avoid async processing
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   store.dispatch(Actions.batchEnable(false));
 
   let wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
+  wait = waitForDOM(contextMenuDoc, "#request-list-context-open-in-debugger");
   EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelectorAll(".request-list-item")[2]);
   EventUtils.sendMouseEvent({ type: "contextmenu" },
     document.querySelectorAll(".request-list-item")[2]);
+  yield wait;
 
   let onDebuggerReady = toolbox.once("jsdebugger-ready");
-  monitor.panelWin.parent.document
-    .querySelector("#request-list-context-open-in-debugger").click();
+  contextMenuDoc.querySelector("#request-list-context-open-in-debugger").click();
   yield onDebuggerReady;
 
   ok(true, "Debugger has been open");
 
   yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_open_in_style_editor.js
+++ b/devtools/client/netmonitor/test/browser_net_open_in_style_editor.js
@@ -7,31 +7,33 @@
  * Test the 'Open in debugger' feature
  */
 
 add_task(function* () {
   let { tab, monitor, toolbox} = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
   info("Starting test... ");
 
   let { document, store, windowRequire } = monitor.panelWin;
-
+  let contextMenuDoc = monitor.panelWin.parent.document;
   // Avoid async processing
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   store.dispatch(Actions.batchEnable(false));
 
   let wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
+  wait = waitForDOM(contextMenuDoc, "#request-list-context-open-in-style-editor");
   EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelectorAll(".request-list-item")[1]);
   EventUtils.sendMouseEvent({ type: "contextmenu" },
     document.querySelectorAll(".request-list-item")[1]);
+  yield wait;
 
   let onStyleEditorReady = toolbox.once("styleeditor-ready");
   monitor.panelWin.parent.document
     .querySelector("#request-list-context-open-in-style-editor").click();
   yield onStyleEditorReady;
 
   ok(true, "Style Editor has been open");
 
--- a/devtools/client/netmonitor/test/browser_net_open_request_in_tab.js
+++ b/devtools/client/netmonitor/test/browser_net_open_request_in_tab.js
@@ -7,30 +7,33 @@
  * Tests if Open in new tab works.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
   info("Starting test...");
 
   let { document, store, windowRequire } = monitor.panelWin;
+  let contextMenuDoc = monitor.panelWin.parent.document;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
 
   store.dispatch(Actions.batchEnable(false));
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests(1);
   });
   yield wait;
 
+  wait = waitForDOM(contextMenuDoc, "#request-list-context-newtab");
   EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelectorAll(".request-list-item")[0]);
   EventUtils.sendMouseEvent({ type: "contextmenu" },
     document.querySelectorAll(".request-list-item")[0]);
+  yield wait;
 
   let onTabOpen = once(gBrowser.tabContainer, "TabOpen", false);
   monitor.panelWin.parent.document
     .querySelector("#request-list-context-newtab").click();
   yield onTabOpen;
 
   ok(true, "A new tab has been opened");
 
--- a/devtools/client/netmonitor/test/browser_net_post-data-03.js
+++ b/devtools/client/netmonitor/test/browser_net_post-data-03.js
@@ -21,25 +21,24 @@ add_task(function* () {
 
   let wait = waitForNetworkEvents(monitor, 0, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
   // Wait for all tree view updated by react
-  wait = waitForDOM(document, "#headers-panel");
+  wait = waitForDOM(document, "#headers-panel .tree-section .treeLabel", 3);
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector(".network-details-panel-toggle"));
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector("#headers-tab"));
   yield wait;
 
   let tabpanel = document.querySelector("#headers-panel");
-
   is(tabpanel.querySelectorAll(".tree-section .treeLabel").length, 3,
     "There should be 3 header sections displayed in this tabpanel.");
 
   is(tabpanel.querySelectorAll(".tree-section .treeLabel")[2].textContent,
     L10N.getStr("requestHeadersFromUpload") + " (" +
     L10N.getFormatStr("networkMenu.sizeB", 74) + ")",
     "The request headers from upload section doesn't have the correct title.");
 
--- a/devtools/client/netmonitor/test/browser_net_raw_headers.js
+++ b/devtools/client/netmonitor/test/browser_net_raw_headers.js
@@ -25,19 +25,21 @@ add_task(function* () {
   });
   yield wait;
 
   wait = waitForDOM(document, ".headers-overview");
   EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelectorAll(".request-list-item")[0]);
   yield wait;
 
+  let onRequestPostData = monitor.panelWin.once(EVENTS.RECEIVED_REQUEST_POST_DATA);
   wait = waitForDOM(document, ".raw-headers-container textarea", 2);
   EventUtils.sendMouseEvent({ type: "click" }, getRawHeadersButton());
   yield wait;
+  yield onRequestPostData;
 
   testRawHeaderButtonStyle(true);
 
   testShowRawHeaders(getSortedRequests(store.getState()).get(0));
 
   EventUtils.sendMouseEvent({ type: "click" }, getRawHeadersButton());
 
   testRawHeaderButtonStyle(false);
--- a/devtools/client/netmonitor/test/browser_net_resend.js
+++ b/devtools/client/netmonitor/test/browser_net_resend.js
@@ -12,16 +12,17 @@ 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));
 
@@ -31,36 +32,38 @@ add_task(function* () {
   });
   yield wait;
 
   let origItem = getSortedRequests(store.getState()).get(0);
 
   store.dispatch(Actions.selectRequest(origItem.id));
 
   // add a new custom request cloned from selected request
+
   store.dispatch(Actions.cloneSelectedRequest());
-
   testCustomForm(origItem);
 
   let customItem = getSelectedRequest(store.getState());
   testCustomItem(customItem, origItem);
 
   // edit the custom request
   yield editCustomForm();
+
   // FIXME: reread the customItem, it's been replaced by a new object (immutable!)
   customItem = getSelectedRequest(store.getState());
   testCustomItemChanged(customItem, origItem);
 
   // send the new request
   wait = waitForNetworkEvents(monitor, 0, 1);
   store.dispatch(Actions.sendCustomRequest(connector));
   yield wait;
 
   let sentItem = getSelectedRequest(store.getState());
-  testSentRequest(sentItem, origItem);
+
+  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,
     "The detail panel is hidden");
 
@@ -137,37 +140,43 @@ add_task(function* () {
     type(ADD_UA_HEADER);
 
     let postData = document.getElementById("custom-postdata-value");
     let postFocus = once(postData, "focus", false);
     postData.setSelectionRange(postData.value.length, postData.value.length);
     postData.focus();
     yield postFocus;
 
-    // add to POST data
+    // add to POST data once textarea has updated
+    yield waitUntil(() => postData.textContent !== "");
     type(ADD_POSTDATA);
   }
 
   /*
    * Make sure newly created event matches expected request
    */
-  function testSentRequest(data, origData) {
+  function* testSentRequest(data, origData) {
     is(data.method, origData.method, "correct method in sent request");
     is(data.url, origData.url + "&" + ADD_QUERY, "correct url in sent request");
 
     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");
 
-    is(data.requestPostData.postData.text,
-       origData.requestPostData.postData.text + ADD_POSTDATA,
-       "post data 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,
+      "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
@@ -58,20 +58,21 @@ add_task(function* () {
   // 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 response content
+      // Force fetching lazy load data
       let responseContent = yield connector.requestData(item.id, "responseContent");
+      let { requestPostData } = yield connector.requestData(item.id, "requestPostData");
 
-      is(item.requestPostData.postData.text, "post-data",
+      is(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: *",
         "The POST response has the right content");
     }
   }
 
   info("Finishing the test");
--- a/devtools/client/netmonitor/test/head.js
+++ b/devtools/client/netmonitor/test/head.js
@@ -298,37 +298,32 @@ function teardown(monitor) {
 }
 
 function waitForNetworkEvents(monitor, getRequests, postRequests = 0) {
   return new Promise((resolve) => {
     let panel = monitor.panelWin;
     let { getNetworkRequest } = panel.connector;
     let progress = {};
     let genericEvents = 0;
-    let postEvents = 0;
     let payloadReady = 0;
     let awaitedEventsToListeners = [
       ["UPDATING_REQUEST_HEADERS", onGenericEvent],
       ["RECEIVED_REQUEST_HEADERS", onGenericEvent],
       ["UPDATING_REQUEST_COOKIES", onGenericEvent],
       ["RECEIVED_REQUEST_COOKIES", onGenericEvent],
-      ["UPDATING_REQUEST_POST_DATA", onPostEvent],
-      ["RECEIVED_REQUEST_POST_DATA", onPostEvent],
       ["UPDATING_RESPONSE_HEADERS", onGenericEvent],
       ["RECEIVED_RESPONSE_HEADERS", onGenericEvent],
       ["UPDATING_RESPONSE_COOKIES", onGenericEvent],
       ["RECEIVED_RESPONSE_COOKIES", onGenericEvent],
       ["UPDATING_EVENT_TIMINGS", onGenericEvent],
       ["RECEIVED_EVENT_TIMINGS", onGenericEvent],
       ["PAYLOAD_READY", onPayloadReady]
     ];
     let expectedGenericEvents = awaitedEventsToListeners
       .filter(([, listener]) => listener == onGenericEvent).length;
-    let expectedPostEvents = awaitedEventsToListeners
-      .filter(([, listener]) => listener == onPostEvent).length;
 
     function initProgressForURL(url) {
       if (progress[url]) {
         return;
       }
       progress[url] = {};
       awaitedEventsToListeners.forEach(function ([e]) {
         progress[url][e] = 0;
@@ -346,27 +341,16 @@ function waitForNetworkEvents(monitor, g
         // Must have been related to reloading document to disable cache.
         // Ignore the event.
         return;
       }
       genericEvents++;
       maybeResolve(event, actor, networkInfo);
     }
 
-    function onPostEvent(event, actor) {
-      let networkInfo = getNetworkRequest(actor);
-      if (!networkInfo) {
-        // Must have been related to reloading document to disable cache.
-        // Ignore the event.
-        return;
-      }
-      postEvents++;
-      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;
       }
 
@@ -374,31 +358,29 @@ function waitForNetworkEvents(monitor, g
       maybeResolve(event, actor, networkInfo);
     }
 
     function maybeResolve(event, actor, networkInfo) {
       info("> Network events progress: " +
         "Payload: " + payloadReady + "/" + (getRequests + postRequests) + ", " +
         "Generic: " + genericEvents + "/" +
           ((getRequests + postRequests) * expectedGenericEvents) + ", " +
-        "Post: " + postEvents + "/" + (postRequests * expectedPostEvents) + ", " +
         "got " + event + " for " + actor);
 
       let url = networkInfo.request.url;
       updateProgressForURL(url, event);
 
       // Uncomment this to get a detailed progress logging (when debugging a test)
       // info("> Current state: " + JSON.stringify(progress, null, 2));
 
       // There are `expectedGenericEvents` updates which need to be fired for a request
       // to be considered finished. The "requestPostData" packet isn't fired for non-POST
       // requests.
       if (payloadReady >= (getRequests + postRequests) &&
-        genericEvents >= (getRequests + postRequests) * expectedGenericEvents &&
-        postEvents >= postRequests * expectedPostEvents) {
+        genericEvents >= (getRequests + postRequests) * expectedGenericEvents) {
         awaitedEventsToListeners.forEach(([e, l]) => panel.off(EVENTS[e], l));
         executeSoon(resolve);
       }
     }
 
     awaitedEventsToListeners.forEach(([e, l]) => panel.on(EVENTS[e], l));
   });
 }