Skip to content

Commit

Permalink
feat(route): match pattern on the server side (#20410)
Browse files Browse the repository at this point in the history
This avoids client-side roundtrip for requests that are not handled by
any route.

Fixes #19607.
  • Loading branch information
dgozman authored Jan 27, 2023
1 parent ead4989 commit d458e84
Show file tree
Hide file tree
Showing 11 changed files with 100 additions and 56 deletions.
24 changes: 16 additions & 8 deletions packages/playwright-core/src/client/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -641,8 +641,7 @@ export class NetworkRouter {

async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise<void> {
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<void> {
Expand All @@ -653,8 +652,7 @@ export class NetworkRouter {

async unroute(url: URLMatch, handler?: RouteHandlerCallback): Promise<void> {
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) {
Expand All @@ -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 });
}
}

Expand Down
20 changes: 14 additions & 6 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions packages/playwright-core/src/server/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ export abstract class BrowserContext extends SdkObject {
const page = await this.newPage(internalMetadata);
await page._setServerRequestInterceptor(handler => {
handler.fulfill({ body: '<html></html>' }).catch(() => {});
return true;
});
for (const origin of this._origins) {
const originStorage: channels.OriginStorage = { origin, localStorage: [] };
Expand Down Expand Up @@ -489,6 +490,7 @@ export abstract class BrowserContext extends SdkObject {
page = page || await this.newPage(internalMetadata);
await page._setServerRequestInterceptor(handler => {
handler.fulfill({ body: '<html></html>' }).catch(() => {});
return true;
});

for (const origin of new Set([...oldOrigins, ...newOrigins.keys()])) {
Expand Down Expand Up @@ -523,6 +525,7 @@ export abstract class BrowserContext extends SdkObject {
const page = await this.newPage(internalMetadata);
await page._setServerRequestInterceptor(handler => {
handler.fulfill({ body: '<html></html>' }).catch(() => {});
return true;
});
for (const originState of state.origins) {
const frame = page.mainFrame();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel {
Expand Down Expand Up @@ -216,13 +216,18 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
await this._context.addInitScript(params.source);
}

async setNetworkInterceptionEnabled(params: channels.BrowserContextSetNetworkInterceptionEnabledParams): Promise<void> {
if (!params.enabled) {
async setNetworkInterceptionPatterns(params: channels.BrowserContextSetNetworkInterceptionPatternsParams): Promise<void> {
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;
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Page, channels.PageChannel, BrowserContextDispatcher> implements channels.PageChannel {
Expand Down Expand Up @@ -154,13 +154,18 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
await this._page.addInitScript(params.source);
}

async setNetworkInterceptionEnabled(params: channels.PageSetNetworkInterceptionEnabledParams, metadata: CallMetadata): Promise<void> {
if (!params.enabled) {
async setNetworkInterceptionPatterns(params: channels.PageSetNetworkInterceptionPatternsParams, metadata: CallMetadata): Promise<void> {
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;
});
}

Expand Down
12 changes: 3 additions & 9 deletions packages/playwright-core/src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Buffer>;

Expand Down
19 changes: 10 additions & 9 deletions packages/playwright-core/src/server/recorder/recorderApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,22 +82,23 @@ 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' }
],
body: buffer.toString('base64'),
isBase64: true
});
return;
}
await route.continue();
});
return true;
});

await this._page.exposeBinding('dispatch', false, (_, data: any) => this.emit('event', data));
Expand Down
28 changes: 18 additions & 10 deletions packages/protocol/src/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1371,7 +1371,7 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT
setExtraHTTPHeaders(params: BrowserContextSetExtraHTTPHeadersParams, metadata?: Metadata): Promise<BrowserContextSetExtraHTTPHeadersResult>;
setGeolocation(params: BrowserContextSetGeolocationParams, metadata?: Metadata): Promise<BrowserContextSetGeolocationResult>;
setHTTPCredentials(params: BrowserContextSetHTTPCredentialsParams, metadata?: Metadata): Promise<BrowserContextSetHTTPCredentialsResult>;
setNetworkInterceptionEnabled(params: BrowserContextSetNetworkInterceptionEnabledParams, metadata?: Metadata): Promise<BrowserContextSetNetworkInterceptionEnabledResult>;
setNetworkInterceptionPatterns(params: BrowserContextSetNetworkInterceptionPatternsParams, metadata?: Metadata): Promise<BrowserContextSetNetworkInterceptionPatternsResult>;
setOffline(params: BrowserContextSetOfflineParams, metadata?: Metadata): Promise<BrowserContextSetOfflineResult>;
storageState(params?: BrowserContextStorageStateParams, metadata?: Metadata): Promise<BrowserContextStorageStateResult>;
pause(params?: BrowserContextPauseParams, metadata?: Metadata): Promise<BrowserContextPauseResult>;
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -1673,7 +1677,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel {
expectScreenshot(params: PageExpectScreenshotParams, metadata?: Metadata): Promise<PageExpectScreenshotResult>;
screenshot(params: PageScreenshotParams, metadata?: Metadata): Promise<PageScreenshotResult>;
setExtraHTTPHeaders(params: PageSetExtraHTTPHeadersParams, metadata?: Metadata): Promise<PageSetExtraHTTPHeadersResult>;
setNetworkInterceptionEnabled(params: PageSetNetworkInterceptionEnabledParams, metadata?: Metadata): Promise<PageSetNetworkInterceptionEnabledResult>;
setNetworkInterceptionPatterns(params: PageSetNetworkInterceptionPatternsParams, metadata?: Metadata): Promise<PageSetNetworkInterceptionPatternsResult>;
setViewportSize(params: PageSetViewportSizeParams, metadata?: Metadata): Promise<PageSetViewportSizeResult>;
keyboardDown(params: PageKeyboardDownParams, metadata?: Metadata): Promise<PageKeyboardDownResult>;
keyboardUp(params: PageKeyboardUpParams, metadata?: Metadata): Promise<PageKeyboardUpResult>;
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 18 additions & 4 deletions packages/protocol/src/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit d458e84

Please sign in to comment.