chore: make CRNetworkManager handle multiple sessions (#29721)

It was already handling worker sessions, but not OOPIFs. As a result,
some functionality was properly implemented only for OOPIFs and not for
workers.

This change removes OOPIFs fanout for network-related calls from CRPage
and moves that to the CRNetworkManager, now also covering workers.
This commit is contained in:
Dmitry Gozman 2024-02-28 15:51:27 -08:00 committed by GitHub
parent 52b803ecf5
commit aedd7ca0be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 172 additions and 126 deletions

View File

@ -562,7 +562,7 @@ export class CRBrowserContext extends BrowserContext {
override async clearCache(): Promise<void> { override async clearCache(): Promise<void> {
for (const page of this._crPages()) for (const page of this._crPages())
await page._mainFrameSession._networkManager.clearCache(); await page._networkManager.clearCache();
} }
async cancelDownload(guid: string) { async cancelDownload(guid: string) {

View File

@ -26,70 +26,88 @@ import type * as contexts from '../browserContext';
import type * as frames from '../frames'; import type * as frames from '../frames';
import type * as types from '../types'; import type * as types from '../types';
import type { CRPage } from './crPage'; import type { CRPage } from './crPage';
import { assert, headersObjectToArray } from '../../utils'; import { assert, headersArrayToObject, headersObjectToArray } from '../../utils';
import type { CRServiceWorker } from './crServiceWorker'; import type { CRServiceWorker } from './crServiceWorker';
import { isProtocolError } from '../protocolError'; import { isProtocolError, isSessionClosedError } from '../protocolError';
type SessionInfo = { type SessionInfo = {
session: CRSession; session: CRSession;
isMain?: boolean;
workerFrame?: frames.Frame; workerFrame?: frames.Frame;
eventListeners: RegisteredListener[];
}; };
export class CRNetworkManager { export class CRNetworkManager {
private _session: CRSession;
private _page: Page | null; private _page: Page | null;
private _serviceWorker: CRServiceWorker | null; private _serviceWorker: CRServiceWorker | null;
private _parentManager: CRNetworkManager | null;
private _requestIdToRequest = new Map<string, InterceptableRequest>(); private _requestIdToRequest = new Map<string, InterceptableRequest>();
private _requestIdToRequestWillBeSentEvent = new Map<string, Protocol.Network.requestWillBeSentPayload>(); private _requestIdToRequestWillBeSentEvent = new Map<string, Protocol.Network.requestWillBeSentPayload>();
private _credentials: {origin?: string, username: string, password: string} | null = null; private _credentials: {origin?: string, username: string, password: string} | null = null;
private _attemptedAuthentications = new Set<string>(); private _attemptedAuthentications = new Set<string>();
private _userRequestInterceptionEnabled = false; private _userRequestInterceptionEnabled = false;
private _protocolRequestInterceptionEnabled = false; private _protocolRequestInterceptionEnabled = false;
private _offline = false;
private _extraHTTPHeaders: types.HeadersArray = [];
private _requestIdToRequestPausedEvent = new Map<string, Protocol.Fetch.requestPausedPayload>(); private _requestIdToRequestPausedEvent = new Map<string, Protocol.Fetch.requestPausedPayload>();
private _eventListeners: RegisteredListener[];
private _responseExtraInfoTracker = new ResponseExtraInfoTracker(); private _responseExtraInfoTracker = new ResponseExtraInfoTracker();
private _sessions = new Map<CRSession, SessionInfo>();
constructor(session: CRSession, page: Page | null, serviceWorker: CRServiceWorker | null, parentManager: CRNetworkManager | null) { constructor(page: Page | null, serviceWorker: CRServiceWorker | null) {
this._session = session;
this._page = page; this._page = page;
this._serviceWorker = serviceWorker; this._serviceWorker = serviceWorker;
this._parentManager = parentManager;
this._eventListeners = this.instrumentNetworkEvents({ session });
} }
instrumentNetworkEvents(sessionInfo: SessionInfo): RegisteredListener[] { async addSession(session: CRSession, workerFrame?: frames.Frame, isMain?: boolean) {
const listeners = [ const sessionInfo: SessionInfo = { session, isMain, workerFrame, eventListeners: [] };
eventsHelper.addEventListener(sessionInfo.session, 'Fetch.requestPaused', this._onRequestPaused.bind(this, sessionInfo)), sessionInfo.eventListeners = [
eventsHelper.addEventListener(sessionInfo.session, 'Fetch.authRequired', this._onAuthRequired.bind(this)), eventsHelper.addEventListener(session, 'Fetch.requestPaused', this._onRequestPaused.bind(this, sessionInfo)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this, sessionInfo)), eventsHelper.addEventListener(session, 'Fetch.authRequired', this._onAuthRequired.bind(this, sessionInfo)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.requestWillBeSentExtraInfo', this._onRequestWillBeSentExtraInfo.bind(this)), eventsHelper.addEventListener(session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this, sessionInfo)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.requestServedFromCache', this._onRequestServedFromCache.bind(this)), eventsHelper.addEventListener(session, 'Network.requestWillBeSentExtraInfo', this._onRequestWillBeSentExtraInfo.bind(this)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.responseReceived', this._onResponseReceived.bind(this, sessionInfo)), eventsHelper.addEventListener(session, 'Network.requestServedFromCache', this._onRequestServedFromCache.bind(this)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.responseReceivedExtraInfo', this._onResponseReceivedExtraInfo.bind(this)), eventsHelper.addEventListener(session, 'Network.responseReceived', this._onResponseReceived.bind(this, sessionInfo)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.loadingFinished', this._onLoadingFinished.bind(this)), eventsHelper.addEventListener(session, 'Network.responseReceivedExtraInfo', this._onResponseReceivedExtraInfo.bind(this)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.loadingFailed', this._onLoadingFailed.bind(this, sessionInfo)), eventsHelper.addEventListener(session, 'Network.loadingFinished', this._onLoadingFinished.bind(this, sessionInfo)),
eventsHelper.addEventListener(session, 'Network.loadingFailed', this._onLoadingFailed.bind(this, sessionInfo)),
]; ];
if (this._page) { if (this._page) {
listeners.push(...[ sessionInfo.eventListeners.push(...[
eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketCreated', e => this._page!._frameManager.onWebSocketCreated(e.requestId, e.url)), eventsHelper.addEventListener(session, 'Network.webSocketCreated', e => this._page!._frameManager.onWebSocketCreated(e.requestId, e.url)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketWillSendHandshakeRequest', e => this._page!._frameManager.onWebSocketRequest(e.requestId)), eventsHelper.addEventListener(session, 'Network.webSocketWillSendHandshakeRequest', e => this._page!._frameManager.onWebSocketRequest(e.requestId)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketHandshakeResponseReceived', e => this._page!._frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)), eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page!._frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page!._frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)), eventsHelper.addEventListener(session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page!._frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page!._frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)), eventsHelper.addEventListener(session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page!._frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketClosed', e => this._page!._frameManager.webSocketClosed(e.requestId)), eventsHelper.addEventListener(session, 'Network.webSocketClosed', e => this._page!._frameManager.webSocketClosed(e.requestId)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketFrameError', e => this._page!._frameManager.webSocketError(e.requestId, e.errorMessage)), eventsHelper.addEventListener(session, 'Network.webSocketFrameError', e => this._page!._frameManager.webSocketError(e.requestId, e.errorMessage)),
]); ]);
} }
return listeners; this._sessions.set(session, sessionInfo);
await Promise.all([
session.send('Network.enable'),
this._updateProtocolRequestInterceptionForSession(sessionInfo, true /* initial */),
this._setOfflineForSession(sessionInfo, true /* initial */),
this._setExtraHTTPHeadersForSession(sessionInfo, true /* initial */),
]);
} }
async initialize() { removeSession(session: CRSession) {
await this._session.send('Network.enable'); const info = this._sessions.get(session);
if (info)
eventsHelper.removeEventListeners(info.eventListeners);
this._sessions.delete(session);
} }
dispose() { private async _forEachSession(cb: (sessionInfo: SessionInfo) => Promise<any>) {
eventsHelper.removeEventListeners(this._eventListeners); await Promise.all([...this._sessions.values()].map(info => {
if (info.isMain)
return cb(info);
return cb(info).catch(e => {
// Broadcasting a message to the closed target should be a noop.
if (isSessionClosedError(e))
return;
throw e;
});
}));
} }
async authenticate(credentials: types.Credentials | null) { async authenticate(credentials: types.Credentials | null) {
@ -98,8 +116,20 @@ export class CRNetworkManager {
} }
async setOffline(offline: boolean) { async setOffline(offline: boolean) {
await this._session.send('Network.emulateNetworkConditions', { if (offline === this._offline)
offline, return;
this._offline = offline;
await this._forEachSession(info => this._setOfflineForSession(info));
}
private async _setOfflineForSession(info: SessionInfo, initial?: boolean) {
if (initial && !this._offline)
return;
// Workers are affected by the owner frame's Network.emulateNetworkConditions.
if (info.workerFrame)
return;
await info.session.send('Network.emulateNetworkConditions', {
offline: this._offline,
// values of 0 remove any active throttling. crbug.com/456324#c9 // values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0, latency: 0,
downloadThroughput: -1, downloadThroughput: -1,
@ -117,28 +147,46 @@ export class CRNetworkManager {
if (enabled === this._protocolRequestInterceptionEnabled) if (enabled === this._protocolRequestInterceptionEnabled)
return; return;
this._protocolRequestInterceptionEnabled = enabled; this._protocolRequestInterceptionEnabled = enabled;
if (enabled) { await this._forEachSession(info => this._updateProtocolRequestInterceptionForSession(info));
await Promise.all([ }
this._session.send('Network.setCacheDisabled', { cacheDisabled: true }),
this._session.send('Fetch.enable', { private async _updateProtocolRequestInterceptionForSession(info: SessionInfo, initial?: boolean) {
handleAuthRequests: true, const enabled = this._protocolRequestInterceptionEnabled;
patterns: [{ urlPattern: '*', requestStage: 'Request' }], if (initial && !enabled)
}), return;
]); const cachePromise = info.session.send('Network.setCacheDisabled', { cacheDisabled: enabled });
} else { let fetchPromise = Promise.resolve<any>(undefined);
await Promise.all([ if (!info.workerFrame) {
this._session.send('Network.setCacheDisabled', { cacheDisabled: false }), if (enabled)
this._session.send('Fetch.disable') fetchPromise = info.session.send('Fetch.enable', { handleAuthRequests: true, patterns: [{ urlPattern: '*', requestStage: 'Request' }] });
]); else
fetchPromise = info.session.send('Fetch.disable');
} }
await Promise.all([cachePromise, fetchPromise]);
}
async setExtraHTTPHeaders(extraHTTPHeaders: types.HeadersArray) {
if (!this._extraHTTPHeaders.length && !extraHTTPHeaders.length)
return;
this._extraHTTPHeaders = extraHTTPHeaders;
await this._forEachSession(info => this._setExtraHTTPHeadersForSession(info));
}
private async _setExtraHTTPHeadersForSession(info: SessionInfo, initial?: boolean) {
if (initial && !this._extraHTTPHeaders.length)
return;
await info.session.send('Network.setExtraHTTPHeaders', { headers: headersArrayToObject(this._extraHTTPHeaders, false /* lowerCase */) });
} }
async clearCache() { async clearCache() {
// Sending 'Network.setCacheDisabled' with 'cacheDisabled = true' will clear the MemoryCache. await this._forEachSession(async info => {
await this._session.send('Network.setCacheDisabled', { cacheDisabled: true }); // Sending 'Network.setCacheDisabled' with 'cacheDisabled = true' will clear the MemoryCache.
if (!this._protocolRequestInterceptionEnabled) await info.session.send('Network.setCacheDisabled', { cacheDisabled: true });
await this._session.send('Network.setCacheDisabled', { cacheDisabled: false }); if (!this._protocolRequestInterceptionEnabled)
await this._session.send('Network.clearBrowserCache'); await info.session.send('Network.setCacheDisabled', { cacheDisabled: false });
if (!info.workerFrame)
await info.session.send('Network.clearBrowserCache');
});
} }
_onRequestWillBeSent(sessionInfo: SessionInfo, event: Protocol.Network.requestWillBeSentPayload) { _onRequestWillBeSent(sessionInfo: SessionInfo, event: Protocol.Network.requestWillBeSentPayload) {
@ -165,7 +213,7 @@ export class CRNetworkManager {
this._responseExtraInfoTracker.requestWillBeSentExtraInfo(event); this._responseExtraInfoTracker.requestWillBeSentExtraInfo(event);
} }
_onAuthRequired(event: Protocol.Fetch.authRequiredPayload) { _onAuthRequired(sessionInfo: SessionInfo, event: Protocol.Fetch.authRequiredPayload) {
let response: 'Default' | 'CancelAuth' | 'ProvideCredentials' = 'Default'; let response: 'Default' | 'CancelAuth' | 'ProvideCredentials' = 'Default';
const shouldProvideCredentials = this._shouldProvideCredentials(event.request.url); const shouldProvideCredentials = this._shouldProvideCredentials(event.request.url);
if (this._attemptedAuthentications.has(event.requestId)) { if (this._attemptedAuthentications.has(event.requestId)) {
@ -175,7 +223,7 @@ export class CRNetworkManager {
this._attemptedAuthentications.add(event.requestId); this._attemptedAuthentications.add(event.requestId);
} }
const { username, password } = shouldProvideCredentials && this._credentials ? this._credentials : { username: undefined, password: undefined }; const { username, password } = shouldProvideCredentials && this._credentials ? this._credentials : { username: undefined, password: undefined };
this._session._sendMayFail('Fetch.continueWithAuth', { sessionInfo.session._sendMayFail('Fetch.continueWithAuth', {
requestId: event.requestId, requestId: event.requestId,
authChallengeResponse: { response, username, password }, authChallengeResponse: { response, username, password },
}); });
@ -191,7 +239,7 @@ export class CRNetworkManager {
if (!event.networkId) { if (!event.networkId) {
// Fetch without networkId means that request was not recognized by inspector, and // Fetch without networkId means that request was not recognized by inspector, and
// it will never receive Network.requestWillBeSent. Continue the request to not affect it. // it will never receive Network.requestWillBeSent. Continue the request to not affect it.
this._session._sendMayFail('Fetch.continueRequest', { requestId: event.requestId }); sessionInfo.session._sendMayFail('Fetch.continueRequest', { requestId: event.requestId });
return; return;
} }
if (event.request.url.startsWith('data:')) if (event.request.url.startsWith('data:'))
@ -215,7 +263,7 @@ export class CRNetworkManager {
// //
// Note: make sure not to prematurely continue the redirect, which shares the // Note: make sure not to prematurely continue the redirect, which shares the
// `networkId` between the original request and the redirect. // `networkId` between the original request and the redirect.
this._session._sendMayFail('Fetch.continueRequest', { sessionInfo.session._sendMayFail('Fetch.continueRequest', {
...alreadyContinuedParams, ...alreadyContinuedParams,
requestId: event.requestId, requestId: event.requestId,
}); });
@ -266,7 +314,7 @@ export class CRNetworkManager {
]; ];
if (requestHeaders['Access-Control-Request-Headers']) if (requestHeaders['Access-Control-Request-Headers'])
responseHeaders.push({ name: 'Access-Control-Allow-Headers', value: requestHeaders['Access-Control-Request-Headers'] }); responseHeaders.push({ name: 'Access-Control-Allow-Headers', value: requestHeaders['Access-Control-Request-Headers'] });
this._session._sendMayFail('Fetch.fulfillRequest', { sessionInfo.session._sendMayFail('Fetch.fulfillRequest', {
requestId: requestPausedEvent.requestId, requestId: requestPausedEvent.requestId,
responseCode: 204, responseCode: 204,
responsePhrase: network.STATUS_TEXTS['204'], responsePhrase: network.STATUS_TEXTS['204'],
@ -279,7 +327,7 @@ export class CRNetworkManager {
// Non-service-worker requests MUST have a frame—if they don't, we pretend there was no request // Non-service-worker requests MUST have a frame—if they don't, we pretend there was no request
if (!frame && !this._serviceWorker) { if (!frame && !this._serviceWorker) {
if (requestPausedEvent) if (requestPausedEvent)
this._session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId }); sessionInfo.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId });
return; return;
} }
@ -289,9 +337,9 @@ export class CRNetworkManager {
if (redirectedFrom || (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled)) { if (redirectedFrom || (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled)) {
// Chromium does not preserve header overrides between redirects, so we have to do it ourselves. // Chromium does not preserve header overrides between redirects, so we have to do it ourselves.
const headers = redirectedFrom?._originalRequestRoute?._alreadyContinuedParams?.headers; const headers = redirectedFrom?._originalRequestRoute?._alreadyContinuedParams?.headers;
this._session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId, headers }); sessionInfo.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId, headers });
} else { } else {
route = new RouteImpl(this._session, requestPausedEvent.requestId); route = new RouteImpl(sessionInfo.session, requestPausedEvent.requestId);
} }
} }
const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document'; const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document';
@ -426,16 +474,15 @@ export class CRNetworkManager {
(this._page?._frameManager || this._serviceWorker)!.requestReceivedResponse(response); (this._page?._frameManager || this._serviceWorker)!.requestReceivedResponse(response);
} }
_onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) { _onLoadingFinished(sessionInfo: SessionInfo, event: Protocol.Network.loadingFinishedPayload) {
this._responseExtraInfoTracker.loadingFinished(event); this._responseExtraInfoTracker.loadingFinished(event);
let request = this._requestIdToRequest.get(event.requestId); const request = this._requestIdToRequest.get(event.requestId);
if (!request)
request = this._maybeAdoptMainRequest(event.requestId);
// For certain requestIds we never receive requestWillBeSent event. // For certain requestIds we never receive requestWillBeSent event.
// @see https://crbug.com/750469 // @see https://crbug.com/750469
if (!request) if (!request)
return; return;
this._maybeUpdateOOPIFMainRequest(sessionInfo, request);
// Under certain conditions we never get the Network.responseReceived // Under certain conditions we never get the Network.responseReceived
// event from protocol. @see https://crbug.com/883475 // event from protocol. @see https://crbug.com/883475
@ -453,8 +500,6 @@ export class CRNetworkManager {
this._responseExtraInfoTracker.loadingFailed(event); this._responseExtraInfoTracker.loadingFailed(event);
let request = this._requestIdToRequest.get(event.requestId); let request = this._requestIdToRequest.get(event.requestId);
if (!request)
request = this._maybeAdoptMainRequest(event.requestId);
if (!request) { if (!request) {
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(event.requestId); const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(event.requestId);
@ -472,6 +517,7 @@ export class CRNetworkManager {
// @see https://crbug.com/750469 // @see https://crbug.com/750469
if (!request) if (!request)
return; return;
this._maybeUpdateOOPIFMainRequest(sessionInfo, request);
const response = request.request._existingResponse(); const response = request.request._existingResponse();
if (response) { if (response) {
response.setTransferSize(null); response.setTransferSize(null);
@ -483,22 +529,12 @@ export class CRNetworkManager {
(this._page?._frameManager || this._serviceWorker)!.requestFailed(request.request, !!event.canceled); (this._page?._frameManager || this._serviceWorker)!.requestFailed(request.request, !!event.canceled);
} }
private _maybeAdoptMainRequest(requestId: Protocol.Network.RequestId): InterceptableRequest | undefined { private _maybeUpdateOOPIFMainRequest(sessionInfo: SessionInfo, request: InterceptableRequest) {
// OOPIF has a main request that starts in the parent session but finishes in the child session. // OOPIF has a main request that starts in the parent session but finishes in the child session.
if (!this._parentManager) // We check for the main request by matching loaderId and requestId, and if it now belongs to
return; // a child session, migrate it there.
const request = this._parentManager._requestIdToRequest.get(requestId); if (request.session !== sessionInfo.session && !sessionInfo.isMain && request._documentId === request._requestId)
// Main requests have matching loaderId and requestId. request.session = sessionInfo.session;
if (!request || request._documentId !== requestId)
return;
this._requestIdToRequest.set(requestId, request);
request.session = this._session;
this._parentManager._requestIdToRequest.delete(requestId);
if (request._interceptionId && this._parentManager._attemptedAuthentications.has(request._interceptionId)) {
this._parentManager._attemptedAuthentications.delete(request._interceptionId);
this._attemptedAuthentications.add(request._interceptionId);
}
return request;
} }
} }

View File

@ -20,7 +20,7 @@ import type { RegisteredListener } from '../../utils/eventsHelper';
import { eventsHelper } from '../../utils/eventsHelper'; import { eventsHelper } from '../../utils/eventsHelper';
import { registry } from '../registry'; import { registry } from '../registry';
import { rewriteErrorMessage } from '../../utils/stackTrace'; import { rewriteErrorMessage } from '../../utils/stackTrace';
import { assert, createGuid, headersArrayToObject } from '../../utils'; import { assert, createGuid } from '../../utils';
import * as dialog from '../dialog'; import * as dialog from '../dialog';
import * as dom from '../dom'; import * as dom from '../dom';
import * as frames from '../frames'; import * as frames from '../frames';
@ -61,6 +61,7 @@ export class CRPage implements PageDelegate {
readonly rawTouchscreen: RawTouchscreenImpl; readonly rawTouchscreen: RawTouchscreenImpl;
readonly _targetId: string; readonly _targetId: string;
readonly _opener: CRPage | null; readonly _opener: CRPage | null;
readonly _networkManager: CRNetworkManager;
private readonly _pdf: CRPDF; private readonly _pdf: CRPDF;
private readonly _coverage: CRCoverage; private readonly _coverage: CRCoverage;
readonly _browserContext: CRBrowserContext; readonly _browserContext: CRBrowserContext;
@ -92,6 +93,13 @@ export class CRPage implements PageDelegate {
this._coverage = new CRCoverage(client); this._coverage = new CRCoverage(client);
this._browserContext = browserContext; this._browserContext = browserContext;
this._page = new Page(this, browserContext); this._page = new Page(this, browserContext);
this._networkManager = new CRNetworkManager(this._page, null);
// Sync any browser context state to the network manager. This does not talk over CDP because
// we have not connected any sessions to the network manager yet.
this.updateOffline();
this.updateExtraHTTPHeaders();
this.updateHttpCredentials();
this.updateRequestInterception();
this._mainFrameSession = new FrameSession(this, client, targetId, null); this._mainFrameSession = new FrameSession(this, client, targetId, null);
this._sessions.set(targetId, this._mainFrameSession); this._sessions.set(targetId, this._mainFrameSession);
if (opener && !browserContext._options.noDefaultViewport) { if (opener && !browserContext._options.noDefaultViewport) {
@ -184,7 +192,11 @@ export class CRPage implements PageDelegate {
} }
async updateExtraHTTPHeaders(): Promise<void> { async updateExtraHTTPHeaders(): Promise<void> {
await this._forAllFrameSessions(frame => frame._updateExtraHTTPHeaders(false)); const headers = network.mergeHeaders([
this._browserContext._options.extraHTTPHeaders,
this._page.extraHTTPHeaders()
]);
await this._networkManager.setExtraHTTPHeaders(headers);
} }
async updateGeolocation(): Promise<void> { async updateGeolocation(): Promise<void> {
@ -192,11 +204,11 @@ export class CRPage implements PageDelegate {
} }
async updateOffline(): Promise<void> { async updateOffline(): Promise<void> {
await this._forAllFrameSessions(frame => frame._updateOffline(false)); await this._networkManager.setOffline(!!this._browserContext._options.offline);
} }
async updateHttpCredentials(): Promise<void> { async updateHttpCredentials(): Promise<void> {
await this._forAllFrameSessions(frame => frame._updateHttpCredentials(false)); await this._networkManager.authenticate(this._browserContext._options.httpCredentials || null);
} }
async updateEmulatedViewportSize(preserveWindowBoundaries?: boolean): Promise<void> { async updateEmulatedViewportSize(preserveWindowBoundaries?: boolean): Promise<void> {
@ -216,7 +228,7 @@ export class CRPage implements PageDelegate {
} }
async updateRequestInterception(): Promise<void> { async updateRequestInterception(): Promise<void> {
await this._forAllFrameSessions(frame => frame._updateRequestInterception()); await this._networkManager.setRequestInterception(this._page.needsRequestInterception());
} }
async updateFileChooserInterception() { async updateFileChooserInterception() {
@ -392,7 +404,6 @@ class FrameSession {
readonly _client: CRSession; readonly _client: CRSession;
readonly _crPage: CRPage; readonly _crPage: CRPage;
readonly _page: Page; readonly _page: Page;
readonly _networkManager: CRNetworkManager;
private readonly _parentSession: FrameSession | null; private readonly _parentSession: FrameSession | null;
private readonly _childSessions = new Set<FrameSession>(); private readonly _childSessions = new Set<FrameSession>();
private readonly _contextIdToContext = new Map<number, dom.FrameExecutionContext>(); private readonly _contextIdToContext = new Map<number, dom.FrameExecutionContext>();
@ -418,7 +429,6 @@ class FrameSession {
this._crPage = crPage; this._crPage = crPage;
this._page = crPage._page; this._page = crPage._page;
this._targetId = targetId; this._targetId = targetId;
this._networkManager = new CRNetworkManager(client, this._page, null, parentSession ? parentSession._networkManager : null);
this._parentSession = parentSession; this._parentSession = parentSession;
if (parentSession) if (parentSession)
parentSession._childSessions.add(this); parentSession._childSessions.add(this);
@ -533,7 +543,7 @@ class FrameSession {
source: '', source: '',
worldName: UTILITY_WORLD_NAME, worldName: UTILITY_WORLD_NAME,
}), }),
this._networkManager.initialize(), this._crPage._networkManager.addSession(this._client, undefined, this._isMainFrame()),
this._client.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }), this._client.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }),
]; ];
if (!isSettingStorageState) { if (!isSettingStorageState) {
@ -559,10 +569,6 @@ class FrameSession {
if (!this._crPage._browserContext._browser.options.headful) if (!this._crPage._browserContext._browser.options.headful)
promises.push(this._setDefaultFontFamilies(this._client)); promises.push(this._setDefaultFontFamilies(this._client));
promises.push(this._updateGeolocation(true)); promises.push(this._updateGeolocation(true));
promises.push(this._updateExtraHTTPHeaders(true));
promises.push(this._updateRequestInterception());
promises.push(this._updateOffline(true));
promises.push(this._updateHttpCredentials(true));
promises.push(this._updateEmulateMedia()); promises.push(this._updateEmulateMedia());
promises.push(this._updateFileChooserInterception(true)); promises.push(this._updateFileChooserInterception(true));
for (const binding of this._crPage._page.allBindings()) for (const binding of this._crPage._page.allBindings())
@ -586,7 +592,7 @@ class FrameSession {
if (this._parentSession) if (this._parentSession)
this._parentSession._childSessions.delete(this); this._parentSession._childSessions.delete(this);
eventsHelper.removeEventListeners(this._eventListeners); eventsHelper.removeEventListeners(this._eventListeners);
this._networkManager.dispose(); this._crPage._networkManager.removeSession(this._client);
this._crPage._sessions.delete(this._targetId); this._crPage._sessions.delete(this._targetId);
this._client.dispose(); this._client.dispose();
} }
@ -752,7 +758,8 @@ class FrameSession {
}); });
// This might fail if the target is closed before we initialize. // This might fail if the target is closed before we initialize.
session._sendMayFail('Runtime.enable'); session._sendMayFail('Runtime.enable');
session._sendMayFail('Network.enable'); // TODO: attribute workers to the right frame.
this._crPage._networkManager.addSession(session, this._page._frameManager.frame(this._targetId) ?? undefined).catch(() => {});
session._sendMayFail('Runtime.runIfWaitingForDebugger'); session._sendMayFail('Runtime.runIfWaitingForDebugger');
session._sendMayFail('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }); session._sendMayFail('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true });
session.on('Target.attachedToTarget', event => this._onAttachedToTarget(event)); session.on('Target.attachedToTarget', event => this._onAttachedToTarget(event));
@ -762,8 +769,6 @@ class FrameSession {
this._page._addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace)); this._page._addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace));
}); });
session.on('Runtime.exceptionThrown', exception => this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, exceptionToError(exception.exceptionDetails), this._page)); session.on('Runtime.exceptionThrown', exception => this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, exceptionToError(exception.exceptionDetails), this._page));
// TODO: attribute workers to the right frame.
this._networkManager.instrumentNetworkEvents({ session, workerFrame: this._page._frameManager.frame(this._targetId) ?? undefined });
} }
_onDetachedFromTarget(event: Protocol.Target.detachedFromTargetPayload) { _onDetachedFromTarget(event: Protocol.Target.detachedFromTargetPayload) {
@ -981,33 +986,12 @@ class FrameSession {
await this._client._sendMayFail('Page.stopScreencast'); await this._client._sendMayFail('Page.stopScreencast');
} }
async _updateExtraHTTPHeaders(initial: boolean): Promise<void> {
const headers = network.mergeHeaders([
this._crPage._browserContext._options.extraHTTPHeaders,
this._page.extraHTTPHeaders()
]);
if (!initial || headers.length)
await this._client.send('Network.setExtraHTTPHeaders', { headers: headersArrayToObject(headers, false /* lowerCase */) });
}
async _updateGeolocation(initial: boolean): Promise<void> { async _updateGeolocation(initial: boolean): Promise<void> {
const geolocation = this._crPage._browserContext._options.geolocation; const geolocation = this._crPage._browserContext._options.geolocation;
if (!initial || geolocation) if (!initial || geolocation)
await this._client.send('Emulation.setGeolocationOverride', geolocation || {}); await this._client.send('Emulation.setGeolocationOverride', geolocation || {});
} }
async _updateOffline(initial: boolean): Promise<void> {
const offline = !!this._crPage._browserContext._options.offline;
if (!initial || offline)
await this._networkManager.setOffline(offline);
}
async _updateHttpCredentials(initial: boolean): Promise<void> {
const credentials = this._crPage._browserContext._options.httpCredentials || null;
if (!initial || credentials)
await this._networkManager.authenticate(credentials);
}
async _updateViewport(preserveWindowBoundaries?: boolean): Promise<void> { async _updateViewport(preserveWindowBoundaries?: boolean): Promise<void> {
if (this._crPage._browserContext._browser.isClank()) if (this._crPage._browserContext._browser.isClank())
return; return;
@ -1106,10 +1090,6 @@ class FrameSession {
await session.send('Page.setFontFamilies', fontFamilies); await session.send('Page.setFontFamilies', fontFamilies);
} }
async _updateRequestInterception(): Promise<void> {
await this._networkManager.setRequestInterception(this._page.needsRequestInterception());
}
async _updateFileChooserInterception(initial: boolean) { async _updateFileChooserInterception(initial: boolean) {
const enabled = this._page.fileChooserIntercepted(); const enabled = this._page.fileChooserIntercepted();
if (initial && !enabled) if (initial && !enabled)

View File

@ -34,13 +34,13 @@ export class CRServiceWorker extends Worker {
this._session = session; this._session = session;
this._browserContext = browserContext; this._browserContext = browserContext;
if (!!process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS) if (!!process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS)
this._networkManager = new CRNetworkManager(session, null, this, null); this._networkManager = new CRNetworkManager(null, this);
session.once('Runtime.executionContextCreated', event => { session.once('Runtime.executionContextCreated', event => {
this._createExecutionContext(new CRExecutionContext(session, event.context)); this._createExecutionContext(new CRExecutionContext(session, event.context));
}); });
if (this._networkManager && this._isNetworkInspectionEnabled()) { if (this._networkManager && this._isNetworkInspectionEnabled()) {
this._networkManager.initialize().catch(() => {}); this._networkManager.addSession(session, undefined, true /* isMain */).catch(() => {});
this.updateRequestInterception(); this.updateRequestInterception();
this.updateExtraHTTPHeaders(true); this.updateExtraHTTPHeaders(true);
this.updateHttpCredentials(true); this.updateHttpCredentials(true);
@ -56,6 +56,7 @@ export class CRServiceWorker extends Worker {
} }
override didClose() { override didClose() {
this._networkManager?.removeSession(this._session);
this._session.dispose(); this._session.dispose();
super.didClose(); super.didClose();
} }

View File

@ -224,3 +224,32 @@ it('should report and intercept network from nested worker', async function({ pa
await expect.poll(() => messages).toEqual(['{"foo":"not bar"}', '{"foo":"not bar"}']); await expect.poll(() => messages).toEqual(['{"foo":"not bar"}', '{"foo":"not bar"}']);
}); });
it('should support extra http headers', async ({ page, server }) => {
await page.setExtraHTTPHeaders({ foo: 'bar' });
const [worker, request1] = await Promise.all([
page.waitForEvent('worker'),
server.waitForRequest('/worker/worker.js'),
page.goto(server.PREFIX + '/worker/worker.html'),
]);
const [request2] = await Promise.all([
server.waitForRequest('/one-style.css'),
worker.evaluate(url => fetch(url), server.PREFIX + '/one-style.css'),
]);
expect(request1.headers['foo']).toBe('bar');
expect(request2.headers['foo']).toBe('bar');
});
it('should support offline', async ({ page, server, browserName }) => {
it.fixme(browserName === 'firefox');
const [worker] = await Promise.all([
page.waitForEvent('worker'),
page.goto(server.PREFIX + '/worker/worker.html'),
]);
await page.context().setOffline(true);
expect(await worker.evaluate(() => navigator.onLine)).toBe(false);
expect(await worker.evaluate(() => fetch('/one-style.css').catch(e => 'error'))).toBe('error');
await page.context().setOffline(false);
expect(await worker.evaluate(() => navigator.onLine)).toBe(true);
});