Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(webkit): delay request event until requestIntercepted is received #28484

Merged
merged 3 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import type * as types from '../types';
import type { Protocol } from './protocol';
import type { WKSession } from './wkConnection';
import { assert, headersObjectToArray, headersArrayToObject } from '../../utils';
import { ManualPromise } from '../../utils/manualPromise';

const errorReasons: { [reason: string]: Protocol.Network.ResourceErrorType } = {
'aborted': 'Cancellation',
Expand All @@ -46,12 +45,10 @@ export class WKInterceptableRequest {
readonly _requestId: string;
_timestamp: number;
_wallTime: number;
readonly _route: WKRouteImpl | null;

constructor(session: WKSession, route: WKRouteImpl | null, frame: frames.Frame, event: Protocol.Network.requestWillBeSentPayload, redirectedFrom: WKInterceptableRequest | null, documentId: string | undefined) {
constructor(session: WKSession, frame: frames.Frame, event: Protocol.Network.requestWillBeSentPayload, redirectedFrom: WKInterceptableRequest | null, documentId: string | undefined) {
this._session = session;
this._requestId = event.requestId;
this._route = route;
const resourceType = event.type ? event.type.toLowerCase() : (redirectedFrom ? redirectedFrom.request.resourceType() : 'other');
let postDataBuffer = null;
this._timestamp = event.timestamp;
Expand Down Expand Up @@ -102,7 +99,6 @@ export class WKInterceptableRequest {
export class WKRouteImpl implements network.RouteDelegate {
private readonly _session: WKSession;
private readonly _requestId: string;
readonly _requestInterceptedPromise = new ManualPromise<void>();

constructor(session: WKSession, requestId: string) {
this._session = session;
Expand All @@ -112,7 +108,6 @@ export class WKRouteImpl implements network.RouteDelegate {
async abort(errorCode: string) {
const errorType = errorReasons[errorCode];
assert(errorType, 'Unknown error code: ' + errorCode);
await this._requestInterceptedPromise;
// 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._session.sendMayFail('Network.interceptRequestWithError', { requestId: this._requestId, errorType });
Expand All @@ -122,7 +117,6 @@ export class WKRouteImpl implements network.RouteDelegate {
if (300 <= response.status && response.status < 400)
throw new Error('Cannot fulfill with redirect status: ' + response.status);

await this._requestInterceptedPromise;
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
let mimeType = response.isBase64 ? 'application/octet-stream' : 'text/plain';
Expand All @@ -143,7 +137,6 @@ export class WKRouteImpl implements network.RouteDelegate {
}

async continue(request: network.Request, overrides: types.NormalizedContinueOverrides) {
await this._requestInterceptedPromise;
// 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._session.sendMayFail('Network.interceptWithRequest', {
Expand Down
69 changes: 46 additions & 23 deletions packages/playwright-core/src/server/webkit/wkPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export class WKPage implements PageDelegate {
private readonly _pageProxySession: WKSession;
readonly _opener: WKPage | null;
private readonly _requestIdToRequest = new Map<string, WKInterceptableRequest>();
private readonly _requestIdToRequestWillBeSentEvent = new Map<string, Protocol.Network.requestWillBeSentPayload>();
private readonly _workers: WKWorkers;
private readonly _contextIdToContext: Map<number, dom.FrameExecutionContext>;
private _sessionListeners: RegisteredListener[] = [];
Expand Down Expand Up @@ -388,9 +389,9 @@ export class WKPage implements PageDelegate {
eventsHelper.addEventListener(this._session, 'Page.fileChooserOpened', event => this._onFileChooserOpened(event)),
eventsHelper.addEventListener(this._session, 'Network.requestWillBeSent', e => this._onRequestWillBeSent(this._session, e)),
eventsHelper.addEventListener(this._session, 'Network.requestIntercepted', e => this._onRequestIntercepted(this._session, e)),
eventsHelper.addEventListener(this._session, 'Network.responseReceived', e => this._onResponseReceived(e)),
eventsHelper.addEventListener(this._session, 'Network.responseReceived', e => this._onResponseReceived(this._session, e)),
eventsHelper.addEventListener(this._session, 'Network.loadingFinished', e => this._onLoadingFinished(e)),
eventsHelper.addEventListener(this._session, 'Network.loadingFailed', e => this._onLoadingFailed(e)),
eventsHelper.addEventListener(this._session, 'Network.loadingFailed', e => this._onLoadingFailed(this._session, e)),
eventsHelper.addEventListener(this._session, 'Network.webSocketCreated', e => this._page._frameManager.onWebSocketCreated(e.requestId, e.url)),
eventsHelper.addEventListener(this._session, 'Network.webSocketWillSendHandshakeRequest', e => this._page._frameManager.onWebSocketRequest(e.requestId)),
eventsHelper.addEventListener(this._session, 'Network.webSocketHandshakeResponseReceived', e => this._page._frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)),
Expand Down Expand Up @@ -1011,6 +1012,15 @@ export class WKPage implements PageDelegate {
_onRequestWillBeSent(session: WKSession, event: Protocol.Network.requestWillBeSentPayload) {
if (event.request.url.startsWith('data:'))
return;

// We do not support intercepting redirects.
if (this._page.needsRequestInterception() && !event.redirectResponse)
this._requestIdToRequestWillBeSentEvent.set(event.requestId, event);
else
this._onRequest(session, event, false);
}

private _onRequest(session: WKSession, event: Protocol.Network.requestWillBeSentPayload, intercepted: boolean) {
let redirectedFrom: WKInterceptableRequest | null = null;
if (event.redirectResponse) {
const request = this._requestIdToRequest.get(event.requestId);
Expand All @@ -1029,13 +1039,16 @@ export class WKPage implements PageDelegate {
// TODO(einbinder) this will fail if we are an XHR document request
const isNavigationRequest = event.type === 'Document';
const documentId = isNavigationRequest ? event.loaderId : undefined;
let route = null;
// We do not support intercepting redirects.
if (this._page.needsRequestInterception() && !redirectedFrom)
route = new WKRouteImpl(session, event.requestId);
const request = new WKInterceptableRequest(session, route, frame, event, redirectedFrom, documentId);
const request = new WKInterceptableRequest(session, frame, event, redirectedFrom, documentId);
let route;
if (intercepted) {
route = new WKRouteImpl(session, request._requestId);
// There is no point in waiting for the raw headers in Network.responseReceived when intercepting.
// Use provisional headers as raw headers, so that client can call allHeaders() from the route handler.
request.request.setRawRequestHeaders(null);
}
this._requestIdToRequest.set(event.requestId, request);
this._page._frameManager.requestStarted(request.request, route || undefined);
this._page._frameManager.requestStarted(request.request, route);
}

private _handleRequestRedirect(request: WKInterceptableRequest, responsePayload: Protocol.Network.Response, timestamp: number) {
Expand All @@ -1051,34 +1064,36 @@ export class WKPage implements PageDelegate {
}

_onRequestIntercepted(session: WKSession, event: Protocol.Network.requestInterceptedPayload) {
const request = this._requestIdToRequest.get(event.requestId);
if (!request) {
session.sendMayFail('Network.interceptRequestWithError', { errorType: 'Cancellation', requestId: event.requestId });
return;
}
// There is no point in waiting for the raw headers in Network.responseReceived when intercepting.
// Use provisional headers as raw headers, so that client can call allHeaders() from the route handler.
request.request.setRawRequestHeaders(null);
if (!request._route) {
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(event.requestId);
if (!requestWillBeSentEvent) {
// Intercepted, although we do not intend to allow interception.
// Just continue.
session.sendMayFail('Network.interceptWithRequest', { requestId: request._requestId });
} else {
request._route._requestInterceptedPromise.resolve();
session.sendMayFail('Network.interceptWithRequest', { requestId: event.requestId });
return;
}
this._requestIdToRequestWillBeSentEvent.delete(event.requestId);
this._onRequest(session, requestWillBeSentEvent, true);
}

_onResponseReceived(event: Protocol.Network.responseReceivedPayload) {
_onResponseReceived(session: WKSession, event: Protocol.Network.responseReceivedPayload) {
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(event.requestId);
if (requestWillBeSentEvent) {
this._requestIdToRequestWillBeSentEvent.delete(event.requestId);
// We received a response, so the request won't be intercepted (e.g. it was handled by a
// service worker and we don't intercept service workers).
this._onRequest(session, requestWillBeSentEvent, false);
}
const request = this._requestIdToRequest.get(event.requestId);
// FileUpload sends a response without a matching request.
if (!request)
return;

this._requestIdToResponseReceivedPayloadEvent.set(request._requestId, event);
const response = request.createResponse(event.response);
this._page._frameManager.requestReceivedResponse(response);

if (response.status() === 204) {
this._onLoadingFailed({
this._onLoadingFailed(session, {
requestId: event.requestId,
errorText: 'Aborted: 204 No Content',
timestamp: event.timestamp
Expand Down Expand Up @@ -1121,7 +1136,15 @@ export class WKPage implements PageDelegate {
this._page._frameManager.reportRequestFinished(request.request, response);
}

_onLoadingFailed(event: Protocol.Network.loadingFailedPayload) {
_onLoadingFailed(session: WKSession, event: Protocol.Network.loadingFailedPayload) {
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(event.requestId);
if (requestWillBeSentEvent) {
this._requestIdToRequestWillBeSentEvent.delete(event.requestId);
// If loading failed, the request won't be intercepted (e.g. it was handled by a
// service worker and we don't intercept service workers).
this._onRequest(session, requestWillBeSentEvent, false);
}

const request = this._requestIdToRequest.get(event.requestId);
// For certain requestIds we never receive requestWillBeSent event.
// @see https://crbug.com/750469
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ export class WKProvisionalPage {
this._sessionListeners = [
eventsHelper.addEventListener(session, 'Network.requestWillBeSent', overrideFrameId(e => wkPage._onRequestWillBeSent(session, e))),
eventsHelper.addEventListener(session, 'Network.requestIntercepted', overrideFrameId(e => wkPage._onRequestIntercepted(session, e))),
eventsHelper.addEventListener(session, 'Network.responseReceived', overrideFrameId(e => wkPage._onResponseReceived(e))),
eventsHelper.addEventListener(session, 'Network.responseReceived', overrideFrameId(e => wkPage._onResponseReceived(session, e))),
eventsHelper.addEventListener(session, 'Network.loadingFinished', overrideFrameId(e => wkPage._onLoadingFinished(e))),
eventsHelper.addEventListener(session, 'Network.loadingFailed', overrideFrameId(e => wkPage._onLoadingFailed(e))),
eventsHelper.addEventListener(session, 'Network.loadingFailed', overrideFrameId(e => wkPage._onLoadingFailed(session, e))),
];

this.initializationPromise = this._wkPage._initializeSession(session, true, ({ frameTree }) => this._handleFrameTree(frameTree));
Expand Down
13 changes: 11 additions & 2 deletions tests/page/page-event-request.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,16 @@ it('should report requests and responses handled by service worker', async ({ pa
expect(await failedRequest.response()).toBe(null);
});

it('should report requests and responses handled by service worker with routing', async ({ page, server, isAndroid, isElectron, mode, platform }) => {
it('should report requests and responses handled by service worker with routing', async ({ page, server, isAndroid, isElectron, mode, browserName, platform }) => {
it.fixme(isAndroid);
it.fixme(isElectron);
it.fixme(mode.startsWith('service') && platform === 'linux', 'Times out for no clear reason');

await page.route('**/*', route => route.continue());
const interceptedUrls = [];
await page.route('**/*', route => {
interceptedUrls.push(route.request().url());
void route.continue();
});
await page.goto(server.PREFIX + '/serviceworkers/fetchdummy/sw.html');
await page.evaluate(() => window['activationPromise']);
const [swResponse, request] = await Promise.all([
Expand All @@ -96,6 +100,11 @@ it('should report requests and responses handled by service worker with routing'
expect(failedRequest.failure()).not.toBe(null);
expect(failedRequest.serviceWorker()).toBe(null);
expect(await failedRequest.response()).toBe(null);

const expectedUrls = [server.PREFIX + '/serviceworkers/fetchdummy/sw.html'];
if (browserName === 'webkit')
expectedUrls.push(server.PREFIX + '/serviceworkers/fetchdummy/sw.js');
expect(interceptedUrls).toEqual(expectedUrls);
});

it('should report navigation requests and responses handled by service worker', async ({ page, server, isAndroid, isElectron, browserName }) => {
Expand Down