fix(webkit): delay request event until requestIntercepted is received (#28484)
Previously we were wrongly firing `route` event for the request which are not in fact intercepted (e.g. requests from service worker). Related https://github.com/microsoft/playwright/pull/28414 Reference https://github.com/microsoft/playwright/issues/23781
This commit is contained in:
parent
9a95d9a60c
commit
ab68d7b9de
|
|
@ -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',
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 });
|
||||
|
|
@ -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';
|
||||
|
|
@ -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', {
|
||||
|
|
|
|||
|
|
@ -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[] = [];
|
||||
|
|
@ -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)),
|
||||
|
|
@ -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);
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue