Bug 1790370 - [bidi] Add basic support for network.responseStarted event r=webdriver-reviewers,Sasha,whimboo

Depends on D165431

Differential Revision: https://phabricator.services.mozilla.com/D165432
This commit is contained in:
Julian Descottes 2023-01-07 00:38:47 +00:00
Родитель b3d6d9d969
Коммит 707344df29
3 изменённых файлов: 336 добавлений и 29 удалений

Просмотреть файл

@ -462,6 +462,8 @@ export class NetworkObserver {
httpActivity.owner.addResponseStart(
{
httpVersion,
protocol: this.#getProtocol(httpActivity),
fromCache: this.#isFromCache(httpActivity),
remoteAddress: "",
remotePort: "",
status,
@ -1036,6 +1038,8 @@ export class NetworkObserver {
const response = {};
response.discardResponseBody = httpActivity.discardResponseBody;
response.httpVersion = httpActivity.httpVersion;
response.protocol = this.#getProtocol(httpActivity);
response.fromCache = this.#isFromCache(httpActivity);
response.remoteAddress = httpActivity.channel.remoteAddress;
response.remotePort = httpActivity.channel.remotePort;
response.status = httpActivity.responseStatus;
@ -1083,6 +1087,66 @@ export class NetworkObserver {
}
}
/**
* Get the protocol for the provided httpActivity. Either the ALPN negotiated
* protocol or as a fallback a protocol computed from the scheme and the
* response status.
*
* TODO: The `protocol` is similar to another response property called
* `httpVersion`. `httpVersion` is uppercase and purely computed from the
* response status, whereas `protocol` uses nsIHttpChannel.protocolVersion by
* default and otherwise falls back on `httpVersion`. Ideally we should merge
* the two properties.
*
* @param {Object} httpActivity
* The httpActivity object for which we need to get the protocol.
*
* @returns {string}
* The protocol as a string.
*/
#getProtocol(httpActivity) {
const { channel, httpVersion } = httpActivity;
let protocol = "";
try {
const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
// protocolVersion corresponds to ALPN negotiated protocol.
protocol = httpChannel.protocolVersion;
} catch (e) {
// Ignore errors reading protocolVersion.
}
if (["", "unknown"].includes(protocol)) {
protocol = channel.URI.scheme;
if (
typeof httpVersion == "string" &&
(protocol === "http" || protocol === "https")
) {
protocol = httpVersion.toLowerCase();
}
}
return protocol;
}
/**
* Check if the channel data for the provided http activity is loaded from the
* cache or not.
*
* @param {Object} httpActivity
* The httpActivity object for which we need to check the cache status.
*
* @returns {boolean}
* True if the channel data is loaded from the cache, false otherwise.
*/
#isFromCache(httpActivity) {
const { channel } = httpActivity;
if (channel instanceof Ci.nsICacheInfoChannel) {
return channel.isFromCache();
}
return false;
}
#getBlockedTiming(timings) {
if (timings.STATUS_RESOLVING && timings.STATUS_CONNECTING_TO) {
return timings.STATUS_RESOLVING.first - timings.REQUEST_HEADER.first;

Просмотреть файл

@ -21,6 +21,7 @@ export class NetworkEventRecord {
#requestData;
#requestId;
#responseData;
#wrappedChannel;
/**
*
@ -36,54 +37,77 @@ export class NetworkEventRecord {
const browsingContext = BrowsingContext.get(networkEvent.browsingContextID);
this.#contextId = lazy.TabManager.getIdForBrowsingContext(browsingContext);
this.#channel = channel;
this.#wrappedChannel = ChannelWrapper.get(channel);
this.#networkListener = networkListener;
// The wrappedChannel id remains identical across redirects, whereas
// nsIChannel.channelId is different for each and every request.
const wrappedChannel = ChannelWrapper.get(channel);
this.#requestId = wrappedChannel.id.toString();
this.#requestId = this.#wrappedChannel.id.toString();
// See the RequestData type definition for the full list of properties that
// should be set on this object.
this.#requestData = {
bodySize: 0,
bodySize: null,
cookies: [],
headers: [],
headersSize: networkEvent.headersSize || null,
method: networkEvent.method || null,
headersSize: networkEvent.headersSize,
method: networkEvent.method,
request: this.#requestId,
timings: {},
url: networkEvent.url || null,
url: networkEvent.url,
};
// See the ResponseData type definition for the full list of properties that
// should be set on this object.
this.#responseData = {
// encoded size (body)
bodySize: null,
content: {
// decoded size
size: null,
},
// encoded size (headers)
headersSize: null,
url: networkEvent.url,
};
}
/**
* This method has to be defined to match the event owner API of the
* NetworkObserver. It will only be called once per network event record, so
* despite the name we will simply store the headers and rawHeaders.
*
* Set network request headers.
*
* Required API for a NetworkObserver event owner.
*
* It will only be called once per network event record, so
* despite the name we will simply store the headers and rawHeaders.
*
* @param {Array} headers
* The request headers array.
* @param {string=} rawHeaders
* The raw headers source.
*/
addRequestHeaders(headers, rawHeaders) {
this.#requestData.headers = headers;
if (typeof headers == "object") {
this.#requestData.headers = headers;
}
this.#requestData.rawHeaders = rawHeaders;
}
/**
* Set network request cookies.
*
* This method has to be defined to match the event owner API of the
* NetworkObserver. It will only be called once per network event record, so
* Required API for a NetworkObserver event owner.
*
* It will only be called once per network event record, so
* despite the name we will simply store the cookies.
*
* @param {Array} cookies
* The request cookies array.
*/
addRequestCookies(cookies) {
this.#requestData.cookies = cookies;
if (typeof cookies == "object") {
this.#requestData.cookies = cookies;
}
// By design, the NetworkObserver will synchronously create a "network event"
// then call addRequestHeaders and finally addRequestCookies.
@ -94,16 +118,143 @@ export class NetworkEventRecord {
this.#emitBeforeRequestSent();
}
// Expected network event owner API from the NetworkObserver.
addEventTimings() {}
addRequestPostData() {}
addResponseCache() {}
addResponseContent() {}
addResponseCookies() {}
addResponseHeaders() {}
addResponseStart() {}
addSecurityInfo() {}
addServerTimings() {}
/**
* Add network request POST data.
*
* Required API for a NetworkObserver event owner.
*
* @param {Object} postData
* The request POST data.
*/
addRequestPostData(postData) {
// Only the postData size is needed for RemoteAgent consumers.
this.#requestData.bodySize = postData.size;
}
/**
* Add the initial network response information.
*
* Required API for a NetworkObserver event owner.
*
* @param {Object} response
* The response information.
* @param {string} rawHeaders
* The raw headers source.
*/
addResponseStart(response, rawHeaders) {
this.#responseData = {
...this.#responseData,
fromCache: response.fromCache,
bodySize: response.bodySize,
// Note: at this point we only have access to the headers size. Parsed
// headers will be added in addResponseHeaders.
headersSize: response.headersSize,
bytesReceived: response.transferredSize,
mimeType: response.mimeType,
protocol: response.protocol,
status: parseInt(response.status),
statusText: response.statusText,
};
}
/**
* Add connection security information.
*
* Required API for a NetworkObserver event owner.
*
* Not used for RemoteAgent.
*
* @param {Object} info
* The object containing security information.
* @param {boolean} isRacing
* True if the corresponding channel raced the cache and network requests.
*/
addSecurityInfo(info, isRacing) {}
/**
* Add network response headers.
*
* Required API for a NetworkObserver event owner.
*
* @param {Array} headers
* The response headers array.
*/
addResponseHeaders(headers) {
this.#responseData.headers = headers;
// This should be triggered when all headers have been received, matching
// the WebDriverBiDi response started trigger in `4.6. HTTP-network fetch`
// from the fetch specification, based on the PR visible at
// https://github.com/whatwg/fetch/pull/1540
this.#emitResponseStarted();
}
/**
* Add network response cookies.
*
* Required API for a NetworkObserver event owner.
*
* Not used for RemoteAgent.
*
* @param {Array} cookies
* The response cookies array.
*/
addResponseCookies(cookies) {}
/**
* Add network event timings.
*
* Required API for a NetworkObserver event owner.
*
* Not used for RemoteAgent.
*
* @param {number} total
* The total time for the request.
* @param {Object} timings
* The har-like timings.
* @param {Object} offsets
* The har-like timings, but as offset from the request start.
* @param {Array} serverTimings
* The server timings.
*/
addEventTimings(total, timings, offsets, serverTimings) {}
/**
* Add response cache entry.
*
* Required API for a NetworkObserver event owner.
*
* Not used for RemoteAgent.
*
* @param {Object} options
* An object which contains a single responseCache property.
*/
addResponseCache(options) {}
/**
* Add response content.
*
* Required API for a NetworkObserver event owner.
*
* Not used for RemoteAgent.
*
* @param {Object} response
* An object which represents the response content.
* @param {Object} responseInfo
* Additional meta data about the response.
*/
addResponseContent(response, responseInfo) {}
/**
* Add server timings.
*
* Required API for a NetworkObserver event owner.
*
* Not used for RemoteAgent.
*
* @param {Array} serverTimings
* The server timings.
*/
addServerTimings(serverTimings) {}
#emitBeforeRequestSent() {
const timedChannel = this.#channel.QueryInterface(Ci.nsITimedChannel);
@ -113,7 +264,19 @@ export class NetworkEventRecord {
contextId: this.#contextId,
redirectCount: timedChannel.redirectCount,
requestData: this.#requestData,
requestId: this.#requestId,
timestamp: Date.now(),
});
}
#emitResponseStarted() {
const timedChannel = this.#channel.QueryInterface(Ci.nsITimedChannel);
this.#requestData.timings = this.#getTimingsFromTimedChannel(timedChannel);
this.#networkListener.emit("response-started", {
contextId: this.#contextId,
redirectCount: timedChannel.redirectCount,
requestData: this.#requestData,
responseData: this.#responseData,
timestamp: Date.now(),
});
}
@ -129,8 +292,9 @@ export class NetworkEventRecord {
* @param {number} requestTime
* High definition timestamp for the request start time relative from the
* time origin.
* @returns {number} High definition timestamp for the request timing relative
* to the start time of the request, or 0 if the provided timing was 0.
* @returns {number}
* High definition timestamp for the request timing relative to the start
* time of the request, or 0 if the provided timing was 0.
*/
#convertTimestamp(timing, requestTime) {
if (timing == 0) {

Просмотреть файл

@ -7,6 +7,7 @@ import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
NetworkListener:
"chrome://remote/content/shared/listeners/NetworkListener.sys.mjs",
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
@ -88,7 +89,8 @@ const InitiatorType = {
/**
* @typedef {Object} RequestData
* @property {number} bodySize
* @property {number|null} bodySize
* Defaults to null.
* @property {Array<Cookie>} cookies
* @property {Array<Header>} headers
* @property {number} headersSize
@ -109,6 +111,40 @@ const InitiatorType = {
* @typedef {BaseParameters & BeforeRequestSentParametersProperties} BeforeRequestSentParameters
*/
/**
* @typedef {Object} ResponseContent
* @property {number|null} size
* Defaults to null.
*/
/**
* @typedef {Object} ResponseData
* @property {string} url
* @property {string} protocol
* @property {number} status
* @property {string} statusText
* @property {boolean} fromCache
* @property {Array<Header>} headers
* @property {string} mimeType
* @property {number} bytesReceived
* @property {number|null} headersSize
* Defaults to null.
* @property {number|null} bodySize
* Defaults to null.
* @property {ResponseContent} content
*/
/**
* @typedef {Object} ResponseStartedParametersProperties
* @property {ResponseData} response
*/
/**
* Parameters for the ResponseStarted event
*
* @typedef {BaseParameters & ResponseStartedParametersProperties} ResponseStartedParameters
*/
class NetworkModule extends Module {
#beforeRequestSentMap;
#networkListener;
@ -128,10 +164,12 @@ class NetworkModule extends Module {
this.#networkListener = new lazy.NetworkListener();
this.#networkListener.on("before-request-sent", this.#onBeforeRequestSent);
this.#networkListener.on("response-started", this.#onResponseStarted);
}
destroy() {
this.#networkListener.off("before-request-sent", this.#onBeforeRequestSent);
this.#networkListener.off("response-started", this.#onResponseStarted);
this.#beforeRequestSentMap = null;
this.#subscribedEvents = null;
@ -178,6 +216,47 @@ class NetworkModule extends Module {
);
};
#onResponseStarted = (name, data) => {
const {
contextId,
requestData,
responseData,
timestamp,
redirectCount,
} = data;
const isRedirect = redirectCount > 0;
const requestId = requestData.requestId;
if (this.#beforeRequestSentMap.get(requestId) != redirectCount) {
throw new lazy.error.UnknownError(
`Redirect count of the request ${requestId} does not match the before request sent map`
);
}
const baseParameters = {
context: contextId,
isRedirect,
redirectCount,
// Bug 1805405: Handle the navigation id.
navigation: null,
request: requestData,
timestamp,
};
const responseStartedEvent = {
...baseParameters,
response: responseData,
};
const browsingContext = lazy.TabManager.getBrowsingContextById(contextId);
this.emitEvent(
"network.responseStarted",
responseStartedEvent,
this.#getContextInfo(browsingContext)
);
};
#startListening(event) {
if (this.#subscribedEvents.size == 0) {
this.#networkListener.startListening();
@ -234,7 +313,7 @@ class NetworkModule extends Module {
}
static get supportedEvents() {
return ["network.beforeRequestSent"];
return ["network.beforeRequestSent", "network.responseStarted"];
}
}