diff --git a/packages/playwright-core/src/server/webkit/wkInterceptableRequest.ts b/packages/playwright-core/src/server/webkit/wkInterceptableRequest.ts index 5ebe92ea53..55aabf0575 100644 --- a/packages/playwright-core/src/server/webkit/wkInterceptableRequest.ts +++ b/packages/playwright-core/src/server/webkit/wkInterceptableRequest.ts @@ -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(); 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', { diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 26104c7dea..6c219bf437 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -60,6 +60,7 @@ export class WKPage implements PageDelegate { private readonly _pageProxySession: WKSession; readonly _opener: WKPage | null; private readonly _requestIdToRequest = new Map(); + private readonly _requestIdToRequestWillBeSentEvent = new Map(); private readonly _workers: WKWorkers; private readonly _contextIdToContext: Map; 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 diff --git a/packages/playwright-core/src/server/webkit/wkProvisionalPage.ts b/packages/playwright-core/src/server/webkit/wkProvisionalPage.ts index aa6c06e57d..0b19ca7a17 100644 --- a/packages/playwright-core/src/server/webkit/wkProvisionalPage.ts +++ b/packages/playwright-core/src/server/webkit/wkProvisionalPage.ts @@ -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)); diff --git a/tests/page/page-event-request.spec.ts b/tests/page/page-event-request.spec.ts index 2b947c4639..07c1fcc3ce 100644 --- a/tests/page/page-event-request.spec.ts +++ b/tests/page/page-event-request.spec.ts @@ -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 }) => {