commit 53654d78bd6fb7fbc11490a226139db47d042e85
parent 532a5614499d6677045daf216b94b29b4f5a2117
Author: Julian Descottes <jdescottes@mozilla.com>
Date: Wed, 5 Nov 2025 16:17:32 +0000
Bug 1988955 - [bidi] Add support for dataType=request to network data collection r=whimboo
Differential Revision: https://phabricator.services.mozilla.com/D267227
Diffstat:
5 files changed, 256 insertions(+), 72 deletions(-)
diff --git a/remote/jar.mn b/remote/jar.mn
@@ -27,6 +27,7 @@ remote.jar:
content/shared/Navigate.sys.mjs (shared/Navigate.sys.mjs)
content/shared/NavigationManager.sys.mjs (shared/NavigationManager.sys.mjs)
content/shared/NetworkCacheManager.sys.mjs (shared/NetworkCacheManager.sys.mjs)
+ content/shared/NetworkDataBytes.sys.mjs (shared/NetworkDataBytes.sys.mjs)
content/shared/NetworkDecodedBodySizeMap.sys.mjs (shared/NetworkDecodedBodySizeMap.sys.mjs)
content/shared/NetworkRequest.sys.mjs (shared/NetworkRequest.sys.mjs)
content/shared/NetworkResponse.sys.mjs (shared/NetworkResponse.sys.mjs)
diff --git a/remote/shared/NetworkDataBytes.sys.mjs b/remote/shared/NetworkDataBytes.sys.mjs
@@ -0,0 +1,35 @@
+/* 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/. */
+
+export class NetworkDataBytes {
+ #getBytesValue;
+ #isBase64;
+
+ /**
+ * Common interface used to handle network BytesValue for collected data which
+ * might be encoded and require an additional async step in order to retrieve
+ * the actual bytes.
+ *
+ * This is a simple wrapper mostly designed to ensure a common interface in
+ * case this is used for request or response bodies.
+ *
+ * @param {object} options
+ * @param {Function} options.getBytesValue
+ * A -potentially async- callable which returns the bytes as a string.
+ * @param {boolean} options.isBase64
+ * Whether this represents a base64-encoded binary data.
+ */
+ constructor(options) {
+ this.#getBytesValue = options.getBytesValue;
+ this.#isBase64 = options.isBase64;
+ }
+
+ get isBase64() {
+ return this.#isBase64;
+ }
+
+ async getBytesValue() {
+ return this.#getBytesValue();
+ }
+}
diff --git a/remote/shared/NetworkRequest.sys.mjs b/remote/shared/NetworkRequest.sys.mjs
@@ -12,6 +12,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs",
NavigationState: "chrome://remote/content/shared/NavigationManager.sys.mjs",
+ NetworkDataBytes: "chrome://remote/content/shared/NetworkDataBytes.sys.mjs",
notifyNavigationStarted:
"chrome://remote/content/shared/NavigationManager.sys.mjs",
});
@@ -29,6 +30,7 @@ export class NetworkRequest {
#isDataURL;
#navigationId;
#navigationManager;
+ #postData;
#postDataSize;
#rawHeaders;
#redirectCount;
@@ -97,9 +99,10 @@ export class NetworkRequest {
this.#contextId = this.#getContextId();
this.#navigationId = this.#getNavigationId();
- // The postDataSize will no longer be available after the channel is closed.
- // Compute and cache the value, to be updated when `setRequestBody` is used.
- this.#postDataSize = this.#computePostDataSize();
+ // The postData will no longer be available after the channel is closed.
+ // Compute the postData and postDataSize properties, to be updated later if
+ // `setRequestBody` is used.
+ this.#updatePostData();
}
get alreadyCompleted() {
@@ -159,6 +162,10 @@ export class NetworkRequest {
return this.#navigationId;
}
+ get postData() {
+ return this.#postData;
+ }
+
get postDataSize() {
return this.#postDataSize;
}
@@ -217,6 +224,19 @@ export class NetworkRequest {
}
/**
+ * Returns the NetworkDataBytes instance representing the request body for
+ * this request.
+ *
+ * @returns {NetworkDataBytes}
+ */
+ readAndProcessRequestBody = () => {
+ return new lazy.NetworkDataBytes({
+ getBytesValue: () => this.#postData.text,
+ isBase64: this.#postData.isBase64,
+ });
+ };
+
+ /**
* Redirect the request to another provided URL.
*
* @param {string} url
@@ -253,7 +273,7 @@ export class NetworkRequest {
} finally {
// Make sure to reset the flag once the modification was attempted.
this.#channel.requestObserversCalled = true;
- this.#postDataSize = this.#computePostDataSize();
+ this.#updatePostData();
}
}
@@ -337,6 +357,7 @@ export class NetworkRequest {
initiatorType: this.initiatorType,
method: this.method,
navigationId: this.navigationId,
+ postData: this.postData,
postDataSize: this.postDataSize,
redirectCount: this.redirectCount,
requestId: this.requestId,
@@ -348,15 +369,6 @@ export class NetworkRequest {
};
}
- #computePostDataSize() {
- const charset = lazy.NetworkUtils.getCharset(this.#channel);
- const sentBody = lazy.NetworkHelper.readPostTextFromRequest(
- this.#channel,
- charset
- );
- return sentBody ? sentBody.length : 0;
- }
-
/**
* Convert the provided request timing to a timing relative to the beginning
* of the request. Note that https://w3c.github.io/resource-timing/#dfn-convert-fetch-timestamp
@@ -517,4 +529,31 @@ export class NetworkRequest {
);
return !browsingContext.parent;
}
+
+ #readPostDataFromRequestAsUTF8() {
+ const postData = lazy.NetworkHelper.readPostDataFromRequest(
+ this.#channel,
+ "UTF-8"
+ );
+
+ if (postData === null || postData.data === null) {
+ return null;
+ }
+
+ return {
+ text: postData.isDecodedAsText ? postData.data : btoa(postData.data),
+ isBase64: !postData.isDecodedAsText,
+ };
+ }
+
+ #updatePostData() {
+ const sentBody = this.#readPostDataFromRequestAsUTF8();
+ if (sentBody) {
+ this.#postData = sentBody;
+ this.#postDataSize = sentBody.text.length;
+ } else {
+ this.#postData = null;
+ this.#postDataSize = 0;
+ }
+ }
}
diff --git a/remote/shared/NetworkResponse.sys.mjs b/remote/shared/NetworkResponse.sys.mjs
@@ -6,6 +6,8 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
NetworkUtils:
"resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
+
+ NetworkDataBytes: "chrome://remote/content/shared/NetworkDataBytes.sys.mjs",
});
/**
@@ -157,34 +159,37 @@ export class NetworkResponse {
);
}
- async readResponseBody() {
- return this.#responseBodyReady.promise;
- }
+ /**
+ * Returns the NetworkDataBytes instance representing the response body for
+ * this response.
+ *
+ * @returns {NetworkDataBytes}
+ */
+ readAndProcessResponseBody = async () => {
+ const responseContent = await this.#responseBodyReady.promise;
+
+ return new lazy.NetworkDataBytes({
+ getBytesValue: async () => {
+ if (responseContent.isContentEncoded) {
+ return lazy.NetworkUtils.decodeResponseChunks(
+ responseContent.encodedData,
+ {
+ // Should always attempt to decode as UTF-8.
+ charset: "UTF-8",
+ compressionEncodings: responseContent.compressionEncodings,
+ encodedBodySize: responseContent.encodedBodySize,
+ encoding: responseContent.encoding,
+ }
+ );
+ }
+ return responseContent.text;
+ },
+ isBase64: responseContent.encoding === "base64",
+ });
+ };
setResponseContent(responseContent) {
- // Extract the properties necessary to decode the response body later on.
- let encodedResponseBody;
-
- if (responseContent.isContentEncoded) {
- encodedResponseBody = {
- encoding: responseContent.encoding,
- getDecodedResponseBody: async () =>
- lazy.NetworkUtils.decodeResponseChunks(responseContent.encodedData, {
- // Should always attempt to decode as UTF-8.
- charset: "UTF-8",
- compressionEncodings: responseContent.compressionEncodings,
- encodedBodySize: responseContent.encodedBodySize,
- encoding: responseContent.encoding,
- }),
- };
- } else {
- encodedResponseBody = {
- encoding: responseContent.encoding,
- getDecodedResponseBody: () => responseContent.text,
- };
- }
-
- this.#responseBodyReady.resolve(encodedResponseBody);
+ this.#responseBodyReady.resolve(responseContent);
}
/**
diff --git a/remote/webdriver-bidi/modules/root/network.sys.mjs b/remote/webdriver-bidi/modules/root/network.sys.mjs
@@ -20,6 +20,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
matchURLPattern:
"chrome://remote/content/shared/webdriver/URLPattern.sys.mjs",
NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs",
+ NetworkDataBytes: "chrome://remote/content/shared/NetworkDataBytes.sys.mjs",
NetworkDecodedBodySizeMap:
"chrome://remote/content/shared/NetworkDecodedBodySizeMap.sys.mjs",
NetworkListener:
@@ -161,6 +162,7 @@ const ContinueWithAuthAction = {
* @enum {DataType}
*/
const DataType = {
+ Request: "request",
Response: "response",
};
@@ -1283,11 +1285,10 @@ class NetworkModule extends RootBiDiModule {
);
}
- const value = await collectedData.bytes.getDecodedResponseBody();
- const type =
- collectedData.bytes.encoding === "base64"
- ? BytesValueType.Base64
- : BytesValueType.String;
+ const value = await collectedData.bytes.getBytesValue();
+ const type = collectedData.bytes.isBase64
+ ? BytesValueType.Base64
+ : BytesValueType.String;
if (disown) {
this.#removeCollectorFromData(collectedData, collector);
@@ -1918,6 +1919,37 @@ class NetworkModule extends RootBiDiModule {
}
}
+ #cloneNetworkRequestBody(request) {
+ if (!this.#networkCollectors.size) {
+ return;
+ }
+
+ // If request body is missing or null, do not store any collected data.
+ if (!request.postData || request.postData === null) {
+ return;
+ }
+
+ const collectedData = {
+ bytes: null,
+ collectors: new Set(),
+ pending: true,
+ // This allows to implement the await/resume on "network data collected"
+ // described in the specification.
+ networkDataCollected: Promise.withResolvers(),
+ request: request.requestId,
+ size: null,
+ type: DataType.Request,
+ };
+
+ // The actual cloning is already handled by the DevTools
+ // NetworkResponseListener, here we just have to prepare the networkData and
+ // add it to the array.
+ this.#collectedNetworkData.set(
+ `${request.requestId}-${DataType.Request}`,
+ collectedData
+ );
+ }
+
#cloneNetworkResponseBody(request) {
if (!this.#networkCollectors.size) {
return;
@@ -1925,7 +1957,6 @@ class NetworkModule extends RootBiDiModule {
const collectedData = {
bytes: null,
- // Note: The specification expects a `clonedBody` property on
// The cloned body is fully handled by DevTools' NetworkResponseListener
// so it will not explicitly be stored here.
collectors: new Set(),
@@ -2287,7 +2318,32 @@ class NetworkModule extends RootBiDiModule {
}
/**
- * Implements https://w3c.github.io/webdriver-bidi/#maybe-collect-network-response-body
+ * Implements https://w3c.github.io/webdriver-bidi/#maybe-collect-network-request-body
+ *
+ * @param {NetworkRequest} request
+ * The request object for which we want to collect the body.
+ */
+ async #maybeCollectNetworkRequestBody(request) {
+ const collectedData = this.#getCollectedData(
+ request.requestId,
+ DataType.Request
+ );
+
+ if (collectedData === null) {
+ return;
+ }
+
+ this.#maybeCollectNetworkData({
+ collectedData,
+ dataType: DataType.Request,
+ request,
+ readAndProcessBodyFn: request.readAndProcessRequestBody,
+ size: request.postDataSize,
+ });
+ }
+
+ /**
+ * Implements https://www.w3.org/TR/webdriver-bidi/#maybe-collect-network-response-body
*
* @param {NetworkRequest} request
* The request object for which we want to collect the body.
@@ -2322,12 +2378,69 @@ class NetworkModule extends RootBiDiModule {
return;
}
+ let readAndProcessBodyFn, size;
+ if (response.isDataURL) {
+ // Handle data URLs as a special case since the response is not provided
+ // by the DevTools ResponseListener in this case.
+ const url = request.serializedURL;
+ const body = url.substring(url.indexOf(",") + 1);
+ const isText =
+ response.mimeType &&
+ lazy.NetworkHelper.isTextMimeType(response.mimeType);
+
+ readAndProcessBodyFn = () =>
+ new lazy.NetworkDataBytes({
+ getBytesValue: () => body,
+ isBase64: !isText,
+ });
+ size = body.length;
+ } else {
+ readAndProcessBodyFn = response.readAndProcessResponseBody;
+ size = response.encodedBodySize;
+ }
+
+ this.#maybeCollectNetworkData({
+ collectedData,
+ dataType: DataType.Response,
+ request,
+ readAndProcessBodyFn,
+ size,
+ });
+ }
+
+ /**
+ * Implements https://www.w3.org/TR/webdriver-bidi/#maybe-collect-network-data
+ *
+ * @param {object} options
+ * @param {Data} options.collectedData
+ * @param {DataType} options.dataType
+ * @param {NetworkRequest} options.request
+ * @param {Function} options.readAndProcessBodyFn
+ * @param {number} options.size
+ */
+ async #maybeCollectNetworkData(options) {
+ const {
+ collectedData,
+ dataType,
+ request,
+ // Note: this parameter is not present in
+ // https://www.w3.org/TR/webdriver-bidi/#maybe-collect-network-data
+ // Each caller is responsible for providing a callable which will return
+ // a NetworkDataBytes instance corresponding to the collected data.
+ readAndProcessBodyFn,
+ // Note: the spec assumes that in some cases the size can be computed
+ // dynamically. But in practice we might be storing encoding data in a
+ // format which makes it hard to get the size. So here we always expect
+ // callers to provide a size.
+ size,
+ } = options;
+
const browsingContext = lazy.NavigableManager.getBrowsingContextById(
request.contextId
);
if (!browsingContext) {
lazy.logger.trace(
- `Network data not collected for request "${request.requestId}" and data type "${DataType.Response}"` +
+ `Network data not collected for request "${request.requestId}" and data type "${dataType}"` +
`: navigable no longer available`
);
collectedData.pending = false;
@@ -2342,7 +2455,7 @@ class NetworkModule extends RootBiDiModule {
let collectors = [];
for (const [, collector] of this.#networkCollectors) {
if (
- collector.dataTypes.includes(DataType.Response) &&
+ collector.dataTypes.includes(dataType) &&
this.#matchCollectorForNavigable(collector, topNavigable)
) {
collectors.push(collector);
@@ -2351,7 +2464,7 @@ class NetworkModule extends RootBiDiModule {
if (!collectors.length) {
lazy.logger.trace(
- `Network data not collected for request "${request.requestId}" and data type "${DataType.Response}"` +
+ `Network data not collected for request "${request.requestId}" and data type "${dataType}"` +
`: no matching collector`
);
collectedData.pending = false;
@@ -2363,32 +2476,16 @@ class NetworkModule extends RootBiDiModule {
}
let bytes = null;
- let size = null;
// At this point, the specification expects to processBody for the cloned
- // body. Since this is handled by the DevTools NetworkResponseListener, so
- // here we wait until the response content is set.
+ // body. Here we do not explicitly clone the bodies.
+ // For responses, DevTools' NetworkResponseListener clones the stream.
+ // For requests, NetworkHelper.readPostTextFromRequest clones the stream on
+ // the fly to read it as text.
try {
- if (response.isDataURL) {
- // Handle data URLs as a special case since the response is not provided
- // by the DevTools ResponseListener in this case.
- const url = request.serializedURL;
- const body = url.substring(url.indexOf(",") + 1);
- const isText =
- response.mimeType &&
- lazy.NetworkHelper.isTextMimeType(response.mimeType);
- // TODO: Reuse a common interface being introduced in Bug 1988955.
- bytes = {
- getDecodedResponseBody: () => body,
- encoding: isText ? null : "base64",
- };
- size = body.length;
- } else {
- const bytesOrNull = await response.readResponseBody();
- if (bytesOrNull !== null) {
- bytes = bytesOrNull;
- size = response.encodedBodySize;
- }
+ const bytesOrNull = await readAndProcessBodyFn();
+ if (bytesOrNull !== null) {
+ bytes = bytesOrNull;
}
} catch {
// Let processBodyError be this step: Do nothing.
@@ -2515,6 +2612,11 @@ class NetworkModule extends RootBiDiModule {
return;
}
+ // Make sure a collected data is created for the request.
+ // Note: this is supposed to be triggered from fetch and doesn't depend on
+ // whether network events are used or not.
+ this.#cloneNetworkRequestBody(request);
+
const relatedNavigables = [browsingContext];
this.#updateRequestHeaders(request, relatedNavigables);
@@ -2529,6 +2631,8 @@ class NetworkModule extends RootBiDiModule {
return;
}
+ this.#maybeCollectNetworkRequestBody(request);
+
const baseParameters = this.#processNetworkEvent(
protocolEventName,
request