fix: make network manager multi session (#10793)

This commit is contained in:
Alex Rudenko 2023-08-29 18:12:04 +02:00 committed by GitHub
parent 6f2e3db883
commit 085936bd7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 287 additions and 138 deletions

View File

@ -113,7 +113,7 @@ export class FrameManager extends EventEmitter {
super(); super();
this.#client = client; this.#client = client;
this.#page = page; this.#page = page;
this.#networkManager = new NetworkManager(client, ignoreHTTPSErrors, this); this.#networkManager = new NetworkManager(ignoreHTTPSErrors, this);
this.#timeoutSettings = timeoutSettings; this.#timeoutSettings = timeoutSettings;
this.setupEventListeners(this.#client); this.setupEventListeners(this.#client);
client.once(CDPSessionEmittedEvents.Disconnected, () => { client.once(CDPSessionEmittedEvents.Disconnected, () => {
@ -176,12 +176,16 @@ export class FrameManager extends EventEmitter {
this.#onClientDisconnect().catch(debugError); this.#onClientDisconnect().catch(debugError);
}); });
await this.initialize(client); await this.initialize(client);
await this.#networkManager.updateClient(client); await this.#networkManager.addClient(client);
if (frame) { if (frame) {
frame.emit(FrameEmittedEvents.FrameSwappedByActivation); frame.emit(FrameEmittedEvents.FrameSwappedByActivation);
} }
} }
async registerSecondaryPage(client: CDPSessionImpl): Promise<void> {
await this.#networkManager.addClient(client);
}
private setupEventListeners(session: CDPSession) { private setupEventListeners(session: CDPSession) {
session.on('Page.frameAttached', event => { session.on('Page.frameAttached', event => {
this.#onFrameAttached(session, event.frameId, event.parentFrameId); this.#onFrameAttached(session, event.frameId, event.parentFrameId);
@ -222,13 +226,13 @@ export class FrameManager extends EventEmitter {
}); });
} }
async initialize(client: CDPSession = this.#client): Promise<void> { async initialize(client: CDPSession): Promise<void> {
try { try {
const networkInit = this.#networkManager.addClient(client);
const result = await Promise.all([ const result = await Promise.all([
client.send('Page.enable'), client.send('Page.enable'),
client.send('Page.getFrameTree'), client.send('Page.getFrameTree'),
]); ]);
const {frameTree} = result[1]; const {frameTree} = result[1];
this.#handleFrameTree(client, frameTree); this.#handleFrameTree(client, frameTree);
await Promise.all([ await Promise.all([
@ -236,10 +240,7 @@ export class FrameManager extends EventEmitter {
client.send('Runtime.enable').then(() => { client.send('Runtime.enable').then(() => {
return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME); return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME);
}), }),
// TODO: Network manager is not aware of OOP iframes yet. networkInit,
client === this.#client
? this.#networkManager.initialize()
: Promise.resolve(),
]); ]);
} catch (error) { } catch (error) {
// The target might have been closed before the initialization finished. // The target might have been closed before the initialization finished.
@ -295,7 +296,7 @@ export class FrameManager extends EventEmitter {
frame.updateClient(target._session()!); frame.updateClient(target._session()!);
} }
this.setupEventListeners(target._session()!); this.setupEventListeners(target._session()!);
void this.initialize(target._session()); void this.initialize(target._session()!);
} }
/** /**

View File

@ -45,11 +45,12 @@ class MockCDPSession extends EventEmitter {
describe('NetworkManager', () => { describe('NetworkManager', () => {
it('should process extra info on multiple redirects', async () => { it('should process extra info on multiple redirects', async () => {
const mockCDPSession = new MockCDPSession(); const mockCDPSession = new MockCDPSession();
new NetworkManager(mockCDPSession, true, { const manager = new NetworkManager(true, {
frame(): Frame | null { frame(): Frame | null {
return null; return null;
}, },
}); });
await manager.addClient(mockCDPSession);
mockCDPSession.emit('Network.requestWillBeSent', { mockCDPSession.emit('Network.requestWillBeSent', {
requestId: '7760711DEFCFA23132D98ABA6B4E175C', requestId: '7760711DEFCFA23132D98ABA6B4E175C',
loaderId: '7760711DEFCFA23132D98ABA6B4E175C', loaderId: '7760711DEFCFA23132D98ABA6B4E175C',
@ -476,11 +477,12 @@ describe('NetworkManager', () => {
}); });
it(`should handle "double pause" (crbug.com/1196004) Fetch.requestPaused events for the same Network.requestWillBeSent event`, async () => { it(`should handle "double pause" (crbug.com/1196004) Fetch.requestPaused events for the same Network.requestWillBeSent event`, async () => {
const mockCDPSession = new MockCDPSession(); const mockCDPSession = new MockCDPSession();
const manager = new NetworkManager(mockCDPSession, true, { const manager = new NetworkManager(true, {
frame(): Frame | null { frame(): Frame | null {
return null; return null;
}, },
}); });
await manager.addClient(mockCDPSession);
await manager.setRequestInterception(true); await manager.setRequestInterception(true);
const requests: HTTPRequest[] = []; const requests: HTTPRequest[] = [];
@ -562,11 +564,12 @@ describe('NetworkManager', () => {
}); });
it(`should handle Network.responseReceivedExtraInfo event after Network.responseReceived event (github.com/puppeteer/puppeteer/issues/8234)`, async () => { it(`should handle Network.responseReceivedExtraInfo event after Network.responseReceived event (github.com/puppeteer/puppeteer/issues/8234)`, async () => {
const mockCDPSession = new MockCDPSession(); const mockCDPSession = new MockCDPSession();
const manager = new NetworkManager(mockCDPSession, true, { const manager = new NetworkManager(true, {
frame(): Frame | null { frame(): Frame | null {
return null; return null;
}, },
}); });
await manager.addClient(mockCDPSession);
const requests: HTTPRequest[] = []; const requests: HTTPRequest[] = [];
manager.on( manager.on(
@ -680,11 +683,12 @@ describe('NetworkManager', () => {
it(`should resolve the response once the late responseReceivedExtraInfo event arrives`, async () => { it(`should resolve the response once the late responseReceivedExtraInfo event arrives`, async () => {
const mockCDPSession = new MockCDPSession(); const mockCDPSession = new MockCDPSession();
const manager = new NetworkManager(mockCDPSession, true, { const manager = new NetworkManager(true, {
frame(): Frame | null { frame(): Frame | null {
return null; return null;
}, },
}); });
await manager.addClient(mockCDPSession);
const finishedRequests: HTTPRequest[] = []; const finishedRequests: HTTPRequest[] = [];
const pendingRequests: HTTPRequest[] = []; const pendingRequests: HTTPRequest[] = [];
@ -832,11 +836,12 @@ describe('NetworkManager', () => {
it(`should send responses for iframe that don't receive loadingFinished event`, async () => { it(`should send responses for iframe that don't receive loadingFinished event`, async () => {
const mockCDPSession = new MockCDPSession(); const mockCDPSession = new MockCDPSession();
const manager = new NetworkManager(mockCDPSession, true, { const manager = new NetworkManager(true, {
frame(): Frame | null { frame(): Frame | null {
return null; return null;
}, },
}); });
await manager.addClient(mockCDPSession);
const responses: HTTPResponse[] = []; const responses: HTTPResponse[] = [];
const requests: HTTPRequest[] = []; const requests: HTTPRequest[] = [];
@ -995,11 +1000,12 @@ describe('NetworkManager', () => {
it(`should send responses for iframe that don't receive loadingFinished event`, async () => { it(`should send responses for iframe that don't receive loadingFinished event`, async () => {
const mockCDPSession = new MockCDPSession(); const mockCDPSession = new MockCDPSession();
const manager = new NetworkManager(mockCDPSession, true, { const manager = new NetworkManager(true, {
frame(): Frame | null { frame(): Frame | null {
return null; return null;
}, },
}); });
await manager.addClient(mockCDPSession);
const responses: HTTPResponse[] = []; const responses: HTTPResponse[] = [];
const requests: HTTPRequest[] = []; const requests: HTTPRequest[] = [];
@ -1141,11 +1147,12 @@ describe('NetworkManager', () => {
it(`should handle cached redirects`, async () => { it(`should handle cached redirects`, async () => {
const mockCDPSession = new MockCDPSession(); const mockCDPSession = new MockCDPSession();
const manager = new NetworkManager(mockCDPSession, true, { const manager = new NetworkManager(true, {
frame(): Frame | null { frame(): Frame | null {
return null; return null;
}, },
}); });
await manager.addClient(mockCDPSession);
const responses: HTTPResponse[] = []; const responses: HTTPResponse[] = [];
const requests: HTTPRequest[] = []; const requests: HTTPRequest[] = [];

View File

@ -16,13 +16,11 @@
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {Frame} from '../api/Frame.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {createDebuggableDeferred} from '../util/DebuggableDeferred.js';
import {Deferred} from '../util/Deferred.js';
import {CDPSession} from './Connection.js'; import {CDPSession, CDPSessionEmittedEvents} from './Connection.js';
import {EventEmitter, Handler} from './EventEmitter.js'; import {EventEmitter, Handler} from './EventEmitter.js';
import {FrameManager} from './FrameManager.js';
import {HTTPRequest} from './HTTPRequest.js'; import {HTTPRequest} from './HTTPRequest.js';
import {HTTPResponse} from './HTTPResponse.js'; import {HTTPResponse} from './HTTPResponse.js';
import {FetchRequestId, NetworkEventManager} from './NetworkEventManager.js'; import {FetchRequestId, NetworkEventManager} from './NetworkEventManager.js';
@ -68,102 +66,103 @@ export const NetworkManagerEmittedEvents = {
RequestFinished: Symbol('NetworkManager.RequestFinished'), RequestFinished: Symbol('NetworkManager.RequestFinished'),
} as const; } as const;
/**
* @internal
*/
interface FrameProvider {
frame(id: string): Frame | null;
}
/** /**
* @internal * @internal
*/ */
export class NetworkManager extends EventEmitter { export class NetworkManager extends EventEmitter {
#client: CDPSession;
#ignoreHTTPSErrors: boolean; #ignoreHTTPSErrors: boolean;
#frameManager: Pick<FrameManager, 'frame'>; #frameManager: FrameProvider;
#networkEventManager = new NetworkEventManager(); #networkEventManager = new NetworkEventManager();
#extraHTTPHeaders: Record<string, string> = {}; #extraHTTPHeaders?: Record<string, string>;
#credentials?: Credentials; #credentials?: Credentials;
#attemptedAuthentications = new Set<string>(); #attemptedAuthentications = new Set<string>();
#userRequestInterceptionEnabled = false; #userRequestInterceptionEnabled = false;
#protocolRequestInterceptionEnabled = false; #protocolRequestInterceptionEnabled = false;
#userCacheDisabled = false; #userCacheDisabled?: boolean;
#emulatedNetworkConditions: InternalNetworkConditions = { #emulatedNetworkConditions?: InternalNetworkConditions;
offline: false, #userAgent?: string;
upload: -1, #userAgentMetadata?: Protocol.Emulation.UserAgentMetadata;
download: -1,
latency: 0,
};
#deferredInit?: Deferred<void>;
#handlers = new Map<string, Handler<any>>([ #handlers = new Map<string, Function>([
['Fetch.requestPaused', this.#onRequestPaused.bind(this)], ['Fetch.requestPaused', this.#onRequestPaused],
['Fetch.authRequired', this.#onAuthRequired.bind(this)], ['Fetch.authRequired', this.#onAuthRequired],
['Network.requestWillBeSent', this.#onRequestWillBeSent.bind(this)], ['Network.requestWillBeSent', this.#onRequestWillBeSent],
[ ['Network.requestServedFromCache', this.#onRequestServedFromCache],
'Network.requestServedFromCache', ['Network.responseReceived', this.#onResponseReceived],
this.#onRequestServedFromCache.bind(this), ['Network.loadingFinished', this.#onLoadingFinished],
], ['Network.loadingFailed', this.#onLoadingFailed],
['Network.responseReceived', this.#onResponseReceived.bind(this)], ['Network.responseReceivedExtraInfo', this.#onResponseReceivedExtraInfo],
['Network.loadingFinished', this.#onLoadingFinished.bind(this)],
['Network.loadingFailed', this.#onLoadingFailed.bind(this)],
[
'Network.responseReceivedExtraInfo',
this.#onResponseReceivedExtraInfo.bind(this),
],
]); ]);
constructor( #clients = new Map<
client: CDPSession, CDPSession,
ignoreHTTPSErrors: boolean, Array<{event: string | symbol; handler: Handler}>
frameManager: Pick<FrameManager, 'frame'> >();
) {
constructor(ignoreHTTPSErrors: boolean, frameManager: FrameProvider) {
super(); super();
this.#client = client;
this.#ignoreHTTPSErrors = ignoreHTTPSErrors; this.#ignoreHTTPSErrors = ignoreHTTPSErrors;
this.#frameManager = frameManager; this.#frameManager = frameManager;
for (const [event, handler] of this.#handlers) {
this.#client.on(event, handler);
}
} }
async updateClient(client: CDPSession): Promise<void> { async addClient(client: CDPSession): Promise<void> {
this.#client = client; if (this.#clients.has(client)) {
return;
}
const listeners: Array<{event: string | symbol; handler: Handler}> = [];
this.#clients.set(client, listeners);
for (const [event, handler] of this.#handlers) { for (const [event, handler] of this.#handlers) {
this.#client.on(event, handler); listeners.push({
event,
handler: handler.bind(this, client),
});
client.on(event, listeners.at(-1)!.handler);
} }
this.#deferredInit = undefined; listeners.push({
await this.initialize(); event: CDPSessionEmittedEvents.Disconnected,
} handler: this.#removeClient.bind(this, client),
});
/** client.on(CDPSessionEmittedEvents.Disconnected, listeners.at(-1)!.handler);
* Initialize calls should avoid async dependencies between CDP calls as those await Promise.all([
* might not resolve until after the target is resumed causing a deadlock.
*/
initialize(): Promise<void> {
if (this.#deferredInit) {
return this.#deferredInit.valueOrThrow();
}
this.#deferredInit = createDebuggableDeferred(
'NetworkManager initialization timed out'
);
const init = Promise.all([
this.#ignoreHTTPSErrors this.#ignoreHTTPSErrors
? this.#client.send('Security.setIgnoreCertificateErrors', { ? client.send('Security.setIgnoreCertificateErrors', {
ignore: true, ignore: true,
}) })
: null, : null,
this.#client.send('Network.enable'), client.send('Network.enable'),
this.#applyExtraHTTPHeaders(client),
this.#applyNetworkConditions(client),
this.#applyProtocolCacheDisabled(client),
this.#applyProtocolRequestInterception(client),
this.#applyUserAgent(client),
]); ]);
const deferredInitPromise = this.#deferredInit; }
init
.then(() => { async #removeClient(client: CDPSession) {
deferredInitPromise.resolve(); const listeners = this.#clients.get(client);
}) for (const {event, handler} of listeners || []) {
.catch(err => { client.off(event, handler);
deferredInitPromise.reject(err); }
}); this.#clients.delete(client);
return this.#deferredInit.valueOrThrow();
} }
async authenticate(credentials?: Credentials): Promise<void> { async authenticate(credentials?: Credentials): Promise<void> {
this.#credentials = credentials; this.#credentials = credentials;
await this.#updateProtocolRequestInterception(); const enabled = this.#userRequestInterceptionEnabled || !!this.#credentials;
if (enabled === this.#protocolRequestInterceptionEnabled) {
return;
}
this.#protocolRequestInterceptionEnabled = enabled;
await this.#applyToAllClients(
this.#applyProtocolRequestInterception.bind(this)
);
} }
async setExtraHTTPHeaders( async setExtraHTTPHeaders(
@ -178,7 +177,15 @@ export class NetworkManager extends EventEmitter {
); );
this.#extraHTTPHeaders[key.toLowerCase()] = value; this.#extraHTTPHeaders[key.toLowerCase()] = value;
} }
await this.#client.send('Network.setExtraHTTPHeaders', {
await this.#applyToAllClients(this.#applyExtraHTTPHeaders.bind(this));
}
async #applyExtraHTTPHeaders(client: CDPSession) {
if (this.#extraHTTPHeaders === undefined) {
return;
}
await client.send('Network.setExtraHTTPHeaders', {
headers: this.#extraHTTPHeaders, headers: this.#extraHTTPHeaders,
}); });
} }
@ -192,13 +199,29 @@ export class NetworkManager extends EventEmitter {
} }
async setOfflineMode(value: boolean): Promise<void> { async setOfflineMode(value: boolean): Promise<void> {
if (!this.#emulatedNetworkConditions) {
this.#emulatedNetworkConditions = {
offline: false,
upload: -1,
download: -1,
latency: 0,
};
}
this.#emulatedNetworkConditions.offline = value; this.#emulatedNetworkConditions.offline = value;
await this.#updateNetworkConditions(); await this.#applyToAllClients(this.#applyNetworkConditions.bind(this));
} }
async emulateNetworkConditions( async emulateNetworkConditions(
networkConditions: NetworkConditions | null networkConditions: NetworkConditions | null
): Promise<void> { ): Promise<void> {
if (!this.#emulatedNetworkConditions) {
this.#emulatedNetworkConditions = {
offline: false,
upload: -1,
download: -1,
latency: 0,
};
}
this.#emulatedNetworkConditions.upload = networkConditions this.#emulatedNetworkConditions.upload = networkConditions
? networkConditions.upload ? networkConditions.upload
: -1; : -1;
@ -209,11 +232,22 @@ export class NetworkManager extends EventEmitter {
? networkConditions.latency ? networkConditions.latency
: 0; : 0;
await this.#updateNetworkConditions(); await this.#applyToAllClients(this.#applyNetworkConditions.bind(this));
} }
async #updateNetworkConditions(): Promise<void> { async #applyToAllClients(fn: (client: CDPSession) => Promise<unknown>) {
await this.#client.send('Network.emulateNetworkConditions', { await Promise.all(
Array.from(this.#clients.keys()).map(client => {
return fn(client);
})
);
}
async #applyNetworkConditions(client: CDPSession): Promise<void> {
if (this.#emulatedNetworkConditions === undefined) {
return;
}
await client.send('Network.emulateNetworkConditions', {
offline: this.#emulatedNetworkConditions.offline, offline: this.#emulatedNetworkConditions.offline,
latency: this.#emulatedNetworkConditions.latency, latency: this.#emulatedNetworkConditions.latency,
uploadThroughput: this.#emulatedNetworkConditions.upload, uploadThroughput: this.#emulatedNetworkConditions.upload,
@ -225,55 +259,71 @@ export class NetworkManager extends EventEmitter {
userAgent: string, userAgent: string,
userAgentMetadata?: Protocol.Emulation.UserAgentMetadata userAgentMetadata?: Protocol.Emulation.UserAgentMetadata
): Promise<void> { ): Promise<void> {
await this.#client.send('Network.setUserAgentOverride', { this.#userAgent = userAgent;
userAgent: userAgent, this.#userAgentMetadata = userAgentMetadata;
userAgentMetadata: userAgentMetadata, await this.#applyToAllClients(this.#applyUserAgent.bind(this));
}
async #applyUserAgent(client: CDPSession) {
if (this.#userAgent === undefined) {
return;
}
await client.send('Network.setUserAgentOverride', {
userAgent: this.#userAgent,
userAgentMetadata: this.#userAgentMetadata,
}); });
} }
async setCacheEnabled(enabled: boolean): Promise<void> { async setCacheEnabled(enabled: boolean): Promise<void> {
this.#userCacheDisabled = !enabled; this.#userCacheDisabled = !enabled;
await this.#updateProtocolCacheDisabled(); await this.#applyToAllClients(this.#applyProtocolCacheDisabled.bind(this));
} }
async setRequestInterception(value: boolean): Promise<void> { async setRequestInterception(value: boolean): Promise<void> {
this.#userRequestInterceptionEnabled = value; this.#userRequestInterceptionEnabled = value;
await this.#updateProtocolRequestInterception();
}
async #updateProtocolRequestInterception(): Promise<void> {
const enabled = this.#userRequestInterceptionEnabled || !!this.#credentials; const enabled = this.#userRequestInterceptionEnabled || !!this.#credentials;
if (enabled === this.#protocolRequestInterceptionEnabled) { if (enabled === this.#protocolRequestInterceptionEnabled) {
return; return;
} }
this.#protocolRequestInterceptionEnabled = enabled; this.#protocolRequestInterceptionEnabled = enabled;
if (enabled) { await this.#applyToAllClients(
this.#applyProtocolRequestInterception.bind(this)
);
}
async #applyProtocolRequestInterception(client: CDPSession): Promise<void> {
if (this.#userCacheDisabled === undefined) {
this.#userCacheDisabled = false;
}
if (this.#protocolRequestInterceptionEnabled) {
await Promise.all([ await Promise.all([
this.#updateProtocolCacheDisabled(), this.#applyProtocolCacheDisabled(client),
this.#client.send('Fetch.enable', { client.send('Fetch.enable', {
handleAuthRequests: true, handleAuthRequests: true,
patterns: [{urlPattern: '*'}], patterns: [{urlPattern: '*'}],
}), }),
]); ]);
} else { } else {
await Promise.all([ await Promise.all([
this.#updateProtocolCacheDisabled(), this.#applyProtocolCacheDisabled(client),
this.#client.send('Fetch.disable'), client.send('Fetch.disable'),
]); ]);
} }
} }
#cacheDisabled(): boolean { async #applyProtocolCacheDisabled(client: CDPSession): Promise<void> {
return this.#userCacheDisabled; if (this.#userCacheDisabled === undefined) {
} return;
}
async #updateProtocolCacheDisabled(): Promise<void> { await client.send('Network.setCacheDisabled', {
await this.#client.send('Network.setCacheDisabled', { cacheDisabled: this.#userCacheDisabled,
cacheDisabled: this.#cacheDisabled(),
}); });
} }
#onRequestWillBeSent(event: Protocol.Network.RequestWillBeSentEvent): void { #onRequestWillBeSent(
client: CDPSession,
event: Protocol.Network.RequestWillBeSentEvent
): void {
// Request interception doesn't happen for data URLs with Network Service. // Request interception doesn't happen for data URLs with Network Service.
if ( if (
this.#userRequestInterceptionEnabled && this.#userRequestInterceptionEnabled &&
@ -291,16 +341,19 @@ export class NetworkManager extends EventEmitter {
if (requestPausedEvent) { if (requestPausedEvent) {
const {requestId: fetchRequestId} = requestPausedEvent; const {requestId: fetchRequestId} = requestPausedEvent;
this.#patchRequestEventHeaders(event, requestPausedEvent); this.#patchRequestEventHeaders(event, requestPausedEvent);
this.#onRequest(event, fetchRequestId); this.#onRequest(client, event, fetchRequestId);
this.#networkEventManager.forgetRequestPaused(networkRequestId); this.#networkEventManager.forgetRequestPaused(networkRequestId);
} }
return; return;
} }
this.#onRequest(event, undefined); this.#onRequest(client, event, undefined);
} }
#onAuthRequired(event: Protocol.Fetch.AuthRequiredEvent): void { #onAuthRequired(
client: CDPSession,
event: Protocol.Fetch.AuthRequiredEvent
): void {
let response: Protocol.Fetch.AuthChallengeResponse['response'] = 'Default'; let response: Protocol.Fetch.AuthChallengeResponse['response'] = 'Default';
if (this.#attemptedAuthentications.has(event.requestId)) { if (this.#attemptedAuthentications.has(event.requestId)) {
response = 'CancelAuth'; response = 'CancelAuth';
@ -312,7 +365,7 @@ export class NetworkManager extends EventEmitter {
username: undefined, username: undefined,
password: undefined, password: undefined,
}; };
this.#client client
.send('Fetch.continueWithAuth', { .send('Fetch.continueWithAuth', {
requestId: event.requestId, requestId: event.requestId,
authChallengeResponse: {response, username, password}, authChallengeResponse: {response, username, password},
@ -327,12 +380,15 @@ export class NetworkManager extends EventEmitter {
* CDP may send multiple Fetch.requestPaused * CDP may send multiple Fetch.requestPaused
* for the same Network.requestWillBeSent. * for the same Network.requestWillBeSent.
*/ */
#onRequestPaused(event: Protocol.Fetch.RequestPausedEvent): void { #onRequestPaused(
client: CDPSession,
event: Protocol.Fetch.RequestPausedEvent
): void {
if ( if (
!this.#userRequestInterceptionEnabled && !this.#userRequestInterceptionEnabled &&
this.#protocolRequestInterceptionEnabled this.#protocolRequestInterceptionEnabled
) { ) {
this.#client client
.send('Fetch.continueRequest', { .send('Fetch.continueRequest', {
requestId: event.requestId, requestId: event.requestId,
}) })
@ -342,7 +398,7 @@ export class NetworkManager extends EventEmitter {
const {networkId: networkRequestId, requestId: fetchRequestId} = event; const {networkId: networkRequestId, requestId: fetchRequestId} = event;
if (!networkRequestId) { if (!networkRequestId) {
this.#onRequestWithoutNetworkInstrumentation(event); this.#onRequestWithoutNetworkInstrumentation(client, event);
return; return;
} }
@ -364,7 +420,7 @@ export class NetworkManager extends EventEmitter {
if (requestWillBeSentEvent) { if (requestWillBeSentEvent) {
this.#patchRequestEventHeaders(requestWillBeSentEvent, event); this.#patchRequestEventHeaders(requestWillBeSentEvent, event);
this.#onRequest(requestWillBeSentEvent, fetchRequestId); this.#onRequest(client, requestWillBeSentEvent, fetchRequestId);
} else { } else {
this.#networkEventManager.storeRequestPaused(networkRequestId, event); this.#networkEventManager.storeRequestPaused(networkRequestId, event);
} }
@ -382,6 +438,7 @@ export class NetworkManager extends EventEmitter {
} }
#onRequestWithoutNetworkInstrumentation( #onRequestWithoutNetworkInstrumentation(
client: CDPSession,
event: Protocol.Fetch.RequestPausedEvent event: Protocol.Fetch.RequestPausedEvent
): void { ): void {
// If an event has no networkId it should not have any network events. We // If an event has no networkId it should not have any network events. We
@ -391,7 +448,7 @@ export class NetworkManager extends EventEmitter {
: null; : null;
const request = new HTTPRequest( const request = new HTTPRequest(
this.#client, client,
frame, frame,
event.requestId, event.requestId,
this.#userRequestInterceptionEnabled, this.#userRequestInterceptionEnabled,
@ -403,6 +460,7 @@ export class NetworkManager extends EventEmitter {
} }
#onRequest( #onRequest(
client: CDPSession,
event: Protocol.Network.RequestWillBeSentEvent, event: Protocol.Network.RequestWillBeSentEvent,
fetchRequestId?: FetchRequestId fetchRequestId?: FetchRequestId
): void { ): void {
@ -434,6 +492,7 @@ export class NetworkManager extends EventEmitter {
// requestWillBeSent event. // requestWillBeSent event.
if (request) { if (request) {
this.#handleRequestRedirect( this.#handleRequestRedirect(
client,
request, request,
event.redirectResponse, event.redirectResponse,
redirectResponseExtraInfo redirectResponseExtraInfo
@ -446,7 +505,7 @@ export class NetworkManager extends EventEmitter {
: null; : null;
const request = new HTTPRequest( const request = new HTTPRequest(
this.#client, client,
frame, frame,
fetchRequestId, fetchRequestId,
this.#userRequestInterceptionEnabled, this.#userRequestInterceptionEnabled,
@ -459,6 +518,7 @@ export class NetworkManager extends EventEmitter {
} }
#onRequestServedFromCache( #onRequestServedFromCache(
_client: CDPSession,
event: Protocol.Network.RequestServedFromCacheEvent event: Protocol.Network.RequestServedFromCacheEvent
): void { ): void {
const request = this.#networkEventManager.getRequest(event.requestId); const request = this.#networkEventManager.getRequest(event.requestId);
@ -469,12 +529,13 @@ export class NetworkManager extends EventEmitter {
} }
#handleRequestRedirect( #handleRequestRedirect(
client: CDPSession,
request: HTTPRequest, request: HTTPRequest,
responsePayload: Protocol.Network.Response, responsePayload: Protocol.Network.Response,
extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null
): void { ): void {
const response = new HTTPResponse( const response = new HTTPResponse(
this.#client, client,
request, request,
responsePayload, responsePayload,
extraInfo extraInfo
@ -490,6 +551,7 @@ export class NetworkManager extends EventEmitter {
} }
#emitResponseEvent( #emitResponseEvent(
client: CDPSession,
responseReceived: Protocol.Network.ResponseReceivedEvent, responseReceived: Protocol.Network.ResponseReceivedEvent,
extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null
): void { ): void {
@ -521,7 +583,7 @@ export class NetworkManager extends EventEmitter {
} }
const response = new HTTPResponse( const response = new HTTPResponse(
this.#client, client,
request, request,
responseReceived.response, responseReceived.response,
extraInfo extraInfo
@ -530,7 +592,10 @@ export class NetworkManager extends EventEmitter {
this.emit(NetworkManagerEmittedEvents.Response, response); this.emit(NetworkManagerEmittedEvents.Response, response);
} }
#onResponseReceived(event: Protocol.Network.ResponseReceivedEvent): void { #onResponseReceived(
client: CDPSession,
event: Protocol.Network.ResponseReceivedEvent
): void {
const request = this.#networkEventManager.getRequest(event.requestId); const request = this.#networkEventManager.getRequest(event.requestId);
let extraInfo = null; let extraInfo = null;
if (request && !request._fromMemoryCache && event.hasExtraInfo) { if (request && !request._fromMemoryCache && event.hasExtraInfo) {
@ -545,10 +610,11 @@ export class NetworkManager extends EventEmitter {
return; return;
} }
} }
this.#emitResponseEvent(event, extraInfo); this.#emitResponseEvent(client, event, extraInfo);
} }
#onResponseReceivedExtraInfo( #onResponseReceivedExtraInfo(
client: CDPSession,
event: Protocol.Network.ResponseReceivedExtraInfoEvent event: Protocol.Network.ResponseReceivedExtraInfoEvent
): void { ): void {
// We may have skipped a redirect response/request pair due to waiting for // We may have skipped a redirect response/request pair due to waiting for
@ -559,7 +625,7 @@ export class NetworkManager extends EventEmitter {
); );
if (redirectInfo) { if (redirectInfo) {
this.#networkEventManager.responseExtraInfo(event.requestId).push(event); this.#networkEventManager.responseExtraInfo(event.requestId).push(event);
this.#onRequest(redirectInfo.event, redirectInfo.fetchRequestId); this.#onRequest(client, redirectInfo.event, redirectInfo.fetchRequestId);
return; return;
} }
@ -570,7 +636,11 @@ export class NetworkManager extends EventEmitter {
); );
if (queuedEvents) { if (queuedEvents) {
this.#networkEventManager.forgetQueuedEventGroup(event.requestId); this.#networkEventManager.forgetQueuedEventGroup(event.requestId);
this.#emitResponseEvent(queuedEvents.responseReceivedEvent, event); this.#emitResponseEvent(
client,
queuedEvents.responseReceivedEvent,
event
);
if (queuedEvents.loadingFinishedEvent) { if (queuedEvents.loadingFinishedEvent) {
this.#emitLoadingFinished(queuedEvents.loadingFinishedEvent); this.#emitLoadingFinished(queuedEvents.loadingFinishedEvent);
} }
@ -597,7 +667,10 @@ export class NetworkManager extends EventEmitter {
} }
} }
#onLoadingFinished(event: Protocol.Network.LoadingFinishedEvent): void { #onLoadingFinished(
_client: CDPSession,
event: Protocol.Network.LoadingFinishedEvent
): void {
// If the response event for this request is still waiting on a // If the response event for this request is still waiting on a
// corresponding ExtraInfo event, then wait to emit this event too. // corresponding ExtraInfo event, then wait to emit this event too.
const queuedEvents = this.#networkEventManager.getQueuedEventGroup( const queuedEvents = this.#networkEventManager.getQueuedEventGroup(
@ -627,7 +700,10 @@ export class NetworkManager extends EventEmitter {
this.emit(NetworkManagerEmittedEvents.RequestFinished, request); this.emit(NetworkManagerEmittedEvents.RequestFinished, request);
} }
#onLoadingFailed(event: Protocol.Network.LoadingFailedEvent): void { #onLoadingFailed(
_client: CDPSession,
event: Protocol.Network.LoadingFailedEvent
): void {
// If the response event for this request is still waiting on a // If the response event for this request is still waiting on a
// corresponding ExtraInfo event, then wait to emit this event too. // corresponding ExtraInfo event, then wait to emit this event too.
const queuedEvents = this.#networkEventManager.getQueuedEventGroup( const queuedEvents = this.#networkEventManager.getQueuedEventGroup(

View File

@ -329,6 +329,15 @@ export class CDPPage extends Page {
await this.#frameManager.swapFrameTree(newSession); await this.#frameManager.swapFrameTree(newSession);
this.#setupEventListeners(); this.#setupEventListeners();
}); });
this.#tabSession?.on(
CDPSessionEmittedEvents.Ready,
(session: CDPSessionImpl) => {
if (session._target()._subtype() !== 'prerender') {
return;
}
this.#frameManager.registerSecondaryPage(session).catch(debugError);
}
);
} }
#setupEventListeners() { #setupEventListeners() {
@ -399,7 +408,7 @@ export class CDPPage extends Page {
async #initialize(): Promise<void> { async #initialize(): Promise<void> {
try { try {
await Promise.all([ await Promise.all([
this.#frameManager.initialize(), this.#frameManager.initialize(this.#client),
this.#client.send('Performance.enable'), this.#client.send('Performance.enable'),
this.#client.send('Log.enable'), this.#client.send('Log.enable'),
]); ]);

View File

@ -3911,6 +3911,12 @@
"parameters": ["firefox", "webDriverBiDi"], "parameters": ["firefox", "webDriverBiDi"],
"expectations": ["SKIP"] "expectations": ["SKIP"]
}, },
{
"testIdPattern": "[prerender.spec] Prerender with network requests can receive requests from the prerendered page",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["SKIP"]
},
{ {
"testIdPattern": "[proxy.spec] request proxy in incognito browser context should proxy requests when configured at browser level", "testIdPattern": "[proxy.spec] request proxy in incognito browser context should proxy requests when configured at browser level",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],

View File

@ -1,2 +1,5 @@
<a id="navigate-within-document" href="#nav">Navigate within document</a> <a id="navigate-within-document" href="#nav">Navigate within document</a>
<a name="nav"></a> <a name="nav"></a>
<script>
fetch('oopif.html?requestFromOOPIF')
</script>

View File

@ -1,4 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<head></head> <head>
<script>fetch('target.html?fromPrerendered')</script>
</head> </head>
<body>target</body> <body>target</body>

View File

@ -307,17 +307,23 @@ describeWithDebugLogs('OOPIF', function () {
it('should load oopif iframes with subresources and request interception', async () => { it('should load oopif iframes with subresources and request interception', async () => {
const {server, page, context} = state; const {server, page, context} = state;
const frame = page.waitForFrame(frame => { const framePromise = page.waitForFrame(frame => {
return frame.url().endsWith('/oopif.html'); return frame.url().endsWith('/oopif.html');
}); });
await page.setRequestInterception(true);
page.on('request', request => { page.on('request', request => {
return request.continue(); void request.continue();
});
await page.setRequestInterception(true);
const requestPromise = page.waitForRequest(request => {
return request.url().includes('requestFromOOPIF');
}); });
await page.goto(server.PREFIX + '/dynamic-oopif.html'); await page.goto(server.PREFIX + '/dynamic-oopif.html');
await frame; const frame = await framePromise;
const request = await requestPromise;
expect(oopifs(context)).toHaveLength(1); expect(oopifs(context)).toHaveLength(1);
expect(request.frame()).toBe(frame);
}); });
it('should support frames within OOP iframes', async () => { it('should support frames within OOP iframes', async () => {
const {server, page} = state; const {server, page} = state;

View File

@ -89,4 +89,44 @@ describe('Prerender', function () {
expect(mainFrame).toBe(page.mainFrame()); expect(mainFrame).toBe(page.mainFrame());
}); });
}); });
describe('with network requests', () => {
it('can receive requests from the prerendered page', async () => {
const {page, server} = await getTestState();
const urls: string[] = [];
page.on('request', request => {
urls.push(request.url());
});
await page.goto(server.PREFIX + '/prerender/index.html');
const button = await page.waitForSelector('button');
await button?.click();
const mainFrame = page.mainFrame();
const link = await mainFrame.waitForSelector('a');
await Promise.all([mainFrame.waitForNavigation(), link?.click()]);
expect(mainFrame).toBe(page.mainFrame());
expect(
await mainFrame.evaluate(() => {
return document.body.innerText;
})
).toBe('target');
expect(mainFrame).toBe(page.mainFrame());
expect(
urls.find(url => {
return url.endsWith('prerender/target.html');
})
).toBeTruthy();
expect(
urls.find(url => {
return url.includes('prerender/index.html');
})
).toBeTruthy();
expect(
urls.find(url => {
return url.includes('prerender/target.html?fromPrerendered');
})
).toBeTruthy();
});
});
}); });