chore: consolidate route handling logic in NetworkRouter (#20353)
References #19607.
This commit is contained in:
parent
d1fb3a2384
commit
cab52cded9
|
|
@ -40,11 +40,10 @@ import { Artifact } from './artifact';
|
||||||
import { APIRequestContext } from './fetch';
|
import { APIRequestContext } from './fetch';
|
||||||
import { createInstrumentation } from './clientInstrumentation';
|
import { createInstrumentation } from './clientInstrumentation';
|
||||||
import { rewriteErrorMessage } from '../utils/stackTrace';
|
import { rewriteErrorMessage } from '../utils/stackTrace';
|
||||||
import { HarRouter } from './harRouter';
|
|
||||||
|
|
||||||
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
|
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
|
||||||
_pages = new Set<Page>();
|
_pages = new Set<Page>();
|
||||||
private _routes: network.RouteHandler[] = [];
|
private _router: network.NetworkRouter;
|
||||||
readonly _browser: Browser | null = null;
|
readonly _browser: Browser | null = null;
|
||||||
private _browserType: BrowserType | undefined;
|
private _browserType: BrowserType | undefined;
|
||||||
readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>();
|
readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>();
|
||||||
|
|
@ -73,6 +72,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||||
if (parent instanceof Browser)
|
if (parent instanceof Browser)
|
||||||
this._browser = parent;
|
this._browser = parent;
|
||||||
this._isChromium = this._browser?._name === 'chromium';
|
this._isChromium = this._browser?._name === 'chromium';
|
||||||
|
this._router = new network.NetworkRouter(this, this._options.baseURL);
|
||||||
this.tracing = Tracing.from(initializer.tracing);
|
this.tracing = Tracing.from(initializer.tracing);
|
||||||
this.request = APIRequestContext.from(initializer.requestContext);
|
this.request = APIRequestContext.from(initializer.requestContext);
|
||||||
|
|
||||||
|
|
@ -153,18 +153,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onRoute(route: network.Route) {
|
async _onRoute(route: network.Route) {
|
||||||
const routeHandlers = this._routes.slice();
|
if (await this._router.handleRoute(route))
|
||||||
for (const routeHandler of routeHandlers) {
|
return;
|
||||||
if (!routeHandler.matches(route.request().url()))
|
|
||||||
continue;
|
|
||||||
if (routeHandler.willExpire())
|
|
||||||
this._routes.splice(this._routes.indexOf(routeHandler), 1);
|
|
||||||
const handled = await routeHandler.handle(route);
|
|
||||||
if (!this._routes.length)
|
|
||||||
this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
|
|
||||||
if (handled)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await route._innerContinue(true);
|
await route._innerContinue(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,9 +251,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||||
}
|
}
|
||||||
|
|
||||||
async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise<void> {
|
async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise<void> {
|
||||||
this._routes.unshift(new network.RouteHandler(this._options.baseURL, url, handler, options.times));
|
await this._router.route(url, handler, options);
|
||||||
if (this._routes.length === 1)
|
|
||||||
await this._channel.setNetworkInterceptionEnabled({ enabled: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _recordIntoHAR(har: string, page: Page | null, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean } = {}): Promise<void> {
|
async _recordIntoHAR(har: string, page: Page | null, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean } = {}): Promise<void> {
|
||||||
|
|
@ -284,18 +272,11 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||||
await this._recordIntoHAR(har, null, options);
|
await this._recordIntoHAR(har, null, options);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const harRouter = await HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', { urlMatch: options.url });
|
await this._router.routeFromHAR(har, options);
|
||||||
harRouter.addContextRoute(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async unroute(url: URLMatch, handler?: network.RouteHandlerCallback): Promise<void> {
|
async unroute(url: URLMatch, handler?: network.RouteHandlerCallback): Promise<void> {
|
||||||
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler));
|
await this._router.unroute(url, handler);
|
||||||
if (!this._routes.length)
|
|
||||||
await this._disableInterception();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _disableInterception() {
|
|
||||||
await this._channel.setNetworkInterceptionEnabled({ enabled: false });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> {
|
async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> {
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { debugLogger } from '../common/debugLogger';
|
import { debugLogger } from '../common/debugLogger';
|
||||||
import type { BrowserContext } from './browserContext';
|
|
||||||
import { Events } from './events';
|
|
||||||
import type { LocalUtils } from './localUtils';
|
import type { LocalUtils } from './localUtils';
|
||||||
import type { Route } from './network';
|
import type { Route } from './network';
|
||||||
import type { URLMatch } from './types';
|
|
||||||
import type { Page } from './page';
|
|
||||||
|
|
||||||
type HarNotFoundAction = 'abort' | 'fallback';
|
type HarNotFoundAction = 'abort' | 'fallback';
|
||||||
|
|
||||||
|
|
@ -28,23 +24,21 @@ export class HarRouter {
|
||||||
private _localUtils: LocalUtils;
|
private _localUtils: LocalUtils;
|
||||||
private _harId: string;
|
private _harId: string;
|
||||||
private _notFoundAction: HarNotFoundAction;
|
private _notFoundAction: HarNotFoundAction;
|
||||||
private _options: { urlMatch?: URLMatch; baseURL?: string; };
|
|
||||||
|
|
||||||
static async create(localUtils: LocalUtils, file: string, notFoundAction: HarNotFoundAction, options: { urlMatch?: URLMatch }): Promise<HarRouter> {
|
static async create(localUtils: LocalUtils, file: string, notFoundAction: HarNotFoundAction): Promise<HarRouter> {
|
||||||
const { harId, error } = await localUtils._channel.harOpen({ file });
|
const { harId, error } = await localUtils._channel.harOpen({ file });
|
||||||
if (error)
|
if (error)
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
return new HarRouter(localUtils, harId!, notFoundAction, options);
|
return new HarRouter(localUtils, harId!, notFoundAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(localUtils: LocalUtils, harId: string, notFoundAction: HarNotFoundAction, options: { urlMatch?: URLMatch }) {
|
private constructor(localUtils: LocalUtils, harId: string, notFoundAction: HarNotFoundAction) {
|
||||||
this._localUtils = localUtils;
|
this._localUtils = localUtils;
|
||||||
this._harId = harId;
|
this._harId = harId;
|
||||||
this._options = options;
|
|
||||||
this._notFoundAction = notFoundAction;
|
this._notFoundAction = notFoundAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _handle(route: Route) {
|
async handleRoute(route: Route) {
|
||||||
const request = route.request();
|
const request = route.request();
|
||||||
|
|
||||||
const response = await this._localUtils._channel.harLookup({
|
const response = await this._localUtils._channel.harLookup({
|
||||||
|
|
@ -83,16 +77,6 @@ export class HarRouter {
|
||||||
await route.fallback();
|
await route.fallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
async addContextRoute(context: BrowserContext) {
|
|
||||||
await context.route(this._options.urlMatch || '**/*', route => this._handle(route));
|
|
||||||
context.once(Events.BrowserContext.Close, () => this.dispose());
|
|
||||||
}
|
|
||||||
|
|
||||||
async addPageRoute(page: Page) {
|
|
||||||
await page.route(this._options.urlMatch || '**/*', route => this._handle(route));
|
|
||||||
page.once(Events.Page.Close, () => this.dispose());
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
this._localUtils._channel.harClose({ harId: this._harId }).catch(() => {});
|
this._localUtils._channel.harClose({ harId: this._harId }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ import { urlMatches } from '../utils/network';
|
||||||
import { MultiMap } from '../utils/multimap';
|
import { MultiMap } from '../utils/multimap';
|
||||||
import { APIResponse } from './fetch';
|
import { APIResponse } from './fetch';
|
||||||
import type { Serializable } from '../../types/structs';
|
import type { Serializable } from '../../types/structs';
|
||||||
|
import type { BrowserContext } from './browserContext';
|
||||||
|
import { HarRouter } from './harRouter';
|
||||||
|
|
||||||
export type NetworkCookie = {
|
export type NetworkCookie = {
|
||||||
name: string,
|
name: string,
|
||||||
|
|
@ -617,7 +619,56 @@ export function validateHeaders(headers: Headers) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RouteHandler {
|
export class NetworkRouter {
|
||||||
|
private _owner: Page | BrowserContext;
|
||||||
|
private _baseURL: string | undefined;
|
||||||
|
private _routes: RouteHandler[] = [];
|
||||||
|
|
||||||
|
constructor(owner: Page | BrowserContext, baseURL: string | undefined) {
|
||||||
|
this._owner = owner;
|
||||||
|
this._baseURL = baseURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback' } = {}): Promise<void> {
|
||||||
|
const harRouter = await HarRouter.create(this._owner._connection.localUtils(), har, options.notFound || 'abort');
|
||||||
|
await this.route(options.url || '**/*', route => harRouter.handleRoute(route));
|
||||||
|
this._owner.once('close', () => harRouter.dispose());
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleRoute(route: Route) {
|
||||||
|
const routeHandlers = this._routes.slice();
|
||||||
|
for (const routeHandler of routeHandlers) {
|
||||||
|
if (!routeHandler.matches(route.request().url()))
|
||||||
|
continue;
|
||||||
|
if (routeHandler.willExpire())
|
||||||
|
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(() => {});
|
||||||
|
if (handled)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _disableInterception() {
|
||||||
|
await this._owner._channel.setNetworkInterceptionEnabled({ enabled: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RouteHandler {
|
||||||
private handledCount = 0;
|
private handledCount = 0;
|
||||||
private readonly _baseURL: string | undefined;
|
private readonly _baseURL: string | undefined;
|
||||||
private readonly _times: number;
|
private readonly _times: number;
|
||||||
|
|
|
||||||
|
|
@ -42,13 +42,12 @@ import type { APIRequestContext } from './fetch';
|
||||||
import { FileChooser } from './fileChooser';
|
import { FileChooser } from './fileChooser';
|
||||||
import type { WaitForNavigationOptions } from './frame';
|
import type { WaitForNavigationOptions } from './frame';
|
||||||
import { Frame, verifyLoadState } from './frame';
|
import { Frame, verifyLoadState } from './frame';
|
||||||
import { HarRouter } from './harRouter';
|
|
||||||
import { Keyboard, Mouse, Touchscreen } from './input';
|
import { Keyboard, Mouse, Touchscreen } from './input';
|
||||||
import { assertMaxArguments, JSHandle, parseResult, serializeArgument } from './jsHandle';
|
import { assertMaxArguments, JSHandle, parseResult, serializeArgument } from './jsHandle';
|
||||||
import type { FrameLocator, Locator, LocatorOptions } from './locator';
|
import type { FrameLocator, Locator, LocatorOptions } from './locator';
|
||||||
import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils';
|
import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils';
|
||||||
import type { RouteHandlerCallback } from './network';
|
import { NetworkRouter, type RouteHandlerCallback } from './network';
|
||||||
import { Response, Route, RouteHandler, validateHeaders, WebSocket } from './network';
|
import { Response, Route, validateHeaders, WebSocket } from './network';
|
||||||
import type { Request } from './network';
|
import type { Request } from './network';
|
||||||
import type { FilePayload, Headers, LifecycleEvent, SelectOption, SelectOptionOptions, Size, URLMatch, WaitForEventOptions, WaitForFunctionOptions } from './types';
|
import type { FilePayload, Headers, LifecycleEvent, SelectOption, SelectOptionOptions, Size, URLMatch, WaitForEventOptions, WaitForFunctionOptions } from './types';
|
||||||
import { Video } from './video';
|
import { Video } from './video';
|
||||||
|
|
@ -85,7 +84,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
||||||
private _closed = false;
|
private _closed = false;
|
||||||
_closedOrCrashedPromise: Promise<void>;
|
_closedOrCrashedPromise: Promise<void>;
|
||||||
private _viewportSize: Size | null;
|
private _viewportSize: Size | null;
|
||||||
private _routes: RouteHandler[] = [];
|
private _router: NetworkRouter;
|
||||||
|
|
||||||
readonly accessibility: Accessibility;
|
readonly accessibility: Accessibility;
|
||||||
readonly coverage: Coverage;
|
readonly coverage: Coverage;
|
||||||
|
|
@ -111,6 +110,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
||||||
super(parent, type, guid, initializer);
|
super(parent, type, guid, initializer);
|
||||||
this._browserContext = parent as unknown as BrowserContext;
|
this._browserContext = parent as unknown as BrowserContext;
|
||||||
this._timeoutSettings = new TimeoutSettings(this._browserContext._timeoutSettings);
|
this._timeoutSettings = new TimeoutSettings(this._browserContext._timeoutSettings);
|
||||||
|
this._router = new NetworkRouter(this, this._browserContext._options.baseURL);
|
||||||
|
|
||||||
this.accessibility = new Accessibility(this._channel);
|
this.accessibility = new Accessibility(this._channel);
|
||||||
this.keyboard = new Keyboard(this);
|
this.keyboard = new Keyboard(this);
|
||||||
|
|
@ -187,18 +187,8 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _onRoute(route: Route) {
|
private async _onRoute(route: Route) {
|
||||||
const routeHandlers = this._routes.slice();
|
if (await this._router.handleRoute(route))
|
||||||
for (const routeHandler of routeHandlers) {
|
return;
|
||||||
if (!routeHandler.matches(route.request().url()))
|
|
||||||
continue;
|
|
||||||
if (routeHandler.willExpire())
|
|
||||||
this._routes.splice(this._routes.indexOf(routeHandler), 1);
|
|
||||||
const handled = await routeHandler.handle(route);
|
|
||||||
if (!this._routes.length)
|
|
||||||
this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
|
|
||||||
if (handled)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this._browserContext._onRoute(route);
|
await this._browserContext._onRoute(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -459,9 +449,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
||||||
}
|
}
|
||||||
|
|
||||||
async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise<void> {
|
async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise<void> {
|
||||||
this._routes.unshift(new RouteHandler(this._browserContext._options.baseURL, url, handler, options.times));
|
await this._router.route(url, handler, options);
|
||||||
if (this._routes.length === 1)
|
|
||||||
await this._channel.setNetworkInterceptionEnabled({ enabled: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean } = {}): Promise<void> {
|
async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean } = {}): Promise<void> {
|
||||||
|
|
@ -469,18 +457,11 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
||||||
await this._browserContext._recordIntoHAR(har, this, options);
|
await this._browserContext._recordIntoHAR(har, this, options);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const harRouter = await HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', { urlMatch: options.url });
|
await this._router.routeFromHAR(har, options);
|
||||||
harRouter.addPageRoute(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async unroute(url: URLMatch, handler?: RouteHandlerCallback): Promise<void> {
|
async unroute(url: URLMatch, handler?: RouteHandlerCallback): Promise<void> {
|
||||||
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler));
|
await this._router.unroute(url, handler);
|
||||||
if (!this._routes.length)
|
|
||||||
await this._disableInterception();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _disableInterception() {
|
|
||||||
await this._channel.setNetworkInterceptionEnabled({ enabled: false });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async screenshot(options: Omit<channels.PageScreenshotOptions, 'mask'> & { path?: string, mask?: Locator[] } = {}): Promise<Buffer> {
|
async screenshot(options: Omit<channels.PageScreenshotOptions, 'mask'> & { path?: string, mask?: Locator[] } = {}): Promise<Buffer> {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue