Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / remote / shared / NetworkRequest.sys.mjs
blobb157e10c6c2b3e76cd85c2e627e4ef255825ce28
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/. */
5 const lazy = {};
6 ChromeUtils.defineESModuleGetters(lazy, {
7   NetworkHelper:
8     "resource://devtools/shared/network-observer/NetworkHelper.sys.mjs",
9   NetworkUtils:
10     "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
12   Log: "chrome://remote/content/shared/Log.sys.mjs",
13   notifyNavigationStarted:
14     "chrome://remote/content/shared/NavigationManager.sys.mjs",
15   TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
16 });
18 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
20 /**
21  * The NetworkRequest class is a wrapper around the internal channel which
22  * provides getters and methods closer to fetch's response concept
23  * (https://fetch.spec.whatwg.org/#concept-response).
24  */
25 export class NetworkRequest {
26   #alreadyCompleted;
27   #channel;
28   #contextId;
29   #eventRecord;
30   #isDataURL;
31   #navigationId;
32   #navigationManager;
33   #rawHeaders;
34   #redirectCount;
35   #requestId;
36   #timedChannel;
37   #wrappedChannel;
39   /**
40    *
41    * @param {nsIChannel} channel
42    *     The channel for the request.
43    * @param {object} params
44    * @param {NetworkEventRecord} params.networkEventRecord
45    *     The NetworkEventRecord owning this NetworkRequest.
46    * @param {NavigationManager} params.navigationManager
47    *     The NavigationManager where navigations for the current session are
48    *     monitored.
49    * @param {string=} params.rawHeaders
50    *     The request's raw (ie potentially compressed) headers
51    */
52   constructor(channel, params) {
53     const { eventRecord, navigationManager, rawHeaders = "" } = params;
55     this.#channel = channel;
56     this.#eventRecord = eventRecord;
57     this.#isDataURL = this.#channel instanceof Ci.nsIDataChannel;
58     this.#navigationManager = navigationManager;
59     this.#rawHeaders = rawHeaders;
61     // Platform timestamp is in microseconds.
62     const currentTimeStamp = Date.now() * 1000;
63     this.#timedChannel =
64       this.#channel instanceof Ci.nsITimedChannel
65         ? this.#channel.QueryInterface(Ci.nsITimedChannel)
66         : {
67             redirectCount: 0,
68             initiatorType: "",
69             asyncOpenTime: currentTimeStamp,
70             redirectStartTime: 0,
71             redirectEndTime: 0,
72             domainLookupStartTime: currentTimeStamp,
73             domainLookupEndTime: currentTimeStamp,
74             connectStartTime: currentTimeStamp,
75             connectEndTime: currentTimeStamp,
76             secureConnectionStartTime: currentTimeStamp,
77             requestStartTime: currentTimeStamp,
78             responseStartTime: currentTimeStamp,
79             responseEndTime: currentTimeStamp,
80           };
81     this.#wrappedChannel = ChannelWrapper.get(channel);
83     this.#redirectCount = this.#timedChannel.redirectCount;
84     // The wrappedChannel id remains identical across redirects, whereas
85     // nsIChannel.channelId is different for each and every request.
86     this.#requestId = this.#wrappedChannel.id.toString();
88     this.#contextId = this.#getContextId();
89     this.#navigationId = this.#getNavigationId();
90   }
92   get alreadyCompleted() {
93     return this.#alreadyCompleted;
94   }
96   get channel() {
97     return this.#channel;
98   }
100   get contextId() {
101     return this.#contextId;
102   }
104   get destination() {
105     if (this.#isTopLevelDocumentLoad()) {
106       return "";
107     }
109     return this.#channel.loadInfo?.fetchDestination;
110   }
112   get errorText() {
113     // TODO: Update with a proper error text. Bug 1873037.
114     return ChromeUtils.getXPCOMErrorName(this.#channel.status);
115   }
117   get headers() {
118     return this.#getHeadersList();
119   }
121   get headersSize() {
122     // TODO: rawHeaders will not be updated after modifying the headers via
123     // request interception. Need to find another way to retrieve the
124     // information dynamically.
125     return this.#rawHeaders.length;
126   }
128   get initiatorType() {
129     const initiatorType = this.#timedChannel.initiatorType;
130     if (initiatorType === "") {
131       return null;
132     }
134     if (this.#isTopLevelDocumentLoad()) {
135       return null;
136     }
138     return initiatorType;
139   }
141   get isHttpChannel() {
142     return this.#channel instanceof Ci.nsIHttpChannel;
143   }
145   get method() {
146     return this.#isDataURL ? "GET" : this.#channel.requestMethod;
147   }
149   get navigationId() {
150     return this.#navigationId;
151   }
153   get postDataSize() {
154     const charset = lazy.NetworkUtils.getCharset(this.#channel);
155     const sentBody = lazy.NetworkHelper.readPostTextFromRequest(
156       this.#channel,
157       charset
158     );
159     return sentBody ? sentBody.length : 0;
160   }
162   get redirectCount() {
163     return this.#redirectCount;
164   }
166   get requestId() {
167     return this.#requestId;
168   }
170   get serializedURL() {
171     return this.#channel.URI.spec;
172   }
174   get supportsInterception() {
175     // The request which doesn't have `wrappedChannel` can not be intercepted.
176     return !!this.#wrappedChannel;
177   }
179   get timings() {
180     return this.#getFetchTimings();
181   }
183   get wrappedChannel() {
184     return this.#wrappedChannel;
185   }
187   set alreadyCompleted(value) {
188     this.#alreadyCompleted = value;
189   }
191   /**
192    * Add information about raw headers, collected from NetworkObserver events.
193    *
194    * @param {string} rawHeaders
195    *     The raw headers.
196    */
197   addRawHeaders(rawHeaders) {
198     this.#rawHeaders = rawHeaders || "";
199   }
201   /**
202    * Clear a request header from the request's headers list.
203    *
204    * @param {string} name
205    *     The header's name.
206    */
207   clearRequestHeader(name) {
208     this.#channel.setRequestHeader(
209       name, // aName
210       "", // aValue="" as an empty value
211       false // aMerge=false to force clearing the header
212     );
213   }
215   /**
216    * Redirect the request to another provided URL.
217    *
218    * @param {string} url
219    *     The URL to redirect to.
220    */
221   redirectTo(url) {
222     this.#channel.transparentRedirectTo(Services.io.newURI(url));
223   }
225   /**
226    * Set the request post body
227    *
228    * @param {string} body
229    *     The body to set.
230    */
231   setRequestBody(body) {
232     // Update the requestObserversCalled flag to allow modifying the request,
233     // and reset once done.
234     this.#channel.requestObserversCalled = false;
236     try {
237       this.#channel.QueryInterface(Ci.nsIUploadChannel2);
238       const bodyStream = Cc[
239         "@mozilla.org/io/string-input-stream;1"
240       ].createInstance(Ci.nsIStringInputStream);
241       bodyStream.setByteStringData(body);
242       this.#channel.explicitSetUploadStream(
243         bodyStream,
244         null,
245         -1,
246         this.#channel.requestMethod,
247         false
248       );
249     } finally {
250       // Make sure to reset the flag once the modification was attempted.
251       this.#channel.requestObserversCalled = true;
252     }
253   }
255   /**
256    * Set a request header
257    *
258    * @param {string} name
259    *     The header's name.
260    * @param {string} value
261    *     The header's value.
262    * @param {object} options
263    * @param {boolean} options.merge
264    *     True if the value should be merged with the existing value, false if it
265    *     should override it. Defaults to false.
266    */
267   setRequestHeader(name, value, options) {
268     const { merge = false } = options;
269     this.#channel.setRequestHeader(name, value, merge);
270   }
272   /**
273    * Update the request's method.
274    *
275    * @param {string} method
276    *     The method to set.
277    */
278   setRequestMethod(method) {
279     // Update the requestObserversCalled flag to allow modifying the request,
280     // and reset once done.
281     this.#channel.requestObserversCalled = false;
283     try {
284       this.#channel.requestMethod = method;
285     } finally {
286       // Make sure to reset the flag once the modification was attempted.
287       this.#channel.requestObserversCalled = true;
288     }
289   }
291   /**
292    * Allows to bypass the actual network request and immediately respond with
293    * the provided nsIReplacedHttpResponse.
294    *
295    * @param {nsIReplacedHttpResponse} replacedHttpResponse
296    *     The replaced response to use.
297    */
298   setResponseOverride(replacedHttpResponse) {
299     this.wrappedChannel.channel
300       .QueryInterface(Ci.nsIHttpChannelInternal)
301       .setResponseOverride(replacedHttpResponse);
303     const rawHeaders = [];
304     replacedHttpResponse.visitResponseHeaders({
305       visitHeader(name, value) {
306         rawHeaders.push(`${name}: ${value}`);
307       },
308     });
310     // Setting an override bypasses the usual codepath for network responses.
311     // There will be no notification about receiving a response.
312     // However, there will be a notification about the end of the response.
313     // Therefore, simulate a addResponseStart here to make sure we handle
314     // addResponseContent properly.
315     this.#eventRecord.prepareResponseStart({
316       channel: this.#channel,
317       fromCache: false,
318       rawHeaders: rawHeaders.join("\n"),
319     });
320   }
322   /**
323    * Return a static version of the class instance.
324    * This method is used to prepare the data to be sent with the events for cached resources
325    * generated from the content process but need to be sent to the parent.
326    */
327   toJSON() {
328     return {
329       destination: this.destination,
330       headers: this.headers,
331       headersSize: this.headersSize,
332       initiatorType: this.initiatorType,
333       method: this.method,
334       navigationId: this.navigationId,
335       postDataSize: this.postDataSize,
336       redirectCount: this.redirectCount,
337       requestId: this.requestId,
338       serializedURL: this.serializedURL,
339       // Since this data is meant to be sent to the parent process
340       // it will not be possible to intercept such request.
341       supportsInterception: false,
342       timings: this.timings,
343     };
344   }
346   /**
347    * Convert the provided request timing to a timing relative to the beginning
348    * of the request. Note that https://w3c.github.io/resource-timing/#dfn-convert-fetch-timestamp
349    * only expects high resolution timestamps (double in milliseconds) as inputs
350    * of this method, but since platform timestamps are integers in microseconds,
351    * they will be converted on the fly in this helper.
352    *
353    * @param {number} timing
354    *     Platform TimeStamp for a request timing relative from the time origin
355    *     in microseconds.
356    * @param {number} requestTime
357    *     Platform TimeStamp for the request start time relative from the time
358    *     origin, in microseconds.
359    *
360    * @returns {number}
361    *     High resolution timestamp (https://www.w3.org/TR/hr-time-3/#dom-domhighrestimestamp)
362    *     for the request timing relative to the start time of the request, or 0
363    *     if the provided timing was 0.
364    */
365   #convertTimestamp(timing, requestTime) {
366     if (timing == 0) {
367       return 0;
368     }
370     // Convert from platform timestamp to high resolution timestamp.
371     return (timing - requestTime) / 1000;
372   }
374   #getContextId() {
375     const id = lazy.NetworkUtils.getChannelBrowsingContextID(this.#channel);
376     const browsingContext = BrowsingContext.get(id);
377     return lazy.TabManager.getIdForBrowsingContext(browsingContext);
378   }
380   /**
381    * Retrieve the Fetch timings for the NetworkRequest.
382    *
383    * @returns {object}
384    *     Object with keys corresponding to fetch timing names, and their
385    *     corresponding values.
386    */
387   #getFetchTimings() {
388     const {
389       asyncOpenTime,
390       channelCreationTime,
391       redirectStartTime,
392       redirectEndTime,
393       dispatchFetchEventStartTime,
394       cacheReadStartTime,
395       domainLookupStartTime,
396       domainLookupEndTime,
397       connectStartTime,
398       connectEndTime,
399       secureConnectionStartTime,
400       requestStartTime,
401       responseStartTime,
402       responseEndTime,
403     } = this.#timedChannel;
405     // fetchStart should be the post-redirect start time, which should be the
406     // first non-zero timing from: dispatchFetchEventStart, cacheReadStart and
407     // domainLookupStart. See https://www.w3.org/TR/navigation-timing-2/#processing-model
408     const fetchStartTime =
409       dispatchFetchEventStartTime ||
410       cacheReadStartTime ||
411       domainLookupStartTime;
413     // Bug 1805478: Per spec, the origin time should match Performance API's
414     // timeOrigin for the global which initiated the request. This is not
415     // available in the parent process, so for now we will use 0.
416     const timeOrigin = 0;
418     let requestTime;
419     if (asyncOpenTime == 0) {
420       lazy.logger.warn(
421         `[NetworkRequest] Invalid asyncOpenTime=0 for channel [id: ${
422           this.#channel.channelId
423         }, url: ${
424           this.#channel.URI.spec
425         }], falling back to channelCreationTime=${
426           this.#timedChannel.channelCreationTime
427         }.`
428       );
429       requestTime = channelCreationTime;
430     } else {
431       requestTime = asyncOpenTime;
432     }
434     return {
435       timeOrigin,
436       requestTime: this.#convertTimestamp(requestTime, timeOrigin),
437       redirectStart: this.#convertTimestamp(redirectStartTime, timeOrigin),
438       redirectEnd: this.#convertTimestamp(redirectEndTime, timeOrigin),
439       fetchStart: this.#convertTimestamp(fetchStartTime, timeOrigin),
440       dnsStart: this.#convertTimestamp(domainLookupStartTime, timeOrigin),
441       dnsEnd: this.#convertTimestamp(domainLookupEndTime, timeOrigin),
442       connectStart: this.#convertTimestamp(connectStartTime, timeOrigin),
443       connectEnd: this.#convertTimestamp(connectEndTime, timeOrigin),
444       tlsStart: this.#convertTimestamp(secureConnectionStartTime, timeOrigin),
445       tlsEnd: this.#convertTimestamp(connectEndTime, timeOrigin),
446       requestStart: this.#convertTimestamp(requestStartTime, timeOrigin),
447       responseStart: this.#convertTimestamp(responseStartTime, timeOrigin),
448       responseEnd: this.#convertTimestamp(responseEndTime, timeOrigin),
449     };
450   }
452   /**
453    * Retrieve the list of headers for the NetworkRequest.
454    *
455    * @returns {Array.Array}
456    *     Array of (name, value) tuples.
457    */
458   #getHeadersList() {
459     const headers = [];
461     if (this.#channel instanceof Ci.nsIHttpChannel) {
462       this.#channel.visitRequestHeaders({
463         visitHeader(name, value) {
464           // The `Proxy-Authorization` header even though it appears on the channel is not
465           // actually sent to the server for non CONNECT requests after the HTTP/HTTPS tunnel
466           // is setup by the proxy.
467           if (name == "Proxy-Authorization") {
468             return;
469           }
470           headers.push([name, value]);
471         },
472       });
473     }
475     if (this.#channel instanceof Ci.nsIDataChannel) {
476       // Data channels have no request headers.
477       return [];
478     }
480     if (this.#channel instanceof Ci.nsIFileChannel) {
481       // File channels have no request headers.
482       return [];
483     }
485     return headers;
486   }
488   #getNavigationId() {
489     if (!this.#channel.isDocument) {
490       return null;
491     }
493     const browsingContext = lazy.TabManager.getBrowsingContextById(
494       this.#contextId
495     );
497     let navigation =
498       this.#navigationManager.getNavigationForBrowsingContext(browsingContext);
500     // `onBeforeRequestSent` might be too early for the NavigationManager.
501     // If there is no ongoing navigation, create one ourselves.
502     // TODO: Bug 1835704 to detect navigations earlier and avoid this.
503     if (!navigation || navigation.state !== "started") {
504       navigation = lazy.notifyNavigationStarted({
505         contextDetails: { context: browsingContext },
506         url: this.serializedURL,
507       });
508     }
510     return navigation ? navigation.navigationId : null;
511   }
513   #isTopLevelDocumentLoad() {
514     if (!this.#channel.isDocument) {
515       return false;
516     }
518     const browsingContext = lazy.TabManager.getBrowsingContextById(
519       this.#contextId
520     );
521     return !browsingContext.parent;
522   }