chrome-devtools-frontend
Version:
Chrome DevTools UI
1,245 lines (1,111 loc) • 110 kB
text/typescript
// Copyright 2011 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js';
import * as Protocol from '../../generated/protocol.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import * as Common from '../common/common.js';
import * as i18n from '../i18n/i18n.js';
import * as Platform from '../platform/platform.js';
import * as Root from '../root/root.js';
import {Cookie} from './Cookie.js';
import {
type BlockedCookieWithReason,
DirectSocketChunkType,
DirectSocketStatus,
DirectSocketType,
Events as NetworkRequestEvents,
type ExtraRequestInfo,
type ExtraResponseInfo,
type IncludedCookieWithReason,
type NameValue,
NetworkRequest,
} from './NetworkRequest.js';
import {SDKModel} from './SDKModel.js';
import {Capability, type Target} from './Target.js';
import {type SDKModelObserver, TargetManager} from './TargetManager.js';
const UIStrings = {
/**
* @description Explanation why no content is shown for WebSocket connection.
*/
noContentForWebSocket: 'Content for WebSockets is currently not supported',
/**
* @description Explanation why no content is shown for redirect response.
*/
noContentForRedirect: 'No content available because this request was redirected',
/**
* @description Explanation why no content is shown for preflight request.
*/
noContentForPreflight: 'No content available for preflight request',
/**
* @description Text to indicate that network throttling is disabled
*/
noThrottling: 'No throttling',
/**
* @description Text to indicate the network connectivity is offline
*/
offline: 'Offline',
/**
* @description Text in Network Manager representing the "3G" throttling preset.
*/
slowG: '3G', // Named `slowG` for legacy reasons and because this value
// is serialized locally on the user's machine: if we
// change it we break their stored throttling settings.
// (See crrev.com/c/2947255)
/**
* @description Text in Network Manager representing the "Slow 4G" throttling preset
*/
fastG: 'Slow 4G', // Named `fastG` for legacy reasons and because this value
// is serialized locally on the user's machine: if we
// change it we break their stored throttling settings.
// (See crrev.com/c/2947255)
/**
* @description Text in Network Manager representing the "Fast 4G" throttling preset
*/
fast4G: 'Fast 4G',
/**
* @description Text in Network Manager representing the "Blocking" throttling preset
*/
block: 'Block',
/**
* @description Text in Network Manager
* @example {https://example.com} PH1
*/
requestWasBlockedByDevtoolsS: 'Request was blocked by DevTools: "{PH1}"',
/**
* @description Message in Network Manager
* @example {XHR} PH1
* @example {GET} PH2
* @example {https://example.com} PH3
*/
sFailedLoadingSS: '{PH1} failed loading: {PH2} "{PH3}".',
/**
* @description Message in Network Manager
* @example {XHR} PH1
* @example {GET} PH2
* @example {https://example.com} PH3
*/
sFinishedLoadingSS: '{PH1} finished loading: {PH2} "{PH3}".',
/**
* @description One of direct socket connection statuses
*/
directSocketStatusOpening: 'Opening',
/**
* @description One of direct socket connection statuses
*/
directSocketStatusOpen: 'Open',
/**
* @description One of direct socket connection statuses
*/
directSocketStatusClosed: 'Closed',
/**
* @description One of direct socket connection statuses
*/
directSocketStatusAborted: 'Aborted',
} as const;
const str_ = i18n.i18n.registerUIStrings('core/sdk/NetworkManager.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_);
const requestToManagerMap = new WeakMap<NetworkRequest, NetworkManager>();
const CONNECTION_TYPES = new Map([
['2g', Protocol.Network.ConnectionType.Cellular2g],
['3g', Protocol.Network.ConnectionType.Cellular3g],
['4g', Protocol.Network.ConnectionType.Cellular4g],
['bluetooth', Protocol.Network.ConnectionType.Bluetooth],
['wifi', Protocol.Network.ConnectionType.Wifi],
['wimax', Protocol.Network.ConnectionType.Wimax],
]);
/**
* We store two settings to disk to persist network throttling.
* 1. The custom conditions that the user has defined.
* 2. The active `key` that applies the correct current preset.
* The reason the setting creation functions are defined here is because they are referred
* to in multiple places, and this ensures we don't have accidental typos which
* mean extra settings get mistakenly created.
*/
export function customUserNetworkConditionsSetting(): Common.Settings.Setting<Conditions[]> {
return Common.Settings.Settings.instance().moduleSetting<Conditions[]>('custom-network-conditions');
}
export function activeNetworkThrottlingKeySetting(): Common.Settings.Setting<ThrottlingConditionKey> {
return Common.Settings.Settings.instance().createSetting(
'active-network-condition-key', PredefinedThrottlingConditionKey.NO_THROTTLING);
}
export class NetworkManager extends SDKModel<EventTypes> {
readonly dispatcher: NetworkDispatcher;
readonly fetchDispatcher: FetchDispatcher;
readonly #networkAgent: ProtocolProxyApi.NetworkApi;
readonly #bypassServiceWorkerSetting: Common.Settings.Setting<boolean>;
readonly activeNetworkThrottlingKey: Common.Settings.Setting<ThrottlingConditionKey> =
activeNetworkThrottlingKeySetting();
constructor(target: Target) {
super(target);
this.dispatcher = new NetworkDispatcher(this);
this.fetchDispatcher = new FetchDispatcher(target.fetchAgent(), this);
this.#networkAgent = target.networkAgent();
target.registerNetworkDispatcher(this.dispatcher);
target.registerFetchDispatcher(this.fetchDispatcher);
if (Common.Settings.Settings.instance().moduleSetting('cache-disabled').get()) {
void this.#networkAgent.invoke_setCacheDisabled({cacheDisabled: true});
}
if (Root.Runtime.hostConfig.devToolsPrivacyUI?.enabled &&
Root.Runtime.hostConfig.thirdPartyCookieControls?.managedBlockThirdPartyCookies !== true &&
(Common.Settings.Settings.instance().createSetting('cookie-control-override-enabled', undefined).get() ||
Common.Settings.Settings.instance().createSetting('grace-period-mitigation-disabled', undefined).get() ||
Common.Settings.Settings.instance().createSetting('heuristic-mitigation-disabled', undefined).get())) {
this.cookieControlFlagsSettingChanged();
}
void this.#networkAgent.invoke_enable({
maxPostDataSize: MAX_EAGER_POST_REQUEST_BODY_LENGTH,
enableDurableMessages: Root.Runtime.hostConfig.devToolsEnableDurableMessages?.enabled,
maxTotalBufferSize: MAX_RESPONSE_BODY_TOTAL_BUFFER_LENGTH,
reportDirectSocketTraffic: true,
});
void this.#networkAgent.invoke_setAttachDebugStack({enabled: true});
this.#bypassServiceWorkerSetting =
Common.Settings.Settings.instance().createSetting('bypass-service-worker', false);
if (this.#bypassServiceWorkerSetting.get()) {
this.bypassServiceWorkerChanged();
}
this.#bypassServiceWorkerSetting.addChangeListener(this.bypassServiceWorkerChanged, this);
Common.Settings.Settings.instance()
.moduleSetting('cache-disabled')
.addChangeListener(this.cacheDisabledSettingChanged, this);
Common.Settings.Settings.instance()
.createSetting('cookie-control-override-enabled', undefined)
.addChangeListener(this.cookieControlFlagsSettingChanged, this);
Common.Settings.Settings.instance()
.createSetting('grace-period-mitigation-disabled', undefined)
.addChangeListener(this.cookieControlFlagsSettingChanged, this);
Common.Settings.Settings.instance()
.createSetting('heuristic-mitigation-disabled', undefined)
.addChangeListener(this.cookieControlFlagsSettingChanged, this);
}
static forRequest(request: NetworkRequest): NetworkManager|null {
return requestToManagerMap.get(request) || null;
}
static canReplayRequest(request: NetworkRequest): boolean {
return Boolean(requestToManagerMap.get(request)) && Boolean(request.backendRequestId()) && !request.isRedirect() &&
request.resourceType() === Common.ResourceType.resourceTypes.XHR;
}
static replayRequest(request: NetworkRequest): void {
const manager = requestToManagerMap.get(request);
const requestId = request.backendRequestId();
if (!manager || !requestId || request.isRedirect()) {
return;
}
void manager.#networkAgent.invoke_replayXHR({requestId});
}
static async searchInRequest(request: NetworkRequest, query: string, caseSensitive: boolean, isRegex: boolean):
Promise<TextUtils.ContentProvider.SearchMatch[]> {
const manager = NetworkManager.forRequest(request);
const requestId = request.backendRequestId();
if (!manager || !requestId || request.isRedirect()) {
return [];
}
const response =
await manager.#networkAgent.invoke_searchInResponseBody({requestId, query, caseSensitive, isRegex});
return TextUtils.TextUtils.performSearchInSearchMatches(response.result || [], query, caseSensitive, isRegex);
}
static async requestContentData(request: NetworkRequest): Promise<TextUtils.ContentData.ContentDataOrError> {
if (request.resourceType() === Common.ResourceType.resourceTypes.WebSocket) {
return {error: i18nString(UIStrings.noContentForWebSocket)};
}
if (!request.finished) {
await request.once(NetworkRequestEvents.FINISHED_LOADING);
}
if (request.isRedirect()) {
return {error: i18nString(UIStrings.noContentForRedirect)};
}
if (request.isPreflightRequest()) {
return {error: i18nString(UIStrings.noContentForPreflight)};
}
const manager = NetworkManager.forRequest(request);
if (!manager) {
return {error: 'No network manager for request'};
}
const requestId = request.backendRequestId();
if (!requestId) {
return {error: 'No backend request id for request'};
}
const response = await manager.#networkAgent.invoke_getResponseBody({requestId});
const error = response.getError();
if (error) {
return {error};
}
return new TextUtils.ContentData.ContentData(
response.body, response.base64Encoded, request.mimeType, request.charset() ?? undefined);
}
/**
* Returns the already received bytes for an in-flight request. After calling this method
* "dataReceived" events will contain additional data.
*/
static async streamResponseBody(request: NetworkRequest): Promise<TextUtils.ContentData.ContentDataOrError> {
if (request.finished) {
return {error: 'Streaming the response body is only available for in-flight requests.'};
}
const manager = NetworkManager.forRequest(request);
if (!manager) {
return {error: 'No network manager for request'};
}
const requestId = request.backendRequestId();
if (!requestId) {
return {error: 'No backend request id for request'};
}
const response = await manager.#networkAgent.invoke_streamResourceContent({requestId});
const error = response.getError();
if (error) {
return {error};
}
// Wait for at least the `responseReceived event so we have accurate mimetype and charset.
await request.waitForResponseReceived();
return new TextUtils.ContentData.ContentData(
response.bufferedData, /* isBase64=*/ true, request.mimeType, request.charset() ?? undefined);
}
static async requestPostData(request: NetworkRequest): Promise<string|null> {
const manager = NetworkManager.forRequest(request);
if (!manager) {
console.error('No network manager for request');
return null;
}
const requestId = request.backendRequestId();
if (!requestId) {
console.error('No backend request id for request');
return null;
}
try {
const {postData} = await manager.#networkAgent.invoke_getRequestPostData({requestId});
return postData;
} catch (e) {
return e.message;
}
}
static connectionType(conditions: Conditions): Protocol.Network.ConnectionType {
if (!conditions.download && !conditions.upload) {
return Protocol.Network.ConnectionType.None;
}
try {
const title =
typeof conditions.title === 'function' ? conditions.title().toLowerCase() : conditions.title.toLowerCase();
for (const [name, protocolType] of CONNECTION_TYPES) {
if (title.includes(name)) {
return protocolType;
}
}
} catch {
// If the i18nKey for this condition has changed, calling conditions.title() will break, so in that case we reset to NONE
return Protocol.Network.ConnectionType.None;
}
return Protocol.Network.ConnectionType.Other;
}
static lowercaseHeaders(headers: Protocol.Network.Headers): Protocol.Network.Headers {
const newHeaders: Protocol.Network.Headers = {};
for (const headerName in headers) {
newHeaders[headerName.toLowerCase()] = headers[headerName];
}
return newHeaders;
}
requestForURL(url: Platform.DevToolsPath.UrlString): NetworkRequest|null {
return this.dispatcher.requestForURL(url);
}
requestForId(id: string): NetworkRequest|null {
return this.dispatcher.requestForId(id);
}
requestForLoaderId(loaderId: Protocol.Network.LoaderId): NetworkRequest|null {
return this.dispatcher.requestForLoaderId(loaderId);
}
private cacheDisabledSettingChanged({data: enabled}: Common.EventTarget.EventTargetEvent<boolean>): void {
void this.#networkAgent.invoke_setCacheDisabled({cacheDisabled: enabled});
}
private cookieControlFlagsSettingChanged(): void {
const overridesEnabled =
Boolean(Common.Settings.Settings.instance().createSetting('cookie-control-override-enabled', undefined).get());
const gracePeriodEnabled = overridesEnabled ?
Boolean(
Common.Settings.Settings.instance().createSetting('grace-period-mitigation-disabled', undefined).get()) :
false;
const heuristicEnabled = overridesEnabled ?
Boolean(Common.Settings.Settings.instance().createSetting('heuristic-mitigation-disabled', undefined).get()) :
false;
void this.#networkAgent.invoke_setCookieControls({
enableThirdPartyCookieRestriction: overridesEnabled,
disableThirdPartyCookieMetadata: gracePeriodEnabled,
disableThirdPartyCookieHeuristics: heuristicEnabled,
});
}
override dispose(): void {
Common.Settings.Settings.instance()
.moduleSetting('cache-disabled')
.removeChangeListener(this.cacheDisabledSettingChanged, this);
}
private bypassServiceWorkerChanged(): void {
void this.#networkAgent.invoke_setBypassServiceWorker({bypass: this.#bypassServiceWorkerSetting.get()});
}
async getSecurityIsolationStatus(frameId: Protocol.Page.FrameId|null):
Promise<Protocol.Network.SecurityIsolationStatus|null> {
const result = await this.#networkAgent.invoke_getSecurityIsolationStatus({frameId: frameId ?? undefined});
if (result.getError()) {
return null;
}
return result.status;
}
async enableReportingApi(enable = true): Promise<Promise<Protocol.ProtocolResponseWithError>> {
return await this.#networkAgent.invoke_enableReportingApi({enable});
}
async loadNetworkResource(
frameId: Protocol.Page.FrameId|null, url: Platform.DevToolsPath.UrlString,
options: Protocol.Network.LoadNetworkResourceOptions): Promise<Protocol.Network.LoadNetworkResourcePageResult> {
const result = await this.#networkAgent.invoke_loadNetworkResource({frameId: frameId ?? undefined, url, options});
if (result.getError()) {
throw new Error(result.getError());
}
return result.resource;
}
clearRequests(): void {
this.dispatcher.clearRequests();
}
}
export enum Events {
/* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */
RequestStarted = 'RequestStarted',
RequestUpdated = 'RequestUpdated',
RequestFinished = 'RequestFinished',
RequestUpdateDropped = 'RequestUpdateDropped',
ResponseReceived = 'ResponseReceived',
MessageGenerated = 'MessageGenerated',
RequestRedirected = 'RequestRedirected',
LoadingFinished = 'LoadingFinished',
ReportingApiReportAdded = 'ReportingApiReportAdded',
ReportingApiReportUpdated = 'ReportingApiReportUpdated',
ReportingApiEndpointsChangedForOrigin = 'ReportingApiEndpointsChangedForOrigin',
/* eslint-enable @typescript-eslint/naming-convention */
}
export interface RequestStartedEvent {
request: NetworkRequest;
originalRequest: Protocol.Network.Request|null;
}
export interface ResponseReceivedEvent {
request: NetworkRequest;
response: Protocol.Network.Response;
}
export interface MessageGeneratedEvent {
message: Common.UIString.LocalizedString;
requestId: string;
warning: boolean;
}
export interface EventTypes {
[Events.RequestStarted]: RequestStartedEvent;
[Events.RequestUpdated]: NetworkRequest;
[Events.RequestFinished]: NetworkRequest;
[Events.RequestUpdateDropped]: RequestUpdateDroppedEventData;
[Events.ResponseReceived]: ResponseReceivedEvent;
[Events.MessageGenerated]: MessageGeneratedEvent;
[Events.RequestRedirected]: NetworkRequest;
[Events.LoadingFinished]: NetworkRequest;
[Events.ReportingApiReportAdded]: Protocol.Network.ReportingApiReport;
[Events.ReportingApiReportUpdated]: Protocol.Network.ReportingApiReport;
[Events.ReportingApiEndpointsChangedForOrigin]: Protocol.Network.ReportingApiEndpointsChangedForOriginEvent;
}
/**
* Define some built-in DevTools throttling presets.
* Note that for the download, upload and RTT values we multiply them by adjustment factors to make DevTools' emulation more accurate.
* @see https://docs.google.com/document/d/10lfVdS1iDWCRKQXPfbxEn4Or99D64mvNlugP1AQuFlE/edit for historical context.
* @see https://crbug.com/342406608#comment10 for context around the addition of 4G presets in June 2024.
*/
export const BlockingConditions: ThrottlingConditions = {
key: PredefinedThrottlingConditionKey.BLOCKING,
block: true,
title: i18nLazyString(UIStrings.block),
};
export const NoThrottlingConditions: Conditions = {
key: PredefinedThrottlingConditionKey.NO_THROTTLING,
title: i18nLazyString(UIStrings.noThrottling),
i18nTitleKey: UIStrings.noThrottling,
download: -1,
upload: -1,
latency: 0,
};
export const OfflineConditions: Conditions = {
key: PredefinedThrottlingConditionKey.OFFLINE,
title: i18nLazyString(UIStrings.offline),
i18nTitleKey: UIStrings.offline,
download: 0,
upload: 0,
latency: 0,
};
const slow3GTargetLatency = 400;
export const Slow3GConditions: Conditions = {
key: PredefinedThrottlingConditionKey.SPEED_3G,
title: i18nLazyString(UIStrings.slowG),
i18nTitleKey: UIStrings.slowG,
// ~500Kbps down
download: 500 * 1000 / 8 * .8,
// ~500Kbps up
upload: 500 * 1000 / 8 * .8,
// 400ms RTT
latency: slow3GTargetLatency * 5,
targetLatency: slow3GTargetLatency,
};
// Note for readers: this used to be called "Fast 3G" but it was renamed in May
// 2024 to align with LH (crbug.com/342406608).
const slow4GTargetLatency = 150;
export const Slow4GConditions: Conditions = {
key: PredefinedThrottlingConditionKey.SPEED_SLOW_4G,
title: i18nLazyString(UIStrings.fastG),
i18nTitleKey: UIStrings.fastG,
// ~1.6 Mbps down
download: 1.6 * 1000 * 1000 / 8 * .9,
// ~0.75 Mbps up
upload: 750 * 1000 / 8 * .9,
// 150ms RTT
latency: slow4GTargetLatency * 3.75,
targetLatency: slow4GTargetLatency,
};
const fast4GTargetLatency = 60;
export const Fast4GConditions: Conditions = {
key: PredefinedThrottlingConditionKey.SPEED_FAST_4G,
title: i18nLazyString(UIStrings.fast4G),
i18nTitleKey: UIStrings.fast4G,
// 9 Mbps down
download: 9 * 1000 * 1000 / 8 * .9,
// 1.5 Mbps up
upload: 1.5 * 1000 * 1000 / 8 * .9,
// 60ms RTT
latency: fast4GTargetLatency * 2.75,
targetLatency: fast4GTargetLatency,
};
const MAX_EAGER_POST_REQUEST_BODY_LENGTH = 64 * 1024; // bytes
const MAX_RESPONSE_BODY_TOTAL_BUFFER_LENGTH = 250 * 1024 * 1024; // bytes
export class FetchDispatcher implements ProtocolProxyApi.FetchDispatcher {
readonly #fetchAgent: ProtocolProxyApi.FetchApi;
readonly #manager: NetworkManager;
constructor(agent: ProtocolProxyApi.FetchApi, manager: NetworkManager) {
this.#fetchAgent = agent;
this.#manager = manager;
}
requestPaused({requestId, request, resourceType, responseStatusCode, responseHeaders, networkId}:
Protocol.Fetch.RequestPausedEvent): void {
const networkRequest = networkId ? this.#manager.requestForId(networkId) : null;
// If there was no 'Network.responseReceivedExtraInfo' event (e.g. for 'file:/' URLSs),
// populate 'originalResponseHeaders' with the headers from the 'Fetch.requestPaused' event.
if (networkRequest?.originalResponseHeaders.length === 0 && responseHeaders) {
networkRequest.originalResponseHeaders = responseHeaders;
}
void MultitargetNetworkManager.instance().requestIntercepted(new InterceptedRequest(
this.#fetchAgent, request, resourceType, requestId, networkRequest, responseStatusCode, responseHeaders));
}
authRequired({}: Protocol.Fetch.AuthRequiredEvent): void {
}
}
export class NetworkDispatcher implements ProtocolProxyApi.NetworkDispatcher {
readonly #manager: NetworkManager;
readonly #requestsById = new Map<string, NetworkRequest>();
readonly #requestsByURL = new Map<Platform.DevToolsPath.UrlString, NetworkRequest>();
readonly #requestsByLoaderId = new Map<Protocol.Network.LoaderId, NetworkRequest>();
readonly #requestIdToExtraInfoBuilder = new Map<string, ExtraInfoBuilder>();
/**
* In case of an early abort or a cache hit, the Trust Token done event is
* reported before the request itself is created in `requestWillBeSent`.
* This causes the event to be lost as no `NetworkRequest` instance has been
* created yet.
* This map caches the events temporarily and populates the NetworkRequest
* once it is created in `requestWillBeSent`.
*/
readonly #requestIdToTrustTokenEvent = new Map<string, Protocol.Network.TrustTokenOperationDoneEvent>();
constructor(manager: NetworkManager) {
this.#manager = manager;
MultitargetNetworkManager.instance().addEventListener(
MultitargetNetworkManager.Events.REQUEST_INTERCEPTED, this.#markAsIntercepted.bind(this));
}
#markAsIntercepted(event: Common.EventTarget.EventTargetEvent<string>): void {
const request = this.requestForId(event.data);
if (request) {
request.setWasIntercepted(true);
}
}
private headersMapToHeadersArray(headersMap: Protocol.Network.Headers): NameValue[] {
const result = [];
for (const name in headersMap) {
const values = headersMap[name].split('\n');
for (let i = 0; i < values.length; ++i) {
result.push({name, value: values[i]});
}
}
return result;
}
private updateNetworkRequestWithRequest(networkRequest: NetworkRequest, request: Protocol.Network.Request): void {
networkRequest.requestMethod = request.method;
networkRequest.setRequestHeaders(this.headersMapToHeadersArray(request.headers));
networkRequest.setRequestFormData(Boolean(request.hasPostData), request.postData || null);
networkRequest.setInitialPriority(request.initialPriority);
networkRequest.mixedContentType = request.mixedContentType || Protocol.Security.MixedContentType.None;
networkRequest.setReferrerPolicy(request.referrerPolicy);
networkRequest.setIsSameSite(request.isSameSite || false);
networkRequest.setIsAdRelated(request.isAdRelated || false);
}
private updateNetworkRequestWithResponse(networkRequest: NetworkRequest, response: Protocol.Network.Response): void {
if (response.url && networkRequest.url() !== response.url) {
networkRequest.setUrl(response.url as Platform.DevToolsPath.UrlString);
}
networkRequest.mimeType = response.mimeType;
networkRequest.setCharset(response.charset);
if (!networkRequest.statusCode || networkRequest.wasIntercepted()) {
networkRequest.statusCode = response.status;
}
if (!networkRequest.statusText || networkRequest.wasIntercepted()) {
networkRequest.statusText = response.statusText;
}
if (!networkRequest.hasExtraResponseInfo() || networkRequest.wasIntercepted()) {
networkRequest.responseHeaders = this.headersMapToHeadersArray(response.headers);
}
if (response.encodedDataLength >= 0) {
networkRequest.setTransferSize(response.encodedDataLength);
}
if (response.requestHeaders && !networkRequest.hasExtraRequestInfo()) {
// TODO(http://crbug.com/1004979): Stop using response.requestHeaders and
// response.requestHeadersText once shared workers
// emit Network.*ExtraInfo events for their network #requests.
networkRequest.setRequestHeaders(this.headersMapToHeadersArray(response.requestHeaders));
networkRequest.setRequestHeadersText(response.requestHeadersText || '');
}
networkRequest.connectionReused = response.connectionReused;
networkRequest.connectionId = String(response.connectionId);
if (response.remoteIPAddress) {
networkRequest.setRemoteAddress(response.remoteIPAddress, response.remotePort || -1);
}
if (response.fromServiceWorker) {
networkRequest.fetchedViaServiceWorker = true;
}
if (response.fromDiskCache) {
networkRequest.setFromDiskCache();
}
if (response.fromPrefetchCache) {
networkRequest.setFromPrefetchCache();
}
if (response.fromEarlyHints) {
networkRequest.setFromEarlyHints();
}
if (response.cacheStorageCacheName) {
networkRequest.setResponseCacheStorageCacheName(response.cacheStorageCacheName);
}
if (response.serviceWorkerRouterInfo) {
networkRequest.serviceWorkerRouterInfo = response.serviceWorkerRouterInfo;
}
if (response.responseTime) {
networkRequest.setResponseRetrievalTime(new Date(response.responseTime));
}
networkRequest.timing = response.timing;
networkRequest.protocol = response.protocol || '';
networkRequest.alternateProtocolUsage = response.alternateProtocolUsage;
if (response.serviceWorkerResponseSource) {
networkRequest.setServiceWorkerResponseSource(response.serviceWorkerResponseSource);
}
networkRequest.setSecurityState(response.securityState);
if (response.securityDetails) {
networkRequest.setSecurityDetails(response.securityDetails);
}
const newResourceType = Common.ResourceType.ResourceType.fromMimeTypeOverride(networkRequest.mimeType);
if (newResourceType) {
networkRequest.setResourceType(newResourceType);
}
if (networkRequest.responseReceivedPromiseResolve) {
// Anyone interested in waiting for response headers being available?
networkRequest.responseReceivedPromiseResolve();
} else {
// If not, make sure no one will wait on it in the future.
networkRequest.responseReceivedPromise = Promise.resolve();
}
}
requestForId(id: string): NetworkRequest|null {
return this.#requestsById.get(id) || null;
}
requestForURL(url: Platform.DevToolsPath.UrlString): NetworkRequest|null {
return this.#requestsByURL.get(url) || null;
}
requestForLoaderId(loaderId: Protocol.Network.LoaderId): NetworkRequest|null {
return this.#requestsByLoaderId.get(loaderId) || null;
}
resourceChangedPriority({requestId, newPriority}: Protocol.Network.ResourceChangedPriorityEvent): void {
const networkRequest = this.#requestsById.get(requestId);
if (networkRequest) {
networkRequest.setPriority(newPriority);
}
}
signedExchangeReceived({requestId, info}: Protocol.Network.SignedExchangeReceivedEvent): void {
// While loading a signed exchange, a signedExchangeReceived event is sent
// between two requestWillBeSent events.
// 1. The first requestWillBeSent is sent while starting the navigation (or
// prefetching).
// 2. This signedExchangeReceived event is sent when the browser detects the
// signed exchange.
// 3. The second requestWillBeSent is sent with the generated redirect
// response and a new redirected request which URL is the inner request
// URL of the signed exchange.
let networkRequest = this.#requestsById.get(requestId);
// |requestId| is available only for navigation #requests. If the request was
// sent from a renderer process for prefetching, it is not available. In the
// case, need to fallback to look for the URL.
// TODO(crbug/841076): Sends the request ID of prefetching to the browser
// process and DevTools to find the matching request.
if (!networkRequest) {
networkRequest = this.#requestsByURL.get(info.outerResponse.url as Platform.DevToolsPath.UrlString);
if (!networkRequest) {
return;
}
// Or clause is never hit, but is here because we can't use non-null assertions.
const backendRequestId = networkRequest.backendRequestId() || requestId;
requestId = backendRequestId;
}
networkRequest.setSignedExchangeInfo(info);
networkRequest.setResourceType(Common.ResourceType.resourceTypes.SignedExchange);
this.updateNetworkRequestWithResponse(networkRequest, info.outerResponse);
this.updateNetworkRequest(networkRequest);
this.getExtraInfoBuilder(requestId).addHasExtraInfo(info.hasExtraInfo);
this.#manager.dispatchEventToListeners(
Events.ResponseReceived, {request: networkRequest, response: info.outerResponse});
}
requestWillBeSent({
requestId,
loaderId,
documentURL,
request,
timestamp,
wallTime,
initiator,
redirectHasExtraInfo,
redirectResponse,
type,
frameId,
hasUserGesture,
}: Protocol.Network.RequestWillBeSentEvent): void {
let networkRequest = this.#requestsById.get(requestId);
if (networkRequest) {
// FIXME: move this check to the backend.
if (!redirectResponse) {
return;
}
// If signedExchangeReceived event has already been sent for the request,
// ignores the internally generated |redirectResponse|. The
// |outerResponse| of SignedExchangeInfo was set to |networkRequest| in
// signedExchangeReceived().
if (!networkRequest.signedExchangeInfo()) {
this.responseReceived({
requestId,
loaderId,
timestamp,
type: type || Protocol.Network.ResourceType.Other,
response: redirectResponse,
hasExtraInfo: redirectHasExtraInfo,
frameId,
});
}
networkRequest = this.appendRedirect(requestId, timestamp, request.url as Platform.DevToolsPath.UrlString);
this.#manager.dispatchEventToListeners(Events.RequestRedirected, networkRequest);
} else {
networkRequest = NetworkRequest.create(
requestId, request.url as Platform.DevToolsPath.UrlString, documentURL as Platform.DevToolsPath.UrlString,
frameId ?? null, loaderId, initiator, hasUserGesture);
requestToManagerMap.set(networkRequest, this.#manager);
}
networkRequest.hasNetworkData = true;
this.updateNetworkRequestWithRequest(networkRequest, request);
networkRequest.setIssueTime(timestamp, wallTime);
networkRequest.setResourceType(
type ? Common.ResourceType.resourceTypes[type] : Common.ResourceType.resourceTypes.Other);
if (request.trustTokenParams) {
networkRequest.setTrustTokenParams(request.trustTokenParams);
}
const maybeTrustTokenEvent = this.#requestIdToTrustTokenEvent.get(requestId);
if (maybeTrustTokenEvent) {
networkRequest.setTrustTokenOperationDoneEvent(maybeTrustTokenEvent);
this.#requestIdToTrustTokenEvent.delete(requestId);
}
this.getExtraInfoBuilder(requestId).addRequest(networkRequest);
this.startNetworkRequest(networkRequest, request);
}
requestServedFromCache({requestId}: Protocol.Network.RequestServedFromCacheEvent): void {
const networkRequest = this.#requestsById.get(requestId);
if (!networkRequest) {
return;
}
networkRequest.setFromMemoryCache();
}
responseReceived({requestId, loaderId, timestamp, type, response, hasExtraInfo, frameId}:
Protocol.Network.ResponseReceivedEvent): void {
const networkRequest = this.#requestsById.get(requestId);
const lowercaseHeaders = NetworkManager.lowercaseHeaders(response.headers);
if (!networkRequest) {
const lastModifiedHeader = lowercaseHeaders['last-modified'];
// We missed the requestWillBeSent.
const eventData: RequestUpdateDroppedEventData = {
url: response.url as Platform.DevToolsPath.UrlString,
frameId: frameId ?? null,
loaderId,
resourceType: type,
mimeType: response.mimeType,
lastModified: lastModifiedHeader ? new Date(lastModifiedHeader) : null,
};
this.#manager.dispatchEventToListeners(Events.RequestUpdateDropped, eventData);
return;
}
networkRequest.responseReceivedTime = timestamp;
networkRequest.setResourceType(Common.ResourceType.resourceTypes[type]);
this.updateNetworkRequestWithResponse(networkRequest, response);
this.updateNetworkRequest(networkRequest);
this.getExtraInfoBuilder(requestId).addHasExtraInfo(hasExtraInfo);
this.#manager.dispatchEventToListeners(Events.ResponseReceived, {request: networkRequest, response});
}
dataReceived(event: Protocol.Network.DataReceivedEvent): void {
let networkRequest: NetworkRequest|null|undefined = this.#requestsById.get(event.requestId);
if (!networkRequest) {
networkRequest = this.maybeAdoptMainResourceRequest(event.requestId);
}
if (!networkRequest) {
return;
}
networkRequest.addDataReceivedEvent(event);
this.updateNetworkRequest(networkRequest);
}
loadingFinished({requestId, timestamp: finishTime, encodedDataLength}: Protocol.Network.LoadingFinishedEvent): void {
let networkRequest: NetworkRequest|null|undefined = this.#requestsById.get(requestId);
if (!networkRequest) {
networkRequest = this.maybeAdoptMainResourceRequest(requestId);
}
if (!networkRequest) {
return;
}
this.getExtraInfoBuilder(requestId).finished();
this.finishNetworkRequest(networkRequest, finishTime, encodedDataLength);
this.#manager.dispatchEventToListeners(Events.LoadingFinished, networkRequest);
}
loadingFailed({
requestId,
timestamp: time,
type: resourceType,
errorText: localizedDescription,
canceled,
blockedReason,
corsErrorStatus,
}: Protocol.Network.LoadingFailedEvent): void {
const networkRequest = this.#requestsById.get(requestId);
if (!networkRequest) {
return;
}
networkRequest.failed = true;
networkRequest.setResourceType(Common.ResourceType.resourceTypes[resourceType]);
networkRequest.canceled = Boolean(canceled);
if (blockedReason) {
networkRequest.setBlockedReason(blockedReason);
if (blockedReason === Protocol.Network.BlockedReason.Inspector) {
const message = i18nString(UIStrings.requestWasBlockedByDevtoolsS, {PH1: networkRequest.url()});
this.#manager.dispatchEventToListeners(Events.MessageGenerated, {message, requestId, warning: true});
}
}
if (corsErrorStatus) {
networkRequest.setCorsErrorStatus(corsErrorStatus);
}
networkRequest.localizedFailDescription = localizedDescription;
this.getExtraInfoBuilder(requestId).finished();
this.finishNetworkRequest(networkRequest, time, -1);
}
webSocketCreated({requestId, url: requestURL, initiator}: Protocol.Network.WebSocketCreatedEvent): void {
const networkRequest =
NetworkRequest.createForSocket(requestId, requestURL as Platform.DevToolsPath.UrlString, initiator);
requestToManagerMap.set(networkRequest, this.#manager);
networkRequest.setResourceType(Common.ResourceType.resourceTypes.WebSocket);
this.startNetworkRequest(networkRequest, null);
}
webSocketWillSendHandshakeRequest({requestId, timestamp: time, wallTime, request}:
Protocol.Network.WebSocketWillSendHandshakeRequestEvent): void {
const networkRequest = this.#requestsById.get(requestId);
if (!networkRequest) {
return;
}
networkRequest.requestMethod = 'GET';
networkRequest.setRequestHeaders(this.headersMapToHeadersArray(request.headers));
networkRequest.setIssueTime(time, wallTime);
this.updateNetworkRequest(networkRequest);
}
webSocketHandshakeResponseReceived({requestId, timestamp: time, response}:
Protocol.Network.WebSocketHandshakeResponseReceivedEvent): void {
const networkRequest = this.#requestsById.get(requestId);
if (!networkRequest) {
return;
}
networkRequest.statusCode = response.status;
networkRequest.statusText = response.statusText;
networkRequest.responseHeaders = this.headersMapToHeadersArray(response.headers);
networkRequest.responseHeadersText = response.headersText || '';
if (response.requestHeaders) {
networkRequest.setRequestHeaders(this.headersMapToHeadersArray(response.requestHeaders));
}
if (response.requestHeadersText) {
networkRequest.setRequestHeadersText(response.requestHeadersText);
}
networkRequest.responseReceivedTime = time;
networkRequest.protocol = 'websocket';
this.updateNetworkRequest(networkRequest);
}
webSocketFrameReceived({requestId, timestamp: time, response}: Protocol.Network.WebSocketFrameReceivedEvent): void {
const networkRequest = this.#requestsById.get(requestId);
if (!networkRequest) {
return;
}
networkRequest.addProtocolFrame(response, time, false);
networkRequest.responseReceivedTime = time;
this.updateNetworkRequest(networkRequest);
}
webSocketFrameSent({requestId, timestamp: time, response}: Protocol.Network.WebSocketFrameSentEvent): void {
const networkRequest = this.#requestsById.get(requestId);
if (!networkRequest) {
return;
}
networkRequest.addProtocolFrame(response, time, true);
networkRequest.responseReceivedTime = time;
this.updateNetworkRequest(networkRequest);
}
webSocketFrameError({requestId, timestamp: time, errorMessage}: Protocol.Network.WebSocketFrameErrorEvent): void {
const networkRequest = this.#requestsById.get(requestId);
if (!networkRequest) {
return;
}
networkRequest.addProtocolFrameError(errorMessage, time);
networkRequest.responseReceivedTime = time;
this.updateNetworkRequest(networkRequest);
}
webSocketClosed({requestId, timestamp: time}: Protocol.Network.WebSocketClosedEvent): void {
const networkRequest = this.#requestsById.get(requestId);
if (!networkRequest) {
return;
}
this.finishNetworkRequest(networkRequest, time, -1);
}
eventSourceMessageReceived({requestId, timestamp: time, eventName, eventId, data}:
Protocol.Network.EventSourceMessageReceivedEvent): void {
const networkRequest = this.#requestsById.get(requestId);
if (!networkRequest) {
return;
}
networkRequest.addEventSourceMessage(time, eventName, eventId, data);
}
requestIntercepted({}: Protocol.Network.RequestInterceptedEvent): void {
}
requestWillBeSentExtraInfo({
requestId,
associatedCookies,
headers,
clientSecurityState,
connectTiming,
siteHasCookieInOtherPartition,
appliedNetworkConditionsId
}: Protocol.Network.RequestWillBeSentExtraInfoEvent): void {
const blockedRequestCookies: BlockedCookieWithReason[] = [];
const includedRequestCookies: IncludedCookieWithReason[] = [];
for (const {blockedReasons, exemptionReason, cookie} of associatedCookies) {
if (blockedReasons.length === 0) {
includedRequestCookies.push({exemptionReason, cookie: Cookie.fromProtocolCookie(cookie)});
} else {
blockedRequestCookies.push({blockedReasons, cookie: Cookie.fromProtocolCookie(cookie)});
}
}
const extraRequestInfo = {
blockedRequestCookies,
includedRequestCookies,
requestHeaders: this.headersMapToHeadersArray(headers),
clientSecurityState,
connectTiming,
siteHasCookieInOtherPartition,
appliedNetworkConditionsId,
};
this.getExtraInfoBuilder(requestId).addRequestExtraInfo(extraRequestInfo);
const networkRequest = this.#requestsById.get(requestId);
if (appliedNetworkConditionsId && networkRequest) {
networkRequest.setAppliedNetworkConditions(appliedNetworkConditionsId);
this.updateNetworkRequest(networkRequest);
}
}
responseReceivedEarlyHints({
requestId,
headers,
}: Protocol.Network.ResponseReceivedEarlyHintsEvent): void {
this.getExtraInfoBuilder(requestId).setEarlyHintsHeaders(this.headersMapToHeadersArray(headers));
}
responseReceivedExtraInfo({
requestId,
blockedCookies,
headers,
headersText,
resourceIPAddressSpace,
statusCode,
cookiePartitionKey,
cookiePartitionKeyOpaque,
exemptedCookies,
}: Protocol.Network.ResponseReceivedExtraInfoEvent): void {
const extraResponseInfo: ExtraResponseInfo = {
blockedResponseCookies:
blockedCookies.map(blockedCookie => ({
blockedReasons: blockedCookie.blockedReasons,
cookieLine: blockedCookie.cookieLine,
cookie: blockedCookie.cookie ? Cookie.fromProtocolCookie(blockedCookie.cookie) : null,
})),
responseHeaders: this.headersMapToHeadersArray(headers),
responseHeadersText: headersText,
resourceIPAddressSpace,
statusCode,
cookiePartitionKey,
cookiePartitionKeyOpaque,
exemptedResponseCookies: exemptedCookies?.map(exemptedCookie => ({
cookie: Cookie.fromProtocolCookie(exemptedCookie.cookie),
cookieLine: exemptedCookie.cookieLine,
exemptionReason: exemptedCookie.exemptionReason,
})),
};
this.getExtraInfoBuilder(requestId).addResponseExtraInfo(extraResponseInfo);
}
private getExtraInfoBuilder(requestId: string): ExtraInfoBuilder {
let builder: ExtraInfoBuilder;
if (!this.#requestIdToExtraInfoBuilder.has(requestId)) {
builder = new ExtraInfoBuilder();
this.#requestIdToExtraInfoBuilder.set(requestId, builder);
} else {
builder = (this.#requestIdToExtraInfoBuilder.get(requestId) as ExtraInfoBuilder);
}
return builder;
}
private appendRedirect(
requestId: Protocol.Network.RequestId, time: number,
redirectURL: Platform.DevToolsPath.UrlString): NetworkRequest {
const originalNetworkRequest = this.#requestsById.get(requestId);
if (!originalNetworkRequest) {
throw new Error(`Could not find original network request for ${requestId}`);
}
let redirectCount = 0;
for (let redirect = originalNetworkRequest.redirectSource(); redirect; redirect = redirect.redirectSource()) {
redirectCount++;
}
originalNetworkRequest.markAsRedirect(redirectCount);
this.finishNetworkRequest(originalNetworkRequest, time, -1);
const newNetworkRequest = NetworkRequest.create(
requestId, redirectURL, originalNetworkRequest.documentURL, originalNetworkRequest.frameId,
originalNetworkRequest.loaderId, originalNetworkRequest.initiator(),
originalNetworkRequest.hasUserGesture() ?? undefined);
requestToManagerMap.set(newNetworkRequest, this.#manager);
newNetworkRequest.setRedirectSource(originalNetworkRequest);
originalNetworkRequest.setRedirectDestination(newNetworkRequest);
return newNetworkRequest;
}
private maybeAdoptMainResourceRequest(requestId: string): NetworkRequest|null {
const request = MultitargetNetworkManager.instance().inflightMainResourceRequests.get(requestId);
if (!request) {
return null;
}
const oldDispatcher = (NetworkManager.forRequest(request) as NetworkManager).dispatcher;
oldDispatcher.#requestsById.delete(requestId);
oldDispatcher.#requestsByURL.delete(request.url());
const loaderId = request.loaderId;
if (loaderId) {
oldDispatcher.#requestsByLoaderId.delete(loaderId);
}
const builder = oldDispatcher.#requestIdToExtraInfoBuilder.get(requestId);
oldDispatcher.#requestIdToExtraInfoBuilder.delete(requestId);
this.#requestsById.set(requestId, request);
this.#requestsByURL.set(request.url(), request);
if (loaderId) {
this.#requestsByLoaderId.set(loaderId, request);
}
if (builder) {
this.#requestIdToExtraInfoBuilder.set(requestId, builder);
}
requestToManagerMap.set(request, this.#manager);
return request;
}
private startNetworkRequest(networkRequest: NetworkRequest, originalRequest: Protocol.Network.Request|null): void {
this.#requestsById.set(networkRequest.requestId(), networkRequest);
this.#requestsByURL.set(networkRequest.url(), networkRequest);
const loaderId = networkRequest.loaderId;
if (loaderId) {
this.#requestsByLoaderId.set(loaderId, networkRequest);
}
// The following relies on the fact that loaderIds and requestIds
// are globally unique and that the main request has them equal. If
// loaderId is an empty string, it indicates a worker request. For the
// request to fetch the main worker script, the request ID is the future
// worker target ID and, therefore, it is unique.
if (networkRequest.loaderId === networkRequest.requestId() || networkRequest.loaderId === '') {
MultitargetNetworkManager.instance().inflightMainResourceRequests.set(networkRequest.requestId(), networkRequest);
}
this.#manager.dispatchEventToListeners(Events.RequestStarted, {request: networkRequest, originalRequest});
}
private updateNetworkRequest(networkRequest: NetworkRequest): void {
this.#manager.dispatchEventToListeners(Events.RequestUpdated, networkRequest);
}
private finishNetworkRequest(
networkRequest: NetworkRequest,
finishTime: number,
encodedDataLength: number,
): void {
networkRequest.endTime = finishTime;
networkRequest.finished = true;
if (encodedDataLength >= 0) {
const redirectSource = networkRequest.redirectSource();
if (redirectSource?.signedExchangeInfo()) {
networkRequest.setTransferSize(0);
redirectSource.setTransferSize(encodedDataLength);
this.updateNetworkRequest(redirectSource);
} else {
networkRequest.setTransferSize(encodedDataLength);
}
}
this.#manager.dispatchEventToListeners(Events.RequestFinished, networkRequest);
MultitargetNetworkManager.instance().inflightMainResourceRequests.delete(networkRequest.requestId());
if (Common.Settings.Settings.instance().moduleSetting('monitoring-xhr-enabled').get() &&
networkRequest.resourceType().category() === Common.ResourceType.resourceCategories.XHR) {
let message;
const failedToLoad = networkRequest.failed || networkRequest.hasErrorStatusCode();
if (failedToLoad) {
message = i18nString(
UIStrings.sFailedLoadingSS,
{PH1: networkRequest.resourceType().title(), PH2: networkRequest.requestMethod, PH3: networkRequest.url()});
} else {
message = i18nString(
UIStrings.sFinishedLoadingSS,
{PH1: networkRequest.resourceType().title(), PH2: networkRequest.requestMethod, PH3: networkRequest.url()});
}
this.#manager.dispatchEventToListeners(
Events.MessageGenerated, {message, requestId: networkRequest.requestId(), warning: false});
}
}
clearRequests(): void {
for (const [requestId, request] of this.#requestsById) {
if (request.finished) {
this.#requestsById.delete(requestId);
}
}
for (const [requestURL, request] of this.#requestsByURL) {
if (request.finished) {
this.#requestsByURL.delete(requestURL);
}
}
for (const [requestLoaderId, request] of this.#requestsByLoaderId) {
if (request.finished) {
this.#requestsByLoaderId.delete(requestLoaderId);
}
}
for (const [requestId, builder] of this.#requestIdToExtraInfoBuilder) {
if (builder.isFinished()) {
this.#requestIdToExtraInfoBuilder.delete(requestId);
}
}
}
webTransportCreated({transportId, url: requestURL, timestamp: time, initiator}:
Protocol.Network.WebTransportCreatedEvent): void {
const networkRequest =
NetworkRequest.createForSocket(transportId, requestURL as Platform.DevToolsPath.UrlString, initiator);
networkRequest.hasNetworkData = true;
requestToManagerMap.set(networkRequest, this.#manager);
networkRequest.setResourceType(Common.ResourceType.resourceTypes.WebTransport);
networkRequest.setIssueTime(time, 0);
// TODO(yoichio): Add appropreate events to address abort cases.
this.startNetworkRequest(networkRequest, null);
}
webTransportConnectionEstablished({transportId, timestamp: time}:
Protocol.Network.WebTransportConnectionEstablishedEvent): void {