Bug 1427718 - Display multiple headers with the same name; r=rickychien draft
authorJan Odvarko <odvarko@gmail.com>
Wed, 17 Jan 2018 09:56:09 +0100
changeset 721391 c5d368d84ce4766fe40e4b750441e1d4474db504
parent 721208 b2cb61e83ac50115a28f04aaa8a32d4db90aad23
child 746334 8cbe6ececbfb62d1346a2840fa137652f183b0f0
push id95837
push userjodvarko@mozilla.com
push dateWed, 17 Jan 2018 09:15:14 +0000
reviewersrickychien
bugs1427718
milestone59.0a1
Bug 1427718 - Display multiple headers with the same name; r=rickychien MozReview-Commit-ID: 2nV51xLyCni
devtools/client/netmonitor/src/components/HeadersPanel.js
devtools/client/netmonitor/src/components/PropertiesView.js
devtools/client/netmonitor/src/utils/headers-provider.js
devtools/client/netmonitor/src/utils/moz.build
devtools/client/netmonitor/test/browser_net_headers_sorted.js
devtools/client/netmonitor/test/browser_net_simple-request-data.js
devtools/client/netmonitor/test/sjs_simple-test-server.sjs
devtools/client/shared/components/tree/TreeView.js
devtools/shared/webconsole/network-monitor.js
--- a/devtools/client/netmonitor/src/components/HeadersPanel.js
+++ b/devtools/client/netmonitor/src/components/HeadersPanel.js
@@ -15,17 +15,20 @@ const { L10N } = require("../utils/l10n"
 const {
   getHeadersURL,
   getHTTPStatusCodeURL,
 } = require("../utils/mdn-utils");
 const {
   fetchNetworkUpdatePacket,
   writeHeaderText,
 } = require("../utils/request-utils");
-const { sortObjectKeys } = require("../utils/sort-utils");
+const {
+  HeadersProvider,
+  HeaderList,
+} = require("../utils/headers-provider");
 
 // Components
 const PropertiesView = createFactory(require("./PropertiesView"));
 
 loader.lazyGetter(this, "MDNLink", function () {
   return createFactory(require("./MdnLink"));
 });
 
@@ -48,17 +51,17 @@ const REQUEST_HEADERS = L10N.getStr("req
 const REQUEST_HEADERS_FROM_UPLOAD = L10N.getStr("requestHeadersFromUpload");
 const RESPONSE_HEADERS = L10N.getStr("responseHeaders");
 const SUMMARY_ADDRESS = L10N.getStr("netmonitor.summary.address");
 const SUMMARY_METHOD = L10N.getStr("netmonitor.summary.method");
 const SUMMARY_URL = L10N.getStr("netmonitor.summary.url");
 const SUMMARY_STATUS = L10N.getStr("netmonitor.summary.status");
 const SUMMARY_VERSION = L10N.getStr("netmonitor.summary.version");
 
-/*
+/**
  * 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,
@@ -98,23 +101,18 @@ class HeadersPanel extends Component {
       "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 }) =>
-            name ? Object.assign(acc, { [name]: value }) : acc
-          , {})
+        [headerKey]: new HeaderList(headers.headers)
       };
-
-      propertiesResult[headerKey] = sortObjectKeys(propertiesResult[headerKey]);
       return propertiesResult;
     }
 
     return null;
   }
 
   toggleRawHeaders() {
     this.setState({
@@ -297,16 +295,17 @@ class HeadersPanel extends Component {
           summaryMethod,
           summaryAddress,
           summaryStatus,
           summaryVersion,
           summaryRawHeaders,
         ),
         PropertiesView({
           object,
+          provider: HeadersProvider,
           filterPlaceHolder: HEADERS_FILTER_TEXT,
           sectionNames: Object.keys(object),
           renderValue: this.renderValue,
           openLink,
         }),
       )
     );
   }
--- a/devtools/client/netmonitor/src/components/PropertiesView.js
+++ b/devtools/client/netmonitor/src/components/PropertiesView.js
@@ -37,31 +37,32 @@ loader.lazyGetter(this, "MODE", function
 });
 
 const { div, tr, td } = dom;
 const AUTO_EXPAND_MAX_LEVEL = 7;
 const AUTO_EXPAND_MAX_NODES = 50;
 const EDITOR_CONFIG_ID = "EDITOR_CONFIG";
 const HTML_PREVIEW_ID = "HTML_PREVIEW";
 
-/*
+/**
  * Properties View component
  * A scrollable tree view component which provides some useful features for
  * representing object properties.
  *
  * Search filter - Set enableFilter to enable / disable SearchBox feature.
  * Tree view - Default enabled.
  * Source editor - Enable by specifying object level 1 property name to EDITOR_CONFIG_ID.
  * HTML preview - Enable by specifying object level 1 property name to HTML_PREVIEW_ID.
  * Rep - Default enabled.
  */
 class PropertiesView extends Component {
   static get propTypes() {
     return {
       object: PropTypes.object,
+      provider: PropTypes.object,
       enableInput: PropTypes.bool,
       expandableStrings: PropTypes.bool,
       filterPlaceHolder: PropTypes.string,
       sectionNames: PropTypes.array,
       openLink: PropTypes.func,
       cropLimit: PropTypes.number
     };
   }
@@ -185,32 +186,34 @@ class PropertiesView extends Component {
       enableInput,
       expandableStrings,
       filterPlaceHolder,
       object,
       renderRow,
       renderValue,
       sectionNames,
       openLink,
+      provider,
     } = this.props;
 
     return (
       div({ className: "properties-view" },
         this.shouldRenderSearchBox(object) &&
           div({ className: "searchbox-section" },
             SearchBox({
               delay: FILTER_SEARCH_DELAY,
               type: "filter",
               onChange: this.updateFilterText,
               placeholder: filterPlaceHolder,
             }),
           ),
         div({ className: "tree-container" },
           TreeView({
             object,
+            provider,
             columns: [{
               id: "value",
               width: "100%",
             }],
             decorator: decorator || {
               getRowClass: (rowObject) => this.getRowClass(rowObject, sectionNames),
             },
             enableInput,
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/headers-provider.js
@@ -0,0 +1,86 @@
+/* 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 { ObjectProvider } = require("devtools/client/shared/components/tree/ObjectProvider");
+
+/**
+ * Custom tree provider.
+ *
+ * This provider is used to provide set of headers and is
+ * utilized by the HeadersPanel.
+ * The default ObjectProvider can't be used since it doesn't
+ * allow duplicities by design and so it can't support duplicity
+ * headers (more headers with the same name).
+ */
+var HeadersProvider = {
+  ...ObjectProvider,
+
+  getChildren(object) {
+    if (object.value instanceof HeaderList) {
+      return object.value.headers.map((header, index) =>
+        new Header(header.name, header.value, index));
+    }
+    return ObjectProvider.getChildren(object);
+  },
+
+  hasChildren: function (object) {
+    if (object.value instanceof HeaderList) {
+      return object.value.headers.length > 0;
+    } else if (object instanceof Header) {
+      return false;
+    }
+    return ObjectProvider.hasChildren(object);
+  },
+
+  getLabel: function (object) {
+    if (object instanceof Header) {
+      return object.name;
+    }
+    return ObjectProvider.getLabel(object);
+  },
+
+  getValue: function (object) {
+    if (object instanceof Header) {
+      return object.value;
+    }
+    return ObjectProvider.getValue(object);
+  },
+
+  getKey(object) {
+    if (object instanceof Header) {
+      return object.key;
+    }
+    return ObjectProvider.getKey(object);
+  },
+
+  getType: function (object) {
+    if (object instanceof Header) {
+      return "string";
+    }
+    return ObjectProvider.getType(object);
+  }
+};
+
+/**
+ * Helper data structures for list of headers.
+ */
+function HeaderList(headers) {
+  this.headers = headers;
+  this.headers.sort((a, b) => {
+    return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
+  });
+}
+
+function Header(name, value, key) {
+  this.name = name;
+  this.value = value;
+  this.key = key;
+}
+
+module.exports = {
+  HeadersProvider,
+  HeaderList
+};
--- a/devtools/client/netmonitor/src/utils/moz.build
+++ b/devtools/client/netmonitor/src/utils/moz.build
@@ -8,16 +8,17 @@ DIRS += [
 ]
 
 DevToolsModules(
     'create-store.js',
     'filter-autocomplete-provider.js',
     'filter-predicates.js',
     'filter-text-utils.js',
     'format-utils.js',
+    'headers-provider.js',
     'l10n.js',
     'mdn-utils.js',
     'menu.js',
     'open-request-in-tab.js',
     'prefs.js',
     'request-utils.js',
     'sort-predicates.js',
     'sort-utils.js'
--- a/devtools/client/netmonitor/test/browser_net_headers_sorted.js
+++ b/devtools/client/netmonitor/test/browser_net_headers_sorted.js
@@ -1,15 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 /**
  * Tests if Request-Headers and Response-Headers are sorted in Headers tab.
+ * The test also verifies that headers with the same name and headers
+ * with an empty value are also displayed.
  */
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(SIMPLE_SJS);
   info("Starting test... ");
 
   let { document, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let {
@@ -30,17 +32,18 @@ add_task(function* () {
   yield waitUntil(() => {
     let request = getSortedRequests(store.getState()).get(0);
     return request.requestHeaders && request.responseHeaders;
   });
 
   info("Check if Request-Headers and Response-Headers are sorted");
   let expectedResponseHeaders = ["cache-control", "connection", "content-length",
                                  "content-type", "date", "expires", "foo-bar",
-                                 "pragma", "server", "set-cookie"];
+                                 "foo-bar", "foo-bar", "pragma", "server", "set-cookie",
+                                 "set-cookie"];
   let expectedRequestHeaders = ["Accept", "Accept-Encoding", "Accept-Language",
                                 "Cache-Control", "Connection", "Cookie", "Host",
                                 "Pragma", "Upgrade-Insecure-Requests", "User-Agent"];
 
   let labelCells = document.querySelectorAll(".treeLabelCell");
   let actualResponseHeaders = [];
   let actualRequestHeaders = [];
 
--- a/devtools/client/netmonitor/test/browser_net_simple-request-data.js
+++ b/devtools/client/netmonitor/test/browser_net_simple-request-data.js
@@ -169,19 +169,19 @@ function test() {
         let requestItem = getSortedRequests(store.getState()).get(0);
         return requestItem && requestItem.responseHeaders;
       });
 
       let requestItem = getSortedRequests(store.getState()).get(0);
 
       ok(requestItem.responseHeaders,
         "There should be a responseHeaders data available.");
-      is(requestItem.responseHeaders.headers.length, 10,
+      is(requestItem.responseHeaders.headers.length, 13,
         "The responseHeaders data has an incorrect |headers| property.");
-      is(requestItem.responseHeaders.headersSize, 330,
+      is(requestItem.responseHeaders.headersSize, 335,
         "The responseHeaders data has an incorrect |headersSize| property.");
 
       verifyRequestItemTarget(
         document,
         getDisplayedRequests(store.getState()),
         requestItem,
         "GET",
         SIMPLE_SJS
@@ -223,17 +223,17 @@ function test() {
       let requestItem = getSortedRequests(store.getState()).get(0);
 
       is(requestItem.httpVersion, "HTTP/1.1",
         "The httpVersion data has an incorrect value.");
       is(requestItem.status, "200",
         "The status data has an incorrect value.");
       is(requestItem.statusText, "Och Aye",
         "The statusText data has an incorrect value.");
-      is(requestItem.headersSize, 330,
+      is(requestItem.headersSize, 335,
         "The headersSize data has an incorrect value.");
 
       let requestListItem = document.querySelector(".request-list-item");
       requestListItem.scrollIntoView();
       let requestsListStatus = requestListItem.querySelector(".requests-list-status");
       EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
       await waitUntil(() => requestsListStatus.title);
 
@@ -256,17 +256,17 @@ function test() {
         return requestItem &&
                requestItem.transferredSize &&
                requestItem.contentSize &&
                requestItem.mimeType;
       });
 
       let requestItem = getSortedRequests(store.getState()).get(0);
 
-      is(requestItem.transferredSize, "342",
+      is(requestItem.transferredSize, "347",
         "The transferredSize data has an incorrect value.");
       is(requestItem.contentSize, "12",
         "The contentSize data has an incorrect value.");
       is(requestItem.mimeType, "text/plain; charset=utf-8",
         "The mimeType data has an incorrect value.");
 
       verifyRequestItemTarget(
         document,
--- a/devtools/client/netmonitor/test/sjs_simple-test-server.sjs
+++ b/devtools/client/netmonitor/test/sjs_simple-test-server.sjs
@@ -3,15 +3,19 @@
 
 function handleRequest(request, response) {
   response.setStatusLine(request.httpVersion, 200, "Och Aye");
 
   response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
   response.setHeader("Pragma", "no-cache");
   response.setHeader("Expires", "0");
 
-  response.setHeader("Set-Cookie", "bob=true; Max-Age=10; HttpOnly", true);
-  response.setHeader("Set-Cookie", "tom=cool; Max-Age=10; HttpOnly", true);
+  response.setHeaderNoCheck("Set-Cookie", "bob=true; Max-Age=10; HttpOnly");
+  response.setHeaderNoCheck("Set-Cookie", "tom=cool; Max-Age=10; HttpOnly");
 
   response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
-  response.setHeader("Foo-Bar", "baz", false);
+
+  response.setHeaderNoCheck("Foo-Bar", "baz");
+  response.setHeaderNoCheck("Foo-Bar", "baz");
+  response.setHeaderNoCheck("Foo-Bar", "");
+
   response.write("Hello world!");
 }
--- a/devtools/client/shared/components/tree/TreeView.js
+++ b/devtools/client/shared/components/tree/TreeView.js
@@ -58,17 +58,17 @@ define(function (require, exports, modul
    *   renderValue: function(object, colId);
    *   renderRow: function(object);
    *   renderCell: function(object, colId);
    *   renderLabelCell: function(object);
    * }
    */
   class TreeView extends Component {
     // The only required property (not set by default) is the input data
-    // object that is used to puputate the tree.
+    // object that is used to populate the tree.
     static get propTypes() {
       return {
         // The input data object.
         object: PropTypes.any,
         className: PropTypes.string,
         label: PropTypes.string,
         // Data provider (see also the interface above)
         provider: PropTypes.shape({
@@ -88,17 +88,17 @@ define(function (require, exports, modul
           renderRow: PropTypes.func,
           renderCell: PropTypes.func,
           renderLabelCell: PropTypes.func,
         }),
         // Custom tree row (node) renderer
         renderRow: PropTypes.func,
         // Custom cell renderer
         renderCell: PropTypes.func,
-        // Custom value renderef
+        // Custom value renderer
         renderValue: PropTypes.func,
         // Custom tree label (including a toggle button) renderer
         renderLabelCell: PropTypes.func,
         // Set of expanded nodes
         expandedNodes: PropTypes.object,
         // Custom filtering callback
         onFilter: PropTypes.func,
         // Custom sorting callback
--- a/devtools/shared/webconsole/network-monitor.js
+++ b/devtools/shared/webconsole/network-monitor.js
@@ -874,35 +874,38 @@ NetworkMonitor.prototype = {
 
     let response = {
       id: gSequenceId(),
       channel: channel,
       headers: [],
       cookies: [],
     };
 
-    let setCookieHeader = null;
+    let setCookieHeaders = [];
 
-    channel.visitResponseHeaders({
+    channel.visitOriginalResponseHeaders({
       visitHeader: function (name, value) {
         let lowerName = name.toLowerCase();
         if (lowerName == "set-cookie") {
-          setCookieHeader = value;
+          setCookieHeaders.push(value);
         }
         response.headers.push({ name: name, value: value });
       }
     });
 
     if (!response.headers.length) {
       // No need to continue.
       return;
     }
 
-    if (setCookieHeader) {
-      response.cookies = NetworkHelper.parseSetCookieHeader(setCookieHeader);
+    if (setCookieHeaders.length) {
+      response.cookies = setCookieHeaders.reduce((result, header) => {
+        let cookies = NetworkHelper.parseSetCookieHeader(header);
+        return result.concat(cookies);
+      }, []);
     }
 
     // Determine the HTTP version.
     let httpVersionMaj = {};
     let httpVersionMin = {};
 
     channel.QueryInterface(Ci.nsIHttpChannelInternal);
     channel.getResponseVersion(httpVersionMaj, httpVersionMin);