1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 ChromeUtils.defineESModuleGetters(lazy, {
8 "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
10 TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
14 * The NetworkEventRecord implements the interface expected from network event
15 * owners for consumers of the DevTools NetworkObserver.
17 * The NetworkEventRecord emits the before-request-sent event on behalf of the
18 * NetworkListener instance which created it.
20 export class NetworkEventRecord {
23 #isMainDocumentChannel;
35 * @param {object} networkEvent
36 * The initial network event information (see createNetworkEvent() in
37 * NetworkUtils.sys.mjs).
38 * @param {nsIChannel} channel
39 * The nsIChannel behind this network event.
40 * @param {NetworkListener} networkListener
41 * The NetworkListener which created this NetworkEventRecord.
43 constructor(networkEvent, channel, networkListener) {
44 this.#requestChannel = channel;
45 this.#responseChannel = null;
47 this.#fromCache = networkEvent.fromCache;
48 this.#isMainDocumentChannel = channel.isMainDocumentChannel;
50 this.#wrappedChannel = ChannelWrapper.get(channel);
52 this.#networkListener = networkListener;
54 // The context ids computed by TabManager have the lifecycle of a navigable
55 // and can be reused for all the events emitted from this record.
56 this.#contextId = this.#getContextId();
58 // The wrappedChannel id remains identical across redirects, whereas
59 // nsIChannel.channelId is different for each and every request.
60 this.#requestId = this.#wrappedChannel.id.toString();
62 const { cookies, headers } =
63 lazy.NetworkUtils.fetchRequestHeadersAndCookies(channel);
65 // See the RequestData type definition for the full list of properties that
66 // should be set on this object.
71 headersSize: networkEvent.rawHeaders ? networkEvent.rawHeaders.length : 0,
72 method: channel.requestMethod,
73 request: this.#requestId,
75 url: channel.URI.spec,
78 // See the ResponseData type definition for the full list of properties that
79 // should be set on this object.
80 this.#responseData = {
81 // encoded size (body)
87 // encoded size (headers)
89 url: channel.URI.spec,
92 // NetworkObserver creates a network event when request headers have been
94 // According to the BiDi spec, we should emit beforeRequestSent when adding
95 // request headers, see https://whatpr.org/fetch/1540.html#http-network-or-cache-fetch
97 // Bug 1802181: switch the NetworkObserver to an event-based API.
98 this.#emitBeforeRequestSent();
100 // If the request is already blocked, we will not receive further updates,
101 // emit a network.fetchError event immediately.
102 if (networkEvent.blockedReason) {
103 this.#emitFetchError();
108 * Add network request POST data.
110 * Required API for a NetworkObserver event owner.
112 * @param {object} postData
113 * The request POST data.
115 addRequestPostData(postData) {
116 // Only the postData size is needed for RemoteAgent consumers.
117 this.#requestData.bodySize = postData.size;
121 * Add the initial network response information.
123 * Required API for a NetworkObserver event owner.
126 * @param {object} options
127 * @param {nsIChannel} options.channel
129 * @param {boolean} options.fromCache
130 * @param {string} options.rawHeaders
132 addResponseStart(options) {
133 const { channel, fromCache, rawHeaders = "" } = options;
134 this.#responseChannel = channel;
137 lazy.NetworkUtils.fetchResponseHeadersAndCookies(channel);
139 const headersSize = rawHeaders.length;
140 this.#responseData = {
141 ...this.#responseData,
143 bytesReceived: headersSize,
144 fromCache: this.#fromCache || !!fromCache,
147 mimeType: this.#getMimeType(),
148 protocol: lazy.NetworkUtils.getProtocol(channel),
149 status: channel.responseStatus,
150 statusText: channel.responseStatusText,
153 // This should be triggered when all headers have been received, matching
154 // the WebDriverBiDi response started trigger in `4.6. HTTP-network fetch`
155 // from the fetch specification, based on the PR visible at
156 // https://github.com/whatwg/fetch/pull/1540
157 this.#emitResponseStarted();
161 * Add connection security information.
163 * Required API for a NetworkObserver event owner.
165 * Not used for RemoteAgent.
170 * Add network event timings.
172 * Required API for a NetworkObserver event owner.
174 * Not used for RemoteAgent.
179 * Add response cache entry.
181 * Required API for a NetworkObserver event owner.
183 * Not used for RemoteAgent.
185 addResponseCache() {}
188 * Add response content.
190 * Required API for a NetworkObserver event owner.
192 * @param {object} response
193 * An object which represents the response content.
194 * @param {object} responseInfo
195 * Additional meta data about the response.
197 addResponseContent(response, responseInfo) {
198 // Update content-related sizes with the latest data from addResponseContent.
199 this.#responseData = {
200 ...this.#responseData,
201 bodySize: response.bodySize,
202 bytesReceived: response.transferredSize,
204 size: response.decodedBodySize,
208 if (responseInfo.blockedReason) {
209 this.#emitFetchError();
211 this.#emitResponseCompleted();
216 * Add server timings.
218 * Required API for a NetworkObserver event owner.
220 * Not used for RemoteAgent.
222 addServerTimings() {}
225 * Add service worker timings.
227 * Required API for a NetworkObserver event owner.
229 * Not used for RemoteAgent.
231 addServiceWorkerTimings() {}
233 onAuthPrompt(authDetails, authCallbacks) {
234 this.#emitAuthRequired(authCallbacks);
238 * Convert the provided request timing to a timing relative to the beginning
239 * of the request. All timings are numbers representing high definition
242 * @param {number} timing
243 * High definition timestamp for a request timing relative from the time
245 * @param {number} requestTime
246 * High definition timestamp for the request start time relative from the
249 * High definition timestamp for the request timing relative to the start
250 * time of the request, or 0 if the provided timing was 0.
252 #convertTimestamp(timing, requestTime) {
257 return timing - requestTime;
260 #emitAuthRequired(authCallbacks) {
261 this.#updateDataFromTimedChannel();
263 this.#networkListener.emit("auth-required", {
265 contextId: this.#contextId,
266 isNavigationRequest: this.#isMainDocumentChannel,
267 redirectCount: this.#redirectCount,
268 requestChannel: this.#requestChannel,
269 requestData: this.#requestData,
270 responseChannel: this.#responseChannel,
271 responseData: this.#responseData,
272 timestamp: Date.now(),
276 #emitBeforeRequestSent() {
277 this.#updateDataFromTimedChannel();
279 this.#networkListener.emit("before-request-sent", {
280 contextId: this.#contextId,
281 isNavigationRequest: this.#isMainDocumentChannel,
282 redirectCount: this.#redirectCount,
283 requestChannel: this.#requestChannel,
284 requestData: this.#requestData,
285 timestamp: Date.now(),
290 this.#updateDataFromTimedChannel();
292 this.#networkListener.emit("fetch-error", {
293 contextId: this.#contextId,
294 // TODO: Update with a proper error text. Bug 1873037.
295 errorText: ChromeUtils.getXPCOMErrorName(this.#requestChannel.status),
296 isNavigationRequest: this.#isMainDocumentChannel,
297 redirectCount: this.#redirectCount,
298 requestChannel: this.#requestChannel,
299 requestData: this.#requestData,
300 timestamp: Date.now(),
304 #emitResponseCompleted() {
305 this.#updateDataFromTimedChannel();
307 this.#networkListener.emit("response-completed", {
308 contextId: this.#contextId,
309 isNavigationRequest: this.#isMainDocumentChannel,
310 redirectCount: this.#redirectCount,
311 requestChannel: this.#requestChannel,
312 requestData: this.#requestData,
313 responseChannel: this.#responseChannel,
314 responseData: this.#responseData,
315 timestamp: Date.now(),
319 #emitResponseStarted() {
320 this.#updateDataFromTimedChannel();
322 this.#networkListener.emit("response-started", {
323 contextId: this.#contextId,
324 isNavigationRequest: this.#isMainDocumentChannel,
325 redirectCount: this.#redirectCount,
326 requestChannel: this.#requestChannel,
327 requestData: this.#requestData,
328 responseChannel: this.#responseChannel,
329 responseData: this.#responseData,
330 timestamp: Date.now(),
334 #getBrowsingContext() {
335 const id = lazy.NetworkUtils.getChannelBrowsingContextID(
338 return BrowsingContext.get(id);
342 * Retrieve the navigable id for the current browsing context associated to
343 * the requests' channel. Network events are recorded in the parent process
344 * so we always expect to be able to use TabManager.getIdForBrowsingContext.
347 * The navigable id corresponding to the given browsing context.
350 return lazy.TabManager.getIdForBrowsingContext(this.#getBrowsingContext());
354 // TODO: DevTools NetworkObserver is computing a similar value in
355 // addResponseContent, but uses an inconsistent implementation in
356 // addResponseStart. This approach can only be used as early as in
357 // addResponseHeaders. We should move this logic to the NetworkObserver and
358 // expose mimeType in addResponseStart. Bug 1809670.
362 mimeType = this.#wrappedChannel.contentType;
363 const contentCharset = this.#requestChannel.contentCharset;
364 if (contentCharset) {
365 mimeType += `;charset=${contentCharset}`;
368 // Ignore exceptions when reading contentType/contentCharset
374 #getTimingsFromTimedChannel(timedChannel) {
379 dispatchFetchEventStartTime,
381 domainLookupStartTime,
385 secureConnectionStartTime,
391 // fetchStart should be the post-redirect start time, which should be the
392 // first non-zero timing from: dispatchFetchEventStart, cacheReadStart and
393 // domainLookupStart. See https://www.w3.org/TR/navigation-timing-2/#processing-model
394 const fetchStartTime =
395 dispatchFetchEventStartTime ||
396 cacheReadStartTime ||
397 domainLookupStartTime;
399 // Bug 1805478: Per spec, the origin time should match Performance API's
400 // timeOrigin for the global which initiated the request. This is not
401 // available in the parent process, so for now we will use 0.
402 const timeOrigin = 0;
406 requestTime: this.#convertTimestamp(channelCreationTime, timeOrigin),
407 redirectStart: this.#convertTimestamp(redirectStartTime, timeOrigin),
408 redirectEnd: this.#convertTimestamp(redirectEndTime, timeOrigin),
409 fetchStart: this.#convertTimestamp(fetchStartTime, timeOrigin),
410 dnsStart: this.#convertTimestamp(domainLookupStartTime, timeOrigin),
411 dnsEnd: this.#convertTimestamp(domainLookupEndTime, timeOrigin),
412 connectStart: this.#convertTimestamp(connectStartTime, timeOrigin),
413 connectEnd: this.#convertTimestamp(connectEndTime, timeOrigin),
414 tlsStart: this.#convertTimestamp(secureConnectionStartTime, timeOrigin),
415 tlsEnd: this.#convertTimestamp(connectEndTime, timeOrigin),
416 requestStart: this.#convertTimestamp(requestStartTime, timeOrigin),
417 responseStart: this.#convertTimestamp(responseStartTime, timeOrigin),
418 responseEnd: this.#convertTimestamp(responseEndTime, timeOrigin),
423 * Update the timings and the redirect count from the nsITimedChannel
424 * corresponding to the current channel. This should be called before emitting
425 * any event from this class.
427 #updateDataFromTimedChannel() {
428 const timedChannel = this.#requestChannel.QueryInterface(
431 this.#redirectCount = timedChannel.redirectCount;
432 this.#requestData.timings = this.#getTimingsFromTimedChannel(timedChannel);