Bug 1404917 - Fetch response content only on-demand. r=Honza
MozReview-Commit-ID: CtpJ8PKsCsm
--- a/devtools/client/netmonitor/src/components/response-panel.js
+++ b/devtools/client/netmonitor/src/components/response-panel.js
@@ -8,16 +8,18 @@ const {
createClass,
createFactory,
DOM,
PropTypes,
} = require("devtools/client/shared/vendor/react");
const { L10N } = require("../utils/l10n");
const { formDataURI, getUrlBaseName } = require("../utils/request-utils");
+const { getResponseContent } = require("../connector/index");
+
// Components
const PropertiesView = createFactory(require("./properties-view"));
const { div, img } = DOM;
const JSON_SCOPE_NAME = L10N.getStr("jsonScopeName");
const JSON_FILTER_TEXT = L10N.getStr("jsonFilterText");
const RESPONSE_IMG_NAME = L10N.getStr("netmonitor.response.name");
const RESPONSE_IMG_DIMENSIONS = L10N.getStr("netmonitor.response.dimensions");
@@ -105,16 +107,30 @@ const ResponsePanel = createClass({
}
return result;
}
return null;
},
+ // componentWillReceiveProps is the only method called when switching to the
+ // ResponsePanel *and* when being on it and switching between two requests.
+ componentWillReceiveProps(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.
+ if (!nextProps.request.responseContent ||
+ !nextProps.request.responseContent.content) {
+ // This method will set `props.request.responseContent.content`
+ // asynchronously and introduce another render.
+ getResponseContent(nextProps.request.id);
+ }
+ },
+
render() {
let { openLink, request } = this.props;
let { responseContent, url } = request;
if (!responseContent || typeof responseContent.content.text !== "string") {
return null;
}
--- a/devtools/client/netmonitor/src/connector/firefox-connector.js
+++ b/devtools/client/netmonitor/src/connector/firefox-connector.js
@@ -288,11 +288,15 @@ class FirefoxConnector {
* @param {string} sourceURL source url
* @param {number} sourceLine source line number
*/
viewSourceInDebugger(sourceURL, sourceLine) {
if (this.toolbox) {
this.toolbox.viewSourceInDebugger(sourceURL, sourceLine);
}
}
+
+ getResponseContent(request) {
+ return this.dataProvider.getResponseContent(request);
+ }
}
module.exports = new FirefoxConnector();
--- a/devtools/client/netmonitor/src/connector/firefox-data-provider.js
+++ b/devtools/client/netmonitor/src/connector/firefox-data-provider.js
@@ -16,16 +16,18 @@ const { fetchHeaders, formDataURI } = re
class FirefoxDataProvider {
constructor({webConsoleClient, actions}) {
// Options
this.webConsoleClient = webConsoleClient;
this.actions = actions;
// Internal properties
this.payloadQueue = [];
+ // Map of [request id => Promise], used by getResponseContent
+ this.pendingResponseContentRequests = new Map();
// Public methods
this.addRequest = this.addRequest.bind(this);
this.updateRequest = this.updateRequest.bind(this);
// Internals
this.fetchImage = this.fetchImage.bind(this);
this.fetchRequestHeaders = this.fetchRequestHeaders.bind(this);
@@ -43,17 +45,16 @@ class FirefoxDataProvider {
this.onNetworkEvent = this.onNetworkEvent.bind(this);
this.onNetworkEventUpdate = this.onNetworkEventUpdate.bind(this);
this.onRequestHeaders = this.onRequestHeaders.bind(this);
this.onRequestCookies = this.onRequestCookies.bind(this);
this.onRequestPostData = this.onRequestPostData.bind(this);
this.onSecurityInfo = this.onSecurityInfo.bind(this);
this.onResponseHeaders = this.onResponseHeaders.bind(this);
this.onResponseCookies = this.onResponseCookies.bind(this);
- this.onResponseContent = this.onResponseContent.bind(this);
this.onEventTimings = this.onEventTimings.bind(this);
}
/**
* Add a new network request to application state.
*
* @param {string} id request id
* @param {object} data data payload will be added to application state
@@ -128,18 +129,22 @@ class FirefoxDataProvider {
postDataObj,
requestCookiesObj,
responseCookiesObj
);
this.pushPayloadToQueue(id, payload);
if (this.actions.updateRequest && this.isQueuePayloadReady(id)) {
- await this.actions.updateRequest(id, this.getPayloadFromQueue(id).payload, true);
+ let payload = this.getPayloadFromQueue(id).payload;
+ await this.actions.updateRequest(id, payload, true);
+ return payload;
}
+
+ return payload;
}
async fetchImage(mimeType, responseContent) {
let payload = {};
if (mimeType && responseContent && responseContent.content) {
let { encoding, text } = responseContent.content;
let response = await this.getLongString(text);
@@ -301,16 +306,49 @@ class FirefoxDataProvider {
* A promise that is resolved when the full string contents
* are available, or rejected if something goes wrong.
*/
getLongString(stringGrip) {
return this.webConsoleClient.getString(stringGrip);
}
/**
+ * Fetches the response content of a given request.
+ *
+ * @param {string} id request id
+ */
+ async getResponseContent(id) {
+ // Return the response if that's already fetched
+ let queuedPayload = this.getPayloadFromQueue(id);
+ if (queuedPayload && queuedPayload.payload.responseContent) {
+ return queuedPayload.payload.responseContent;
+ }
+ // Prevent fetching the response more than once at a time
+ // in case this method is called multiple times in a raw.
+ if (this.pendingResponseContentRequests.has(id)) {
+ return this.pendingResponseContentRequests.get(id);
+ }
+ let promise = this.webConsoleClient.getResponseContent(id)
+ .then(async response => {
+ // We have to pass mimeType in order to ensure calling fetchImage in updateRequest
+ // and have the LongString in `response.content.text` converted to text.
+ let payload = await this.updateRequest(id, {
+ responseContent: response,
+ mimeType: response.content.mimeType
+ });
+ this.pendingResponseContentRequests.delete(id);
+ emit(EVENTS.RECEIVED_RESPONSE_CONTENT, id);
+ return payload.responseContent;
+ });
+ this.pendingResponseContentRequests.set(id, promise);
+ emit(EVENTS.UPDATING_RESPONSE_CONTENT, id);
+ return promise;
+ }
+
+ /**
* The "networkEvent" message type handler.
*
* @param {string} type message type
* @param {object} networkInfo network request information
*/
onNetworkEvent(type, networkInfo) {
let {
actor,
@@ -385,23 +423,21 @@ class FirefoxDataProvider {
status: networkInfo.response.status,
statusText: networkInfo.response.statusText,
headersSize: networkInfo.response.headersSize
}).then(() => {
emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
});
break;
case "responseContent":
- this.webConsoleClient.getResponseContent(actor,
- this.onResponseContent.bind(this, {
- contentSize: networkInfo.response.bodySize,
- transferredSize: networkInfo.response.transferredSize,
- mimeType: networkInfo.response.content.mimeType
- }));
- emit(EVENTS.UPDATING_RESPONSE_CONTENT, actor);
+ this.updateRequest(actor, {
+ contentSize: networkInfo.response.bodySize,
+ transferredSize: networkInfo.response.transferredSize,
+ mimeType: networkInfo.response.content.mimeType
+ });
break;
case "eventTimings":
this.updateRequest(actor, { totalTime: networkInfo.totalTime })
.then(() => {
this.webConsoleClient.getEventTimings(actor, this.onEventTimings);
emit(EVENTS.UPDATING_EVENT_TIMINGS, actor);
});
break;
@@ -482,29 +518,16 @@ class FirefoxDataProvider {
this.updateRequest(response.from, {
responseCookies: response
}).then(() => {
emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
});
}
/**
- * Handles additional information received for a "responseContent" packet.
- *
- * @param {object} data the message received from the server event.
- * @param {object} response the message received from the server.
- */
- onResponseContent(data, response) {
- let payload = Object.assign({ responseContent: response }, data);
- this.updateRequest(response.from, payload).then(() => {
- emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
- });
- }
-
- /**
* Handles additional information received for a "eventTimings" packet.
*
* @param {object} response the message received from the server.
*/
onEventTimings(response) {
this.updateRequest(response.from, {
eventTimings: response
}).then(() => {
--- a/devtools/client/netmonitor/src/connector/index.js
+++ b/devtools/client/netmonitor/src/connector/index.js
@@ -64,22 +64,27 @@ function setPreferences() {
function triggerActivity() {
return connector.triggerActivity(...arguments);
}
function viewSourceInDebugger() {
return connector.viewSourceInDebugger(...arguments);
}
+function getResponseContent() {
+ return connector.getResponseContent(...arguments);
+}
+
module.exports = {
onConnect,
onChromeConnect,
onFirefoxConnect,
onDisconnect,
getLongString,
getNetworkRequest,
getTabTarget,
inspectRequest,
sendHTTPRequest,
setPreferences,
triggerActivity,
viewSourceInDebugger,
+ getResponseContent,
};
--- a/devtools/client/netmonitor/src/request-list-context-menu.js
+++ b/devtools/client/netmonitor/src/request-list-context-menu.js
@@ -8,16 +8,17 @@ 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 { HarExporter } = require("./har/har-exporter");
const {
getLongString,
getTabTarget,
+ getResponseContent,
} = require("./connector/index");
const {
getSelectedRequest,
getSortedRequests,
} = require("./selectors/index");
const { L10N } = require("./utils/l10n");
const { showMenu } = require("devtools/client/netmonitor/src/utils/menu");
const {
@@ -109,20 +110,17 @@ RequestListContextMenu.prototype = {
visible: !!(selectedRequest && selectedRequest.responseHeaders),
click: () => this.copyResponseHeaders(),
});
copySubmenu.push({
id: "request-list-context-copy-response",
label: L10N.getStr("netmonitor.context.copyResponse"),
accesskey: L10N.getStr("netmonitor.context.copyResponse.accesskey"),
- visible: !!(selectedRequest &&
- selectedRequest.responseContent &&
- selectedRequest.responseContent.content.text &&
- selectedRequest.responseContent.content.text.length !== 0),
+ visible: !!selectedRequest,
click: () => this.copyResponse(),
});
copySubmenu.push({
id: "request-list-context-copy-image-as-data-uri",
label: L10N.getStr("netmonitor.context.copyImageAsDataUri"),
accesskey: L10N.getStr("netmonitor.context.copyImageAsDataUri.accesskey"),
visible: !!(selectedRequest &&
@@ -333,25 +331,27 @@ RequestListContextMenu.prototype = {
rawHeaders = rawHeaders.replace(/\r/g, "");
}
copyString(rawHeaders);
},
/**
* Copy image as data uri.
*/
- copyImageAsDataUri() {
+ async copyImageAsDataUri() {
+ await getResponseContent(this.selectedRequest.id);
copyString(this.selectedRequest.responseContentDataUri);
},
/**
* Save image as.
*/
- saveImageAs() {
- let { encoding, text } = this.selectedRequest.responseContent.content;
+ async saveImageAs() {
+ let responseContent = await getResponseContent(this.selectedRequest.id);
+ let { encoding, text } = responseContent.content;
let fileName = getUrlBaseName(this.selectedRequest.url);
let data;
if (encoding === "base64") {
let decoded = atob(text);
data = new Uint8Array(decoded.length);
for (let i = 0; i < decoded.length; ++i) {
data[i] = decoded.charCodeAt(i);
}
@@ -359,18 +359,19 @@ RequestListContextMenu.prototype = {
data = text;
}
saveAs(new Blob([data]), fileName, document);
},
/**
* Copy response data as a string.
*/
- copyResponse() {
- copyString(this.selectedRequest.responseContent.content.text);
+ async copyResponse() {
+ let responseContent = await getResponseContent(this.selectedRequest.id);
+ copyString(responseContent.content.text);
},
/**
* Copy HAR from the network panel content to the clipboard.
*/
copyAllAsHar() {
return HarExporter.copy(this.getDefaultHarOptions());
},