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> {
for (const page of this._crPages())
await page._mainFrameSession._networkManager.clearCache();
await page._networkManager.clearCache();
}
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 types from '../types';
import type { CRPage } from './crPage';
import { assert, headersObjectToArray } from '../../utils';
import { assert, headersArrayToObject, headersObjectToArray } from '../../utils';
import type { CRServiceWorker } from './crServiceWorker';
import { isProtocolError } from '../protocolError';
import { isProtocolError, isSessionClosedError } from '../protocolError';
type SessionInfo = {
session: CRSession;
isMain?: boolean;
workerFrame?: frames.Frame;
eventListeners: RegisteredListener[];
};
export class CRNetworkManager {
private _session: CRSession;
private _page: Page | null;
private _serviceWorker: CRServiceWorker | null;
private _parentManager: CRNetworkManager | null;
private _requestIdToRequest = new Map<string, InterceptableRequest>();
private _requestIdToRequestWillBeSentEvent = new Map<string, Protocol.Network.requestWillBeSentPayload>();
private _credentials: {origin?: string, username: string, password: string} | null = null;
private _attemptedAuthentications = new Set<string>();
private _userRequestInterceptionEnabled = false;
private _protocolRequestInterceptionEnabled = false;
private _offline = false;
private _extraHTTPHeaders: types.HeadersArray = [];
private _requestIdToRequestPausedEvent = new Map<string, Protocol.Fetch.requestPausedPayload>();
private _eventListeners: RegisteredListener[];
private _responseExtraInfoTracker = new ResponseExtraInfoTracker();
private _sessions = new Map<CRSession, SessionInfo>();
constructor(session: CRSession, page: Page | null, serviceWorker: CRServiceWorker | null, parentManager: CRNetworkManager | null) {
this._session = session;
constructor(page: Page | null, serviceWorker: CRServiceWorker | null) {
this._page = page;
this._serviceWorker = serviceWorker;
this._parentManager = parentManager;
this._eventListeners = this.instrumentNetworkEvents({ session });
}
instrumentNetworkEvents(sessionInfo: SessionInfo): RegisteredListener[] {
const listeners = [
eventsHelper.addEventListener(sessionInfo.session, 'Fetch.requestPaused', this._onRequestPaused.bind(this, sessionInfo)),
eventsHelper.addEventListener(sessionInfo.session, 'Fetch.authRequired', this._onAuthRequired.bind(this)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this, sessionInfo)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.requestWillBeSentExtraInfo', this._onRequestWillBeSentExtraInfo.bind(this)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.requestServedFromCache', this._onRequestServedFromCache.bind(this)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.responseReceived', this._onResponseReceived.bind(this, sessionInfo)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.responseReceivedExtraInfo', this._onResponseReceivedExtraInfo.bind(this)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.loadingFinished', this._onLoadingFinished.bind(this)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.loadingFailed', this._onLoadingFailed.bind(this, sessionInfo)),
async addSession(session: CRSession, workerFrame?: frames.Frame, isMain?: boolean) {
const sessionInfo: SessionInfo = { session, isMain, workerFrame, eventListeners: [] };
sessionInfo.eventListeners = [
eventsHelper.addEventListener(session, 'Fetch.requestPaused', this._onRequestPaused.bind(this, sessionInfo)),
eventsHelper.addEventListener(session, 'Fetch.authRequired', this._onAuthRequired.bind(this, sessionInfo)),
eventsHelper.addEventListener(session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this, sessionInfo)),
eventsHelper.addEventListener(session, 'Network.requestWillBeSentExtraInfo', this._onRequestWillBeSentExtraInfo.bind(this)),
eventsHelper.addEventListener(session, 'Network.requestServedFromCache', this._onRequestServedFromCache.bind(this)),
eventsHelper.addEventListener(session, 'Network.responseReceived', this._onResponseReceived.bind(this, sessionInfo)),
eventsHelper.addEventListener(session, 'Network.responseReceivedExtraInfo', this._onResponseReceivedExtraInfo.bind(this)),
eventsHelper.addEventListener(session, 'Network.loadingFinished', this._onLoadingFinished.bind(this, sessionInfo)),
eventsHelper.addEventListener(session, 'Network.loadingFailed', this._onLoadingFailed.bind(this, sessionInfo)),
];
if (this._page) {
listeners.push(...[
eventsHelper.addEventListener(sessionInfo.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(sessionInfo.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(sessionInfo.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(sessionInfo.session, 'Network.webSocketFrameError', e => this._page!._frameManager.webSocketError(e.requestId, e.errorMessage)),
sessionInfo.eventListeners.push(...[
eventsHelper.addEventListener(session, 'Network.webSocketCreated', e => this._page!._frameManager.onWebSocketCreated(e.requestId, e.url)),
eventsHelper.addEventListener(session, 'Network.webSocketWillSendHandshakeRequest', e => this._page!._frameManager.onWebSocketRequest(e.requestId)),
eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page!._frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)),
eventsHelper.addEventListener(session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page!._frameManager.onWebSocketFrameSent(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(session, 'Network.webSocketClosed', e => this._page!._frameManager.webSocketClosed(e.requestId)),
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() {
await this._session.send('Network.enable');
removeSession(session: CRSession) {
const info = this._sessions.get(session);
if (info)
eventsHelper.removeEventListeners(info.eventListeners);
this._sessions.delete(session);
}
dispose() {
eventsHelper.removeEventListeners(this._eventListeners);
private async _forEachSession(cb: (sessionInfo: SessionInfo) => Promise<any>) {
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) {
@ -98,8 +116,20 @@ export class CRNetworkManager {
}
async setOffline(offline: boolean) {
await this._session.send('Network.emulateNetworkConditions', {
offline,
if (offline === this._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
latency: 0,
downloadThroughput: -1,
@ -117,28 +147,46 @@ export class CRNetworkManager {
if (enabled === this._protocolRequestInterceptionEnabled)
return;
this._protocolRequestInterceptionEnabled = enabled;
if (enabled) {
await Promise.all([
this._session.send('Network.setCacheDisabled', { cacheDisabled: true }),
this._session.send('Fetch.enable', {
handleAuthRequests: true,
patterns: [{ urlPattern: '*', requestStage: 'Request' }],
}),
]);
} else {
await Promise.all([
this._session.send('Network.setCacheDisabled', { cacheDisabled: false }),
this._session.send('Fetch.disable')
]);
await this._forEachSession(info => this._updateProtocolRequestInterceptionForSession(info));
}
private async _updateProtocolRequestInterceptionForSession(info: SessionInfo, initial?: boolean) {
const enabled = this._protocolRequestInterceptionEnabled;
if (initial && !enabled)
return;
const cachePromise = info.session.send('Network.setCacheDisabled', { cacheDisabled: enabled });
let fetchPromise = Promise.resolve<any>(undefined);
if (!info.workerFrame) {
if (enabled)
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() {
// Sending 'Network.setCacheDisabled' with 'cacheDisabled = true' will clear the MemoryCache.
await this._session.send('Network.setCacheDisabled', { cacheDisabled: true });
if (!this._protocolRequestInterceptionEnabled)
await this._session.send('Network.setCacheDisabled', { cacheDisabled: false });
await this._session.send('Network.clearBrowserCache');
await this._forEachSession(async info => {
// Sending 'Network.setCacheDisabled' with 'cacheDisabled = true' will clear the MemoryCache.
await info.session.send('Network.setCacheDisabled', { cacheDisabled: true });
if (!this._protocolRequestInterceptionEnabled)
await info.session.send('Network.setCacheDisabled', { cacheDisabled: false });
if (!info.workerFrame)
await info.session.send('Network.clearBrowserCache');
});
}
_onRequestWillBeSent(sessionInfo: SessionInfo, event: Protocol.Network.requestWillBeSentPayload) {
@ -165,7 +213,7 @@ export class CRNetworkManager {
this._responseExtraInfoTracker.requestWillBeSentExtraInfo(event);
}
_onAuthRequired(event: Protocol.Fetch.authRequiredPayload) {
_onAuthRequired(sessionInfo: SessionInfo, event: Protocol.Fetch.authRequiredPayload) {
let response: 'Default' | 'CancelAuth' | 'ProvideCredentials' = 'Default';
const shouldProvideCredentials = this._shouldProvideCredentials(event.request.url);
if (this._attemptedAuthentications.has(event.requestId)) {
@ -175,7 +223,7 @@ export class CRNetworkManager {
this._attemptedAuthentications.add(event.requestId);
}
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,
authChallengeResponse: { response, username, password },
});
@ -191,7 +239,7 @@ export class CRNetworkManager {
if (!event.networkId) {
// 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.
this._session._sendMayFail('Fetch.continueRequest', { requestId: event.requestId });
sessionInfo.session._sendMayFail('Fetch.continueRequest', { requestId: event.requestId });
return;
}
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
// `networkId` between the original request and the redirect.
this._session._sendMayFail('Fetch.continueRequest', {
sessionInfo.session._sendMayFail('Fetch.continueRequest', {
...alreadyContinuedParams,
requestId: event.requestId,
});
@ -266,7 +314,7 @@ export class CRNetworkManager {
];
if (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,
responseCode: 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
if (!frame && !this._serviceWorker) {
if (requestPausedEvent)
this._session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId });
sessionInfo.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId });
return;
}
@ -289,9 +337,9 @@ export class CRNetworkManager {
if (redirectedFrom || (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled)) {
// Chromium does not preserve header overrides between redirects, so we have to do it ourselves.
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 {
route = new RouteImpl(this._session, requestPausedEvent.requestId);
route = new RouteImpl(sessionInfo.session, requestPausedEvent.requestId);
}
}
const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document';
@ -426,16 +474,15 @@ export class CRNetworkManager {
(this._page?._frameManager || this._serviceWorker)!.requestReceivedResponse(response);
}
_onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) {
_onLoadingFinished(sessionInfo: SessionInfo, event: Protocol.Network.loadingFinishedPayload) {
this._responseExtraInfoTracker.loadingFinished(event);
let request = this._requestIdToRequest.get(event.requestId);
if (!request)
request = this._maybeAdoptMainRequest(event.requestId);
const request = this._requestIdToRequest.get(event.requestId);
// For certain requestIds we never receive requestWillBeSent event.
// @see https://crbug.com/750469
if (!request)
return;
this._maybeUpdateOOPIFMainRequest(sessionInfo, request);
// Under certain conditions we never get the Network.responseReceived
// event from protocol. @see https://crbug.com/883475
@ -453,8 +500,6 @@ export class CRNetworkManager {
this._responseExtraInfoTracker.loadingFailed(event);
let request = this._requestIdToRequest.get(event.requestId);
if (!request)
request = this._maybeAdoptMainRequest(event.requestId);
if (!request) {
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(event.requestId);
@ -472,6 +517,7 @@ export class CRNetworkManager {
// @see https://crbug.com/750469
if (!request)
return;
this._maybeUpdateOOPIFMainRequest(sessionInfo, request);
const response = request.request._existingResponse();
if (response) {
response.setTransferSize(null);
@ -483,22 +529,12 @@ export class CRNetworkManager {
(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.
if (!this._parentManager)
return;
const request = this._parentManager._requestIdToRequest.get(requestId);
// Main requests have matching loaderId and requestId.
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;
// We check for the main request by matching loaderId and requestId, and if it now belongs to
// a child session, migrate it there.
if (request.session !== sessionInfo.session && !sessionInfo.isMain && request._documentId === request._requestId)
request.session = sessionInfo.session;
}
}

View File

@ -20,7 +20,7 @@ import type { RegisteredListener } from '../../utils/eventsHelper';
import { eventsHelper } from '../../utils/eventsHelper';
import { registry } from '../registry';
import { rewriteErrorMessage } from '../../utils/stackTrace';
import { assert, createGuid, headersArrayToObject } from '../../utils';
import { assert, createGuid } from '../../utils';
import * as dialog from '../dialog';
import * as dom from '../dom';
import * as frames from '../frames';
@ -61,6 +61,7 @@ export class CRPage implements PageDelegate {
readonly rawTouchscreen: RawTouchscreenImpl;
readonly _targetId: string;
readonly _opener: CRPage | null;
readonly _networkManager: CRNetworkManager;
private readonly _pdf: CRPDF;
private readonly _coverage: CRCoverage;
readonly _browserContext: CRBrowserContext;
@ -92,6 +93,13 @@ export class CRPage implements PageDelegate {
this._coverage = new CRCoverage(client);
this._browserContext = 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._sessions.set(targetId, this._mainFrameSession);
if (opener && !browserContext._options.noDefaultViewport) {
@ -184,7 +192,11 @@ export class CRPage implements PageDelegate {
}
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> {
@ -192,11 +204,11 @@ export class CRPage implements PageDelegate {
}
async updateOffline(): Promise<void> {
await this._forAllFrameSessions(frame => frame._updateOffline(false));
await this._networkManager.setOffline(!!this._browserContext._options.offline);
}
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> {
@ -216,7 +228,7 @@ export class CRPage implements PageDelegate {
}
async updateRequestInterception(): Promise<void> {
await this._forAllFrameSessions(frame => frame._updateRequestInterception());
await this._networkManager.setRequestInterception(this._page.needsRequestInterception());
}
async updateFileChooserInterception() {
@ -392,7 +404,6 @@ class FrameSession {
readonly _client: CRSession;
readonly _crPage: CRPage;
readonly _page: Page;
readonly _networkManager: CRNetworkManager;
private readonly _parentSession: FrameSession | null;
private readonly _childSessions = new Set<FrameSession>();
private readonly _contextIdToContext = new Map<number, dom.FrameExecutionContext>();
@ -418,7 +429,6 @@ class FrameSession {
this._crPage = crPage;
this._page = crPage._page;
this._targetId = targetId;
this._networkManager = new CRNetworkManager(client, this._page, null, parentSession ? parentSession._networkManager : null);
this._parentSession = parentSession;
if (parentSession)
parentSession._childSessions.add(this);
@ -533,7 +543,7 @@ class FrameSession {
source: '',
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 }),
];
if (!isSettingStorageState) {
@ -559,10 +569,6 @@ class FrameSession {
if (!this._crPage._browserContext._browser.options.headful)
promises.push(this._setDefaultFontFamilies(this._client));
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._updateFileChooserInterception(true));
for (const binding of this._crPage._page.allBindings())
@ -586,7 +592,7 @@ class FrameSession {
if (this._parentSession)
this._parentSession._childSessions.delete(this);
eventsHelper.removeEventListeners(this._eventListeners);
this._networkManager.dispose();
this._crPage._networkManager.removeSession(this._client);
this._crPage._sessions.delete(this._targetId);
this._client.dispose();
}
@ -752,7 +758,8 @@ class FrameSession {
});
// This might fail if the target is closed before we initialize.
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('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true });
session.on('Target.attachedToTarget', event => this._onAttachedToTarget(event));
@ -762,8 +769,6 @@ class FrameSession {
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));
// TODO: attribute workers to the right frame.
this._networkManager.instrumentNetworkEvents({ session, workerFrame: this._page._frameManager.frame(this._targetId) ?? undefined });
}
_onDetachedFromTarget(event: Protocol.Target.detachedFromTargetPayload) {
@ -981,33 +986,12 @@ class FrameSession {
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> {
const geolocation = this._crPage._browserContext._options.geolocation;
if (!initial || 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> {
if (this._crPage._browserContext._browser.isClank())
return;
@ -1106,10 +1090,6 @@ class FrameSession {
await session.send('Page.setFontFamilies', fontFamilies);
}
async _updateRequestInterception(): Promise<void> {
await this._networkManager.setRequestInterception(this._page.needsRequestInterception());
}
async _updateFileChooserInterception(initial: boolean) {
const enabled = this._page.fileChooserIntercepted();
if (initial && !enabled)

View File

@ -34,13 +34,13 @@ export class CRServiceWorker extends Worker {
this._session = session;
this._browserContext = browserContext;
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 => {
this._createExecutionContext(new CRExecutionContext(session, event.context));
});
if (this._networkManager && this._isNetworkInspectionEnabled()) {
this._networkManager.initialize().catch(() => {});
this._networkManager.addSession(session, undefined, true /* isMain */).catch(() => {});
this.updateRequestInterception();
this.updateExtraHTTPHeaders(true);
this.updateHttpCredentials(true);
@ -56,6 +56,7 @@ export class CRServiceWorker extends Worker {
}
override didClose() {
this._networkManager?.removeSession(this._session);
this._session.dispose();
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"}']);
});
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);
});