fix(chromium): response.body() for worker requests should work (#23764)

Previously, worker requests used page's session to call
`Network.getResponseBody`.

Fixes #23750.
This commit is contained in:
Dmitry Gozman 2023-06-16 20:44:32 -07:00 committed by GitHub
parent 426d6dde0e
commit 09b1e3ffa9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 73 additions and 66 deletions

View file

@ -29,8 +29,13 @@ import type { CRPage } from './crPage';
import { assert, headersObjectToArray } from '../../utils';
import type { CRServiceWorker } from './crServiceWorker';
type SessionInfo = {
session: CRSession;
workerFrame?: frames.Frame;
};
export class CRNetworkManager {
private _client: CRSession;
private _session: CRSession;
private _page: Page | null;
private _serviceWorker: CRServiceWorker | null;
private _parentManager: CRNetworkManager | null;
@ -44,42 +49,42 @@ export class CRNetworkManager {
private _eventListeners: RegisteredListener[];
private _responseExtraInfoTracker = new ResponseExtraInfoTracker();
constructor(client: CRSession, page: Page | null, serviceWorker: CRServiceWorker | null, parentManager: CRNetworkManager | null) {
this._client = client;
constructor(session: CRSession, page: Page | null, serviceWorker: CRServiceWorker | null, parentManager: CRNetworkManager | null) {
this._session = session;
this._page = page;
this._serviceWorker = serviceWorker;
this._parentManager = parentManager;
this._eventListeners = this.instrumentNetworkEvents(client);
this._eventListeners = this.instrumentNetworkEvents({ session });
}
instrumentNetworkEvents(session: CRSession, workerFrame?: frames.Frame): RegisteredListener[] {
instrumentNetworkEvents(sessionInfo: SessionInfo): RegisteredListener[] {
const listeners = [
eventsHelper.addEventListener(session, 'Fetch.requestPaused', this._onRequestPaused.bind(this, workerFrame)),
eventsHelper.addEventListener(session, 'Fetch.authRequired', this._onAuthRequired.bind(this)),
eventsHelper.addEventListener(session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this, workerFrame)),
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)),
eventsHelper.addEventListener(session, 'Network.responseReceivedExtraInfo', this._onResponseReceivedExtraInfo.bind(this)),
eventsHelper.addEventListener(session, 'Network.loadingFinished', this._onLoadingFinished.bind(this)),
eventsHelper.addEventListener(session, 'Network.loadingFailed', this._onLoadingFailed.bind(this, workerFrame)),
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)),
];
if (this._page) {
listeners.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)),
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)),
]);
}
return listeners;
}
async initialize() {
await this._client.send('Network.enable');
await this._session.send('Network.enable');
}
dispose() {
@ -92,7 +97,7 @@ export class CRNetworkManager {
}
async setOffline(offline: boolean) {
await this._client.send('Network.emulateNetworkConditions', {
await this._session.send('Network.emulateNetworkConditions', {
offline,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
@ -113,41 +118,41 @@ export class CRNetworkManager {
this._protocolRequestInterceptionEnabled = enabled;
if (enabled) {
await Promise.all([
this._client.send('Network.setCacheDisabled', { cacheDisabled: true }),
this._client.send('Fetch.enable', {
this._session.send('Network.setCacheDisabled', { cacheDisabled: true }),
this._session.send('Fetch.enable', {
handleAuthRequests: true,
patterns: [{ urlPattern: '*', requestStage: 'Request' }],
}),
]);
} else {
await Promise.all([
this._client.send('Network.setCacheDisabled', { cacheDisabled: false }),
this._client.send('Fetch.disable')
this._session.send('Network.setCacheDisabled', { cacheDisabled: false }),
this._session.send('Fetch.disable')
]);
}
}
async clearCache() {
// Sending 'Network.setCacheDisabled' with 'cacheDisabled = true' will clear the MemoryCache.
await this._client.send('Network.setCacheDisabled', { cacheDisabled: true });
await this._session.send('Network.setCacheDisabled', { cacheDisabled: true });
if (!this._protocolRequestInterceptionEnabled)
await this._client.send('Network.setCacheDisabled', { cacheDisabled: false });
await this._client.send('Network.clearBrowserCache');
await this._session.send('Network.setCacheDisabled', { cacheDisabled: false });
await this._session.send('Network.clearBrowserCache');
}
_onRequestWillBeSent(workerFrame: frames.Frame | undefined, event: Protocol.Network.requestWillBeSentPayload) {
_onRequestWillBeSent(sessionInfo: SessionInfo, event: Protocol.Network.requestWillBeSentPayload) {
// Request interception doesn't happen for data URLs with Network Service.
if (this._protocolRequestInterceptionEnabled && !event.request.url.startsWith('data:')) {
const requestId = event.requestId;
const requestPausedEvent = this._requestIdToRequestPausedEvent.get(requestId);
if (requestPausedEvent) {
this._onRequest(workerFrame, event, requestPausedEvent);
this._onRequest(sessionInfo, event, requestPausedEvent);
this._requestIdToRequestPausedEvent.delete(requestId);
} else {
this._requestIdToRequestWillBeSentEvent.set(event.requestId, event);
}
} else {
this._onRequest(workerFrame, event, null);
this._onRequest(sessionInfo, event, null);
}
}
@ -169,7 +174,7 @@ export class CRNetworkManager {
this._attemptedAuthentications.add(event.requestId);
}
const { username, password } = shouldProvideCredentials && this._credentials ? this._credentials : { username: undefined, password: undefined };
this._client._sendMayFail('Fetch.continueWithAuth', {
this._session._sendMayFail('Fetch.continueWithAuth', {
requestId: event.requestId,
authChallengeResponse: { response, username, password },
});
@ -181,12 +186,12 @@ export class CRNetworkManager {
return !this._credentials.origin || new URL(url).origin.toLowerCase() === this._credentials.origin.toLowerCase();
}
_onRequestPaused(workerFrame: frames.Frame | undefined, event: Protocol.Fetch.requestPausedPayload) {
_onRequestPaused(sessionInfo: SessionInfo, event: Protocol.Fetch.requestPausedPayload) {
if (!event.networkId) {
// Fetch without networkId means that request was not recongnized by inspector, and
// it will never receive Network.requestWillBeSent. Most likely, this is an internal request
// that we can safely fail.
this._client._sendMayFail('Fetch.failRequest', {
this._session._sendMayFail('Fetch.failRequest', {
requestId: event.requestId,
errorReason: 'Aborted',
});
@ -198,14 +203,14 @@ export class CRNetworkManager {
const requestId = event.networkId;
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(requestId);
if (requestWillBeSentEvent) {
this._onRequest(workerFrame, requestWillBeSentEvent, event);
this._onRequest(sessionInfo, requestWillBeSentEvent, event);
this._requestIdToRequestWillBeSentEvent.delete(requestId);
} else {
this._requestIdToRequestPausedEvent.set(requestId, event);
}
}
_onRequest(workerFrame: frames.Frame | undefined, requestWillBeSentEvent: Protocol.Network.requestWillBeSentPayload, requestPausedEvent: Protocol.Fetch.requestPausedPayload | null) {
_onRequest(sessionInfo: SessionInfo, requestWillBeSentEvent: Protocol.Network.requestWillBeSentPayload, requestPausedEvent: Protocol.Fetch.requestPausedPayload | null) {
if (requestWillBeSentEvent.request.url.startsWith('data:'))
return;
let redirectedFrom: InterceptableRequest | null = null;
@ -217,7 +222,7 @@ export class CRNetworkManager {
redirectedFrom = request;
}
}
let frame = requestWillBeSentEvent.frameId ? this._page?._frameManager.frame(requestWillBeSentEvent.frameId) : workerFrame;
let frame = requestWillBeSentEvent.frameId ? this._page?._frameManager.frame(requestWillBeSentEvent.frameId) : sessionInfo.workerFrame;
// Requests from workers lack frameId, because we receive Network.requestWillBeSent
// on the worker target. However, we receive Fetch.requestPaused on the page target,
// and lack workerFrame there. Luckily, Fetch.requestPaused provides a frameId.
@ -246,7 +251,7 @@ export class CRNetworkManager {
];
if (requestHeaders['Access-Control-Request-Headers'])
responseHeaders.push({ name: 'Access-Control-Allow-Headers', value: requestHeaders['Access-Control-Request-Headers'] });
this._client._sendMayFail('Fetch.fulfillRequest', {
this._session._sendMayFail('Fetch.fulfillRequest', {
requestId: requestPausedEvent.requestId,
responseCode: 204,
responsePhrase: network.STATUS_TEXTS['204'],
@ -259,7 +264,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._client._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId });
this._session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId });
return;
}
@ -267,14 +272,14 @@ export class CRNetworkManager {
if (requestPausedEvent) {
// We do not support intercepting redirects.
if (redirectedFrom || (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled))
this._client._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId });
this._session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId });
else
route = new RouteImpl(this._client, requestPausedEvent.requestId);
route = new RouteImpl(this._session, requestPausedEvent.requestId);
}
const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document';
const documentId = isNavigationRequest ? requestWillBeSentEvent.loaderId : undefined;
const request = new InterceptableRequest({
owningNetworkManager: this,
session: sessionInfo.session,
context: (this._page || this._serviceWorker)!._browserContext,
frame: frame || null,
serviceWorker: this._serviceWorker || null,
@ -300,19 +305,19 @@ export class CRNetworkManager {
const contentLengthHeader = Object.entries(responsePayload.headers).find(header => header[0].toLowerCase() === 'content-length');
const expectedLength = contentLengthHeader ? +contentLengthHeader[1] : undefined;
const client = request._owningNetworkManager._client;
const response = await client.send('Network.getResponseBody', { requestId: request._requestId });
const session = request.session;
const response = await session.send('Network.getResponseBody', { requestId: request._requestId });
if (response.body || !expectedLength)
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
// For <link prefetch we are going to receive empty body with non-emtpy content-length expectation. Reach out for the actual content.
const resource = await client.send('Network.loadNetworkResource', { url: request.request.url(), frameId: this._serviceWorker ? undefined : request.request.frame()!._id, options: { disableCache: false, includeCredentials: true } });
const resource = await session.send('Network.loadNetworkResource', { url: request.request.url(), frameId: this._serviceWorker ? undefined : request.request.frame()!._id, options: { disableCache: false, includeCredentials: true } });
const chunks: Buffer[] = [];
while (resource.resource.stream) {
const chunk = await client.send('IO.read', { handle: resource.resource.stream });
const chunk = await session.send('IO.read', { handle: resource.resource.stream });
chunks.push(Buffer.from(chunk.data, chunk.base64Encoded ? 'base64' : 'utf-8'));
if (chunk.eof) {
await client.send('IO.close', { handle: resource.resource.stream });
await session.send('IO.close', { handle: resource.resource.stream });
break;
}
}
@ -379,7 +384,7 @@ export class CRNetworkManager {
this._responseExtraInfoTracker.responseReceivedExtraInfo(event);
}
_onResponseReceived(event: Protocol.Network.responseReceivedPayload) {
_onResponseReceived(sessionInfo: SessionInfo, event: Protocol.Network.responseReceivedPayload) {
let request = this._requestIdToRequest.get(event.requestId);
// For frame-level Requests that are handled by a Service Worker's fetch handler, we'll never get a requestPaused event, so we need to
// manually create the request. In an ideal world, crNetworkManager would be able to know this on Network.requestWillBeSent, but there
@ -388,7 +393,7 @@ export class CRNetworkManager {
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(event.requestId);
if (requestWillBeSentEvent) {
this._requestIdToRequestWillBeSentEvent.delete(event.requestId);
this._onRequest(undefined, requestWillBeSentEvent, null /* requestPausedPayload */);
this._onRequest(sessionInfo, requestWillBeSentEvent, null /* requestPausedPayload */);
request = this._requestIdToRequest.get(event.requestId);
}
}
@ -424,7 +429,7 @@ export class CRNetworkManager {
(this._page?._frameManager || this._serviceWorker)!.reportRequestFinished(request.request, response);
}
_onLoadingFailed(workerFrame: frames.Frame | undefined, event: Protocol.Network.loadingFailedPayload) {
_onLoadingFailed(sessionInfo: SessionInfo, event: Protocol.Network.loadingFailedPayload) {
this._responseExtraInfoTracker.loadingFailed(event);
let request = this._requestIdToRequest.get(event.requestId);
@ -438,7 +443,7 @@ export class CRNetworkManager {
// We stop waiting for Fetch.requestPaused (it might never come), and dispatch request event
// right away, followed by requestfailed event.
this._requestIdToRequestWillBeSentEvent.delete(event.requestId);
this._onRequest(workerFrame, requestWillBeSentEvent, null);
this._onRequest(sessionInfo, requestWillBeSentEvent, null);
request = this._requestIdToRequest.get(event.requestId);
}
}
@ -469,7 +474,7 @@ export class CRNetworkManager {
if (!request || request._documentId !== requestId)
return;
this._requestIdToRequest.set(requestId, request);
request._owningNetworkManager = this;
request.session = this._session;
this._parentManager._requestIdToRequest.delete(requestId);
if (request._interceptionId && this._parentManager._attemptedAuthentications.has(request._interceptionId)) {
this._parentManager._attemptedAuthentications.delete(request._interceptionId);
@ -488,10 +493,10 @@ class InterceptableRequest {
readonly _wallTime: number;
private _route: RouteImpl | null;
private _redirectedFrom: InterceptableRequest | null;
_owningNetworkManager: CRNetworkManager;
session: CRSession;
constructor(options: {
owningNetworkManager: CRNetworkManager,
session: CRSession,
context: contexts.BrowserContext;
frame: frames.Frame | null;
serviceWorker: CRServiceWorker | null;
@ -501,8 +506,8 @@ class InterceptableRequest {
requestPausedEvent: Protocol.Fetch.requestPausedPayload | null;
redirectedFrom: InterceptableRequest | null;
}) {
const { owningNetworkManager, context, frame, documentId, route, requestWillBeSentEvent, requestPausedEvent, redirectedFrom, serviceWorker } = options;
this._owningNetworkManager = owningNetworkManager;
const { session, context, frame, documentId, route, requestWillBeSentEvent, requestPausedEvent, redirectedFrom, serviceWorker } = options;
this.session = session;
this._timestamp = requestWillBeSentEvent.timestamp;
this._wallTime = requestWillBeSentEvent.wallTime;
this._requestId = requestWillBeSentEvent.requestId;
@ -534,18 +539,18 @@ class InterceptableRequest {
}
class RouteImpl implements network.RouteDelegate {
private readonly _client: CRSession;
private readonly _session: CRSession;
private _interceptionId: string;
constructor(client: CRSession, interceptionId: string) {
this._client = client;
constructor(session: CRSession, interceptionId: string) {
this._session = session;
this._interceptionId = interceptionId;
}
async continue(request: network.Request, overrides: types.NormalizedContinueOverrides): Promise<void> {
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
await this._client._sendMayFail('Fetch.continueRequest', {
await this._session._sendMayFail('Fetch.continueRequest', {
requestId: this._interceptionId!,
url: overrides.url,
headers: overrides.headers,
@ -560,7 +565,7 @@ class RouteImpl implements network.RouteDelegate {
const responseHeaders = splitSetCookieHeader(response.headers);
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
await this._client._sendMayFail('Fetch.fulfillRequest', {
await this._session._sendMayFail('Fetch.fulfillRequest', {
requestId: this._interceptionId!,
responseCode: response.status,
responsePhrase: network.STATUS_TEXTS[String(response.status)],
@ -574,7 +579,7 @@ class RouteImpl implements network.RouteDelegate {
assert(errorReason, 'Unknown error code: ' + errorCode);
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
await this._client._sendMayFail('Fetch.failRequest', {
await this._session._sendMayFail('Fetch.failRequest', {
requestId: this._interceptionId!,
errorReason
});

View file

@ -758,7 +758,7 @@ class FrameSession {
});
session.on('Runtime.exceptionThrown', exception => this._page.emit(Page.Events.PageError, exceptionToError(exception.exceptionDetails)));
// TODO: attribute workers to the right frame.
this._networkManager.instrumentNetworkEvents(session, this._page._frameManager.frame(this._targetId)!);
this._networkManager.instrumentNetworkEvents({ session, workerFrame: this._page._frameManager.frame(this._targetId) ?? undefined });
}
_onDetachedFromTarget(event: Protocol.Target.detachedFromTargetPayload) {

View file

@ -18,6 +18,7 @@
import { test as it, expect } from './pageTest';
import { attachFrame } from '../config/utils';
import type { ConsoleMessage } from 'playwright-core';
import fs from 'fs';
it('Page.workers @smoke', async function({ page, server }) {
await Promise.all([
@ -143,7 +144,7 @@ it('should attribute network activity for worker inside iframe to the iframe', a
expect(request.frame()).toBe(frame);
});
it('should report network activity', async function({ page, server, browserName, browserMajorVersion }) {
it('should report network activity', async function({ page, server, browserName, browserMajorVersion, asset }) {
it.skip(browserName === 'firefox' && browserMajorVersion < 114, 'https://github.com/microsoft/playwright/issues/21760');
const [worker] = await Promise.all([
page.waitForEvent('worker'),
@ -158,6 +159,7 @@ it('should report network activity', async function({ page, server, browserName,
expect(request.url()).toBe(url);
expect(response.request()).toBe(request);
expect(response.ok()).toBe(true);
expect(await response.text()).toBe(fs.readFileSync(asset('one-style.css'), 'utf8'));
});
it('should report network activity on worker creation', async function({ page, server, browserName, browserMajorVersion }) {