mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
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:
parent
ead4989947
commit
d458e84f5b
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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>;
|
||||
|
||||
|
@ -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));
|
||||
|
@ -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,
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user