diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index 3c372b40f5..3654855161 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -22,7 +22,7 @@ import { Worker } from './worker'; import type { Headers, RemoteAddr, SecurityDetails, WaitForEventOptions } from './types'; import fs from 'fs'; import { mime } from '../utilsBundle'; -import { assert, isString, headersObjectToArray } from '../utils'; +import { assert, isString, headersObjectToArray, isRegExp } from '../utils'; import { ManualPromise } from '../utils/manualPromise'; import { Events } from './events'; import type { Page } from './page'; @@ -641,8 +641,7 @@ export class NetworkRouter { async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise { this._routes.unshift(new RouteHandler(this._baseURL, url, handler, options.times)); - if (this._routes.length === 1) - await this._owner._channel.setNetworkInterceptionEnabled({ enabled: true }); + await this._updateInterception(); } async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback' } = {}): Promise { @@ -653,8 +652,7 @@ export class NetworkRouter { async unroute(url: URLMatch, handler?: RouteHandlerCallback): Promise { this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler)); - if (!this._routes.length) - await this._disableInterception(); + await this._updateInterception(); } async handleRoute(route: Route) { @@ -666,15 +664,25 @@ export class NetworkRouter { this._routes.splice(this._routes.indexOf(routeHandler), 1); const handled = await routeHandler.handle(route); if (!this._routes.length) - this._owner._wrapApiCall(() => this._disableInterception(), true).catch(() => {}); + this._owner._wrapApiCall(() => this._updateInterception(), true).catch(() => {}); if (handled) return true; } return false; } - private async _disableInterception() { - await this._owner._channel.setNetworkInterceptionEnabled({ enabled: false }); + private async _updateInterception() { + const patterns: channels.BrowserContextSetNetworkInterceptionPatternsParams['patterns'] = []; + let all = false; + for (const handler of this._routes) { + if (isString(handler.url)) + patterns.push({ glob: handler.url }); + else if (isRegExp(handler.url)) + patterns.push({ regexSource: handler.url.source, regexFlags: handler.url.flags }); + else + all = true; + } + await this._owner._channel.setNetworkInterceptionPatterns(all ? { patterns: [{ glob: '**/*' }] } : { patterns }); } } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 85f6b5bcc8..85f349c5fe 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -821,10 +821,14 @@ scheme.BrowserContextSetHTTPCredentialsParams = tObject({ })), }); scheme.BrowserContextSetHTTPCredentialsResult = tOptional(tObject({})); -scheme.BrowserContextSetNetworkInterceptionEnabledParams = tObject({ - enabled: tBoolean, +scheme.BrowserContextSetNetworkInterceptionPatternsParams = tObject({ + patterns: tArray(tObject({ + glob: tOptional(tString), + regexSource: tOptional(tString), + regexFlags: tOptional(tString), + })), }); -scheme.BrowserContextSetNetworkInterceptionEnabledResult = tOptional(tObject({})); +scheme.BrowserContextSetNetworkInterceptionPatternsResult = tOptional(tObject({})); scheme.BrowserContextSetOfflineParams = tObject({ offline: tBoolean, }); @@ -1035,10 +1039,14 @@ scheme.PageSetExtraHTTPHeadersParams = tObject({ headers: tArray(tType('NameValue')), }); scheme.PageSetExtraHTTPHeadersResult = tOptional(tObject({})); -scheme.PageSetNetworkInterceptionEnabledParams = tObject({ - enabled: tBoolean, +scheme.PageSetNetworkInterceptionPatternsParams = tObject({ + patterns: tArray(tObject({ + glob: tOptional(tString), + regexSource: tOptional(tString), + regexFlags: tOptional(tString), + })), }); -scheme.PageSetNetworkInterceptionEnabledResult = tOptional(tObject({})); +scheme.PageSetNetworkInterceptionPatternsResult = tOptional(tObject({})); scheme.PageSetViewportSizeParams = tObject({ viewportSize: tObject({ width: tNumber, diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index b9ee149425..c8d54471ee 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -461,6 +461,7 @@ export abstract class BrowserContext extends SdkObject { const page = await this.newPage(internalMetadata); await page._setServerRequestInterceptor(handler => { handler.fulfill({ body: '' }).catch(() => {}); + return true; }); for (const origin of this._origins) { const originStorage: channels.OriginStorage = { origin, localStorage: [] }; @@ -489,6 +490,7 @@ export abstract class BrowserContext extends SdkObject { page = page || await this.newPage(internalMetadata); await page._setServerRequestInterceptor(handler => { handler.fulfill({ body: '' }).catch(() => {}); + return true; }); for (const origin of new Set([...oldOrigins, ...newOrigins.keys()])) { @@ -523,6 +525,7 @@ export abstract class BrowserContext extends SdkObject { const page = await this.newPage(internalMetadata); await page._setServerRequestInterceptor(handler => { handler.fulfill({ body: '' }).catch(() => {}); + return true; }); for (const originState of state.origins) { const frame = page.mainFrame(); diff --git a/packages/playwright-core/src/server/chromium/crServiceWorker.ts b/packages/playwright-core/src/server/chromium/crServiceWorker.ts index 8c23215113..064afd5130 100644 --- a/packages/playwright-core/src/server/chromium/crServiceWorker.ts +++ b/packages/playwright-core/src/server/chromium/crServiceWorker.ts @@ -112,10 +112,8 @@ export class CRServiceWorker extends Worker { this._browserContext.emit(BrowserContext.Events.Request, request); if (route) { const r = new network.Route(request, route); - if (this._browserContext._requestInterceptor) { - this._browserContext._requestInterceptor(r, request); + if (this._browserContext._requestInterceptor?.(r, request)) return; - } r.continue(); } } diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 1c5168c776..2b92abbedb 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -31,7 +31,7 @@ import type { Request, Response } from '../network'; import { TracingDispatcher } from './tracingDispatcher'; import * as fs from 'fs'; import * as path from 'path'; -import { createGuid } from '../../utils'; +import { createGuid, urlMatches } from '../../utils'; import { WritableStreamDispatcher } from './writableStreamDispatcher'; export class BrowserContextDispatcher extends Dispatcher implements channels.BrowserContextChannel { @@ -216,13 +216,18 @@ export class BrowserContextDispatcher extends Dispatcher { - if (!params.enabled) { + async setNetworkInterceptionPatterns(params: channels.BrowserContextSetNetworkInterceptionPatternsParams): Promise { + if (!params.patterns.length) { await this._context.setRequestInterceptor(undefined); return; } + const urlMatchers = params.patterns.map(pattern => pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags!) : pattern.glob!); await this._context.setRequestInterceptor((route, request) => { + const matchesSome = urlMatchers.some(urlMatch => urlMatches(this._context._options.baseURL, request.url(), urlMatch)); + if (!matchesSome) + return false; this._dispatchEvent('route', { route: RouteDispatcher.from(RequestDispatcher.from(this, request), route) }); + return true; }); } diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 34da4c5e30..4d2fdfc4d0 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -35,7 +35,7 @@ import type { CallMetadata } from '../instrumentation'; import type { Artifact } from '../artifact'; import { ArtifactDispatcher } from './artifactDispatcher'; import type { Download } from '../download'; -import { createGuid } from '../../utils'; +import { createGuid, urlMatches } from '../../utils'; import type { BrowserContextDispatcher } from './browserContextDispatcher'; export class PageDispatcher extends Dispatcher implements channels.PageChannel { @@ -154,13 +154,18 @@ export class PageDispatcher extends Dispatcher { - if (!params.enabled) { + async setNetworkInterceptionPatterns(params: channels.PageSetNetworkInterceptionPatternsParams, metadata: CallMetadata): Promise { + if (!params.patterns.length) { await this._page.setClientRequestInterceptor(undefined); return; } + const urlMatchers = params.patterns.map(pattern => pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags!) : pattern.glob!); await this._page.setClientRequestInterceptor((route, request) => { + const matchesSome = urlMatchers.some(urlMatch => urlMatches(this._page._browserContext._options.baseURL, request.url(), urlMatch)); + if (!matchesSome) + return false; this._dispatchEvent('route', { route: RouteDispatcher.from(RequestDispatcher.from(this.parentScope(), request), route) }); + return true; }); } diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index fd1bd6ea1a..d5a9763a1b 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -304,18 +304,12 @@ export class FrameManager { this._page.emitOnContext(BrowserContext.Events.Request, request); if (route) { const r = new network.Route(request, route); - if (this._page._serverRequestInterceptor) { - this._page._serverRequestInterceptor(r, request); + if (this._page._serverRequestInterceptor?.(r, request)) return; - } - if (this._page._clientRequestInterceptor) { - this._page._clientRequestInterceptor(r, request); + if (this._page._clientRequestInterceptor?.(r, request)) return; - } - if (this._page._browserContext._requestInterceptor) { - this._page._browserContext._requestInterceptor(r, request); + if (this._page._browserContext._requestInterceptor?.(r, request)) return; - } r.continue(); } } diff --git a/packages/playwright-core/src/server/network.ts b/packages/playwright-core/src/server/network.ts index ed17553451..8d74f93a5b 100644 --- a/packages/playwright-core/src/server/network.ts +++ b/packages/playwright-core/src/server/network.ts @@ -333,7 +333,7 @@ export class Route extends SdkObject { } } -export type RouteHandler = (route: Route, request: Request) => void; +export type RouteHandler = (route: Route, request: Request) => boolean; type GetResponseBodyCallback = () => Promise; diff --git a/packages/playwright-core/src/server/recorder/recorderApp.ts b/packages/playwright-core/src/server/recorder/recorderApp.ts index 66e2b26340..790b1e24d0 100644 --- a/packages/playwright-core/src/server/recorder/recorderApp.ts +++ b/packages/playwright-core/src/server/recorder/recorderApp.ts @@ -82,12 +82,14 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { await installAppIcon(this._page); await syncLocalStorageWithSettings(this._page, 'recorder'); - await this._page._setServerRequestInterceptor(async route => { - if (route.request().url().startsWith('https://playwright/')) { - const uri = route.request().url().substring('https://playwright/'.length); - const file = require.resolve('../../webpack/recorder/' + uri); - const buffer = await fs.promises.readFile(file); - await route.fulfill({ + await this._page._setServerRequestInterceptor(route => { + if (!route.request().url().startsWith('https://playwright/')) + return false; + + const uri = route.request().url().substring('https://playwright/'.length); + const file = require.resolve('../../webpack/recorder/' + uri); + fs.promises.readFile(file).then(buffer => { + route.fulfill({ status: 200, headers: [ { name: 'Content-Type', value: mime.getType(path.extname(file)) || 'application/octet-stream' } @@ -95,9 +97,8 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { body: buffer.toString('base64'), isBase64: true }); - return; - } - await route.continue(); + }); + return true; }); await this._page.exposeBinding('dispatch', false, (_, data: any) => this.emit('event', data)); diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 70168edfb1..3e75809e84 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1371,7 +1371,7 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT setExtraHTTPHeaders(params: BrowserContextSetExtraHTTPHeadersParams, metadata?: Metadata): Promise; setGeolocation(params: BrowserContextSetGeolocationParams, metadata?: Metadata): Promise; setHTTPCredentials(params: BrowserContextSetHTTPCredentialsParams, metadata?: Metadata): Promise; - setNetworkInterceptionEnabled(params: BrowserContextSetNetworkInterceptionEnabledParams, metadata?: Metadata): Promise; + setNetworkInterceptionPatterns(params: BrowserContextSetNetworkInterceptionPatternsParams, metadata?: Metadata): Promise; setOffline(params: BrowserContextSetOfflineParams, metadata?: Metadata): Promise; storageState(params?: BrowserContextStorageStateParams, metadata?: Metadata): Promise; pause(params?: BrowserContextPauseParams, metadata?: Metadata): Promise; @@ -1523,13 +1523,17 @@ export type BrowserContextSetHTTPCredentialsOptions = { }, }; export type BrowserContextSetHTTPCredentialsResult = void; -export type BrowserContextSetNetworkInterceptionEnabledParams = { - enabled: boolean, +export type BrowserContextSetNetworkInterceptionPatternsParams = { + patterns: { + glob?: string, + regexSource?: string, + regexFlags?: string, + }[], }; -export type BrowserContextSetNetworkInterceptionEnabledOptions = { +export type BrowserContextSetNetworkInterceptionPatternsOptions = { }; -export type BrowserContextSetNetworkInterceptionEnabledResult = void; +export type BrowserContextSetNetworkInterceptionPatternsResult = void; export type BrowserContextSetOfflineParams = { offline: boolean, }; @@ -1673,7 +1677,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel { expectScreenshot(params: PageExpectScreenshotParams, metadata?: Metadata): Promise; screenshot(params: PageScreenshotParams, metadata?: Metadata): Promise; setExtraHTTPHeaders(params: PageSetExtraHTTPHeadersParams, metadata?: Metadata): Promise; - setNetworkInterceptionEnabled(params: PageSetNetworkInterceptionEnabledParams, metadata?: Metadata): Promise; + setNetworkInterceptionPatterns(params: PageSetNetworkInterceptionPatternsParams, metadata?: Metadata): Promise; setViewportSize(params: PageSetViewportSizeParams, metadata?: Metadata): Promise; keyboardDown(params: PageKeyboardDownParams, metadata?: Metadata): Promise; keyboardUp(params: PageKeyboardUpParams, metadata?: Metadata): Promise; @@ -1918,13 +1922,17 @@ export type PageSetExtraHTTPHeadersOptions = { }; export type PageSetExtraHTTPHeadersResult = void; -export type PageSetNetworkInterceptionEnabledParams = { - enabled: boolean, +export type PageSetNetworkInterceptionPatternsParams = { + patterns: { + glob?: string, + regexSource?: string, + regexFlags?: string, + }[], }; -export type PageSetNetworkInterceptionEnabledOptions = { +export type PageSetNetworkInterceptionPatternsOptions = { }; -export type PageSetNetworkInterceptionEnabledResult = void; +export type PageSetNetworkInterceptionPatternsResult = void; export type PageSetViewportSizeParams = { viewportSize: { width: number, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index acfcf8223f..0551af5876 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1032,9 +1032,16 @@ BrowserContext: username: string password: string - setNetworkInterceptionEnabled: + setNetworkInterceptionPatterns: parameters: - enabled: boolean + patterns: + type: array + items: + type: object + properties: + glob: string? + regexSource: string? + regexFlags: string? setOffline: parameters: @@ -1311,9 +1318,16 @@ Page: type: array items: NameValue - setNetworkInterceptionEnabled: + setNetworkInterceptionPatterns: parameters: - enabled: boolean + patterns: + type: array + items: + type: object + properties: + glob: string? + regexSource: string? + regexFlags: string? setViewportSize: parameters: