feat(route): match pattern on the server side (#20410)

This avoids client-side roundtrip for requests that are not handled by
any route.

Fixes #19607.
This commit is contained in:
Dmitry Gozman 2023-01-27 10:43:19 -08:00 committed by GitHub
parent ead4989947
commit d458e84f5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 100 additions and 56 deletions

View File

@ -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<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> {
@ -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) {
@ -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 });
}
}

View File

@ -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,

View File

@ -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: [] };
@ -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()])) {
@ -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();

View File

@ -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();
}
}

View File

@ -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 {
@ -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;
});
}

View File

@ -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 {
@ -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;
});
}

View File

@ -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();
}
}

View File

@ -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>;

View File

@ -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));

View File

@ -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>;
@ -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<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>;
@ -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,

View File

@ -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: