diff --git a/devtools/client/netmonitor/src/actions/requests.js b/devtools/client/netmonitor/src/actions/requests.js index b5cfe9fa4ed12..640b568cc81da 100644 --- a/devtools/client/netmonitor/src/actions/requests.js +++ b/devtools/client/netmonitor/src/actions/requests.js @@ -122,10 +122,13 @@ function sendCustomRequest(connector, requestId = null) { data.body = request.requestPostData.postData.text; } - const response = await connector.sendHTTPRequest(data); + // @backward-compat { version 85 } Introduced `channelId` to eventually + // replace `actor`. + const { channelId, actor } = await connector.sendHTTPRequest(data); + dispatch({ type: SEND_CUSTOM_REQUEST, - id: response.eventActor.actor, + id: channelId || actor, }); }; } diff --git a/devtools/client/netmonitor/src/connector/firefox-connector.js b/devtools/client/netmonitor/src/connector/firefox-connector.js index 9203c60988d4f..655d9e8e6902b 100644 --- a/devtools/client/netmonitor/src/connector/firefox-connector.js +++ b/devtools/client/netmonitor/src/connector/firefox-connector.js @@ -391,8 +391,18 @@ class FirefoxConnector { * * @param {object} data data payload would like to sent to backend */ - sendHTTPRequest(data) { - return this.webConsoleFront.sendHTTPRequest(data); + async sendHTTPRequest(data) { + if (this.hasResourceWatcherSupport && this.currentTarget) { + const networkContentFront = await this.currentTarget.getFront( + "networkContent" + ); + const { channelId } = await networkContentFront.sendHTTPRequest(data); + return { channelId }; + } + const { + eventActor: { actor }, + } = await this.webConsoleFront.sendHTTPRequest(data); + return { actor }; } /** diff --git a/devtools/client/netmonitor/src/reducers/requests.js b/devtools/client/netmonitor/src/reducers/requests.js index c5fab4cb7bfd8..7c9d8f6a9dadb 100644 --- a/devtools/client/netmonitor/src/reducers/requests.js +++ b/devtools/client/netmonitor/src/reducers/requests.js @@ -33,6 +33,8 @@ function Requests() { requests: [], // Selected request ID selectedId: null, + // @backward-compact { version 85 } The preselectedId can either be + // the actor id on old servers, or the resourceId on new ones. preselectedId: null, // True if the monitor is recording HTTP traffic recording: true, @@ -171,8 +173,16 @@ function addRequest(state, action) { } // Select the request if it was preselected and there is no other selection. - if (state.preselectedId && state.preselectedId === action.id) { - nextState.selectedId = state.selectedId || state.preselectedId; + if (state.preselectedId) { + if (state.preselectedId === action.id) { + nextState.selectedId = state.selectedId || state.preselectedId; + } + // @backward-compact { version 85 } The preselectedId can be resourceId + // instead of actor id when a custom request is created, and could not be + // selected immediately because it was not yet in the request map. + else if (state.preselectedId === newRequest.resourceId) { + nextState.selectedId = action.id; + } nextState.preselectedId = null; } @@ -272,12 +282,18 @@ function closeCustomRequest(state) { return state; } + // Find the cloned requests to be removed const removedRequest = requests.find(needle => needle.id === selectedId); // If the custom request is already in the Map, select it immediately, // and reset `preselectedId` attribute. - const hasPreselectedId = - preselectedId && requests.find(needle => needle.id === preselectedId); + // @backward-compact { version 85 } The preselectId can also be a resourceId + // or an actor id. + const customRequest = requests.find( + needle => needle.id === preselectedId || needle.resourceId === preselectedId + ); + const hasPreselectedId = preselectedId && customRequest; + return { ...state, // Only custom requests can be removed @@ -285,7 +301,7 @@ function closeCustomRequest(state) { item => item.id !== selectedId ), preselectedId: hasPreselectedId ? null : preselectedId, - selectedId: hasPreselectedId ? preselectedId : null, + selectedId: hasPreselectedId ? customRequest.id : null, }; } diff --git a/devtools/client/netmonitor/test/browser_net_resend_xhr.js b/devtools/client/netmonitor/test/browser_net_resend_xhr.js index 6f56a3b28a12f..258962bf2662f 100644 --- a/devtools/client/netmonitor/test/browser_net_resend_xhr.js +++ b/devtools/client/netmonitor/test/browser_net_resend_xhr.js @@ -29,11 +29,12 @@ add_task(async function() { ); const originalRequest = getSelectedRequest(store.getState()); + const waitForResentRequestEvent = waitForNetworkEvents(monitor, 1); // Context Menu > "Resend" EventUtils.sendMouseEvent({ type: "contextmenu" }, firstRequest); getContextMenuItem(monitor, "request-list-context-resend-only").click(); - await performRequests(monitor, tab, 2); + await waitForResentRequestEvent; // Selects request that was resent const selectedRequest = getSelectedRequest(store.getState()); diff --git a/devtools/server/actors/network-monitor/network-content.js b/devtools/server/actors/network-monitor/network-content.js index 45ac6529498f8..8704a19504379 100644 --- a/devtools/server/actors/network-monitor/network-content.js +++ b/devtools/server/actors/network-monitor/network-content.js @@ -7,6 +7,22 @@ const { ActorClassWithSpec, Actor } = require("devtools/shared/protocol"); const { networkContentSpec } = require("devtools/shared/specs/network-content"); +const { Cc, Ci } = require("chrome"); + +loader.lazyRequireGetter( + this, + "NetUtil", + "resource://gre/modules/NetUtil.jsm", + true +); + +loader.lazyRequireGetter( + this, + "stringToCauseType", + "devtools/server/actors/network-monitor/network-observer", + true +); + loader.lazyRequireGetter( this, "WebConsoleUtils", @@ -36,21 +52,91 @@ const NetworkContentActor = ActorClassWithSpec(networkContentSpec, { Actor.prototype.destroy.call(this, conn); }, + get networkEventStackTraceWatcher() { + return getResourceWatcher(this.targetActor, NETWORK_EVENT_STACKTRACE); + }, + /** - * The "getStackTrace" packet type handler. + * Send an HTTP request * - * @return object + * @param {Object} request + * The details of the HTTP Request. + * @return {Number} + * The channel id for the request + */ + async sendHTTPRequest(request) { + const { url, method, headers, body, cause } = request; + // Set the loadingNode and loadGroup to the target document - otherwise the + // request won't show up in the opened netmonitor. + const doc = this.targetActor.window.document; + + const channel = NetUtil.newChannel({ + uri: NetUtil.newURI(url), + loadingNode: doc, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: + stringToCauseType(cause.type) || Ci.nsIContentPolicy.TYPE_OTHER, + }); + + channel.QueryInterface(Ci.nsIHttpChannel); + + channel.loadGroup = doc.documentLoadGroup; + channel.loadFlags |= + Ci.nsIRequest.LOAD_BYPASS_CACHE | + Ci.nsIRequest.INHIBIT_CACHING | + Ci.nsIRequest.LOAD_ANONYMOUS; + + channel.requestMethod = method; + if (headers) { + for (const { name, value } of headers) { + if (name.toLowerCase() == "referer") { + // The referer header and referrerInfo object should always match. So + // if we want to set the header from privileged context, we should set + // referrerInfo. The referrer header will get set internally. + channel.setNewReferrerInfo( + value, + Ci.nsIReferrerInfo.UNSAFE_URL, + true + ); + } else { + channel.setRequestHeader(name, value, false); + } + } + } + + if (body) { + channel.QueryInterface(Ci.nsIUploadChannel2); + const bodyStream = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + bodyStream.setData(body, body.length); + channel.explicitSetUploadStream(bodyStream, null, -1, method, false); + } + + return new Promise(resolve => { + // Make sure the fetch has completed before sending the channel id, + // so that there is a higher possibilty that the request get into the + // redux store beforehand (but this does not gurantee that). + NetUtil.asyncFetch(channel, () => + resolve({ channelId: channel.channelId }) + ); + }); + }, + + /** + * Gets the stacktrace for the specified network resource. + * @param {Number} resourceId + * The id for the network resource + * @return {Object} * The response packet - stack trace. */ getStackTrace(resourceId) { - const networkEventStackTraceWatcher = getResourceWatcher( - this.targetActor, - NETWORK_EVENT_STACKTRACE - ); - if (!networkEventStackTraceWatcher) { + if (!this.networkEventStackTraceWatcher) { throw new Error("Not listening for network event stacktraces"); } - const stacktrace = networkEventStackTraceWatcher.getStackTrace(resourceId); + const stacktrace = this.networkEventStackTraceWatcher.getStackTrace( + resourceId + ); return { stacktrace: WebConsoleUtils.removeFramesAboveDebuggerEval(stacktrace), }; diff --git a/devtools/server/actors/network-monitor/network-event-actor.js b/devtools/server/actors/network-monitor/network-event-actor.js index da75043e7276d..86bb7b9ea96d6 100644 --- a/devtools/server/actors/network-monitor/network-event-actor.js +++ b/devtools/server/actors/network-monitor/network-event-actor.js @@ -97,7 +97,8 @@ const NetworkEventActor = protocol.ActorClassWithSpec(networkEventSpec, { asResource() { return { resourceType: NETWORK_EVENT, - // The browsingContextID is used by the ResourceWatcher on the client to find the related Target Front. + // The browsingContextID is used by the ResourceWatcher on the client + // to find the related Target Front. browsingContextID: this.networkEventWatcher.watcherActor.browserElement .browsingContext.id, resourceId: this._channelId, diff --git a/devtools/server/actors/resources/network-events.js b/devtools/server/actors/resources/network-events.js index e875521ae7981..f5763ae03cfb4 100644 --- a/devtools/server/actors/resources/network-events.js +++ b/devtools/server/actors/resources/network-events.js @@ -23,7 +23,8 @@ class NetworkEventWatcher { * Start watching for all network events related to a given Watcher Actor. * * @param WatcherActor watcherActor - * The watcher actor from which we should observe network events + * The watcher actor in the parent process from which we should + * observe network events. * @param Object options * Dictionary object with following attributes: * - onAvailable: mandatory function @@ -91,7 +92,7 @@ class NetworkEventWatcher { onNetworkEvent(event) { const { channelId } = event; - if (this.networkEvents.get(channelId)) { + if (this.networkEvents.has(channelId)) { throw new Error( `Got notified about channel ${channelId} more than once.` ); diff --git a/devtools/shared/specs/network-content.js b/devtools/shared/specs/network-content.js index b0aafe7039a82..cf612243d82a7 100644 --- a/devtools/shared/specs/network-content.js +++ b/devtools/shared/specs/network-content.js @@ -9,6 +9,12 @@ const { generateActorSpec, RetVal, Arg } = require("devtools/shared/protocol"); const networkContentSpec = generateActorSpec({ typeName: "networkContent", methods: { + sendHTTPRequest: { + request: { + request: Arg(0, "json"), + }, + response: RetVal("number"), + }, getStackTrace: { request: { resourceId: Arg(0) }, // stacktrace is an "array:string", but not always.