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/NetworkHelper.sys.mjs",
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",
18 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
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).
25 export class NetworkRequest {
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
49 * @param {string=} params.rawHeaders
50 * The request's raw (ie potentially compressed) headers
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;
64 this.#channel instanceof Ci.nsITimedChannel
65 ? this.#channel.QueryInterface(Ci.nsITimedChannel)
69 asyncOpenTime: currentTimeStamp,
72 domainLookupStartTime: currentTimeStamp,
73 domainLookupEndTime: currentTimeStamp,
74 connectStartTime: currentTimeStamp,
75 connectEndTime: currentTimeStamp,
76 secureConnectionStartTime: currentTimeStamp,
77 requestStartTime: currentTimeStamp,
78 responseStartTime: currentTimeStamp,
79 responseEndTime: currentTimeStamp,
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();
92 get alreadyCompleted() {
93 return this.#alreadyCompleted;
101 return this.#contextId;
105 if (this.#isTopLevelDocumentLoad()) {
109 return this.#channel.loadInfo?.fetchDestination;
113 // TODO: Update with a proper error text. Bug 1873037.
114 return ChromeUtils.getXPCOMErrorName(this.#channel.status);
118 return this.#getHeadersList();
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;
128 get initiatorType() {
129 const initiatorType = this.#timedChannel.initiatorType;
130 if (initiatorType === "") {
134 if (this.#isTopLevelDocumentLoad()) {
138 return initiatorType;
141 get isHttpChannel() {
142 return this.#channel instanceof Ci.nsIHttpChannel;
146 return this.#isDataURL ? "GET" : this.#channel.requestMethod;
150 return this.#navigationId;
154 const charset = lazy.NetworkUtils.getCharset(this.#channel);
155 const sentBody = lazy.NetworkHelper.readPostTextFromRequest(
159 return sentBody ? sentBody.length : 0;
162 get redirectCount() {
163 return this.#redirectCount;
167 return this.#requestId;
170 get serializedURL() {
171 return this.#channel.URI.spec;
174 get supportsInterception() {
175 // The request which doesn't have `wrappedChannel` can not be intercepted.
176 return !!this.#wrappedChannel;
180 return this.#getFetchTimings();
183 get wrappedChannel() {
184 return this.#wrappedChannel;
187 set alreadyCompleted(value) {
188 this.#alreadyCompleted = value;
192 * Add information about raw headers, collected from NetworkObserver events.
194 * @param {string} rawHeaders
197 addRawHeaders(rawHeaders) {
198 this.#rawHeaders = rawHeaders || "";
202 * Clear a request header from the request's headers list.
204 * @param {string} name
207 clearRequestHeader(name) {
208 this.#channel.setRequestHeader(
210 "", // aValue="" as an empty value
211 false // aMerge=false to force clearing the header
216 * Redirect the request to another provided URL.
218 * @param {string} url
219 * The URL to redirect to.
222 this.#channel.transparentRedirectTo(Services.io.newURI(url));
226 * Set the request post body
228 * @param {string} body
231 setRequestBody(body) {
232 // Update the requestObserversCalled flag to allow modifying the request,
233 // and reset once done.
234 this.#channel.requestObserversCalled = false;
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(
246 this.#channel.requestMethod,
250 // Make sure to reset the flag once the modification was attempted.
251 this.#channel.requestObserversCalled = true;
256 * Set a request header
258 * @param {string} 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.
267 setRequestHeader(name, value, options) {
268 const { merge = false } = options;
269 this.#channel.setRequestHeader(name, value, merge);
273 * Update the request's method.
275 * @param {string} method
278 setRequestMethod(method) {
279 // Update the requestObserversCalled flag to allow modifying the request,
280 // and reset once done.
281 this.#channel.requestObserversCalled = false;
284 this.#channel.requestMethod = method;
286 // Make sure to reset the flag once the modification was attempted.
287 this.#channel.requestObserversCalled = true;
292 * Allows to bypass the actual network request and immediately respond with
293 * the provided nsIReplacedHttpResponse.
295 * @param {nsIReplacedHttpResponse} replacedHttpResponse
296 * The replaced response to use.
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}`);
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,
318 rawHeaders: rawHeaders.join("\n"),
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.
329 destination: this.destination,
330 headers: this.headers,
331 headersSize: this.headersSize,
332 initiatorType: this.initiatorType,
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,
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.
353 * @param {number} timing
354 * Platform TimeStamp for a request timing relative from the time origin
356 * @param {number} requestTime
357 * Platform TimeStamp for the request start time relative from the time
358 * origin, in microseconds.
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.
365 #convertTimestamp(timing, requestTime) {
370 // Convert from platform timestamp to high resolution timestamp.
371 return (timing - requestTime) / 1000;
375 const id = lazy.NetworkUtils.getChannelBrowsingContextID(this.#channel);
376 const browsingContext = BrowsingContext.get(id);
377 return lazy.TabManager.getIdForBrowsingContext(browsingContext);
381 * Retrieve the Fetch timings for the NetworkRequest.
384 * Object with keys corresponding to fetch timing names, and their
385 * corresponding values.
393 dispatchFetchEventStartTime,
395 domainLookupStartTime,
399 secureConnectionStartTime,
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;
419 if (asyncOpenTime == 0) {
421 `[NetworkRequest] Invalid asyncOpenTime=0 for channel [id: ${
422 this.#channel.channelId
424 this.#channel.URI.spec
425 }], falling back to channelCreationTime=${
426 this.#timedChannel.channelCreationTime
429 requestTime = channelCreationTime;
431 requestTime = asyncOpenTime;
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),
453 * Retrieve the list of headers for the NetworkRequest.
455 * @returns {Array.Array}
456 * Array of (name, value) tuples.
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") {
470 headers.push([name, value]);
475 if (this.#channel instanceof Ci.nsIDataChannel) {
476 // Data channels have no request headers.
480 if (this.#channel instanceof Ci.nsIFileChannel) {
481 // File channels have no request headers.
489 if (!this.#channel.isDocument) {
493 const browsingContext = lazy.TabManager.getBrowsingContextById(
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,
510 return navigation ? navigation.navigationId : null;
513 #isTopLevelDocumentLoad() {
514 if (!this.#channel.isDocument) {
518 const browsingContext = lazy.TabManager.getBrowsingContextById(
521 return !browsingContext.parent;