Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/src/api/class-browsercontext.md
Original file line number Diff line number Diff line change
Expand Up @@ -1307,6 +1307,12 @@ When set to `minimal`, only record information necessary for routing from HAR. T

Optional setting to control resource content management. If `attach` is specified, resources are persisted as separate files or entries in the ZIP archive. If `embed` is specified, content is stored inline the HAR file.

### option: BrowserContext.routeFromHAR.interceptAPIRequests
* since: v1.62
- `interceptAPIRequests` <[boolean]>

If set to `true`, requests made via [APIRequestContext] (such as [`property: BrowserContext.request`] or [`property: Page.request`]) are also served from the HAR file. By default these requests are sent to the network, matching the behavior prior to v1.62. Defaults to `false` for backward compatibility.


## async method: BrowserContext.routeWebSocket
* since: v1.48
Expand Down
2 changes: 2 additions & 0 deletions packages/isomorphic/protocolMetainfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export const methodMetainfo = new Map<string, MethodMetainfo>([
['BrowserContext.setGeolocation', { title: 'Set geolocation', group: 'configuration', }],
['BrowserContext.setHTTPCredentials', { title: 'Set HTTP credentials', group: 'configuration', }],
['BrowserContext.setNetworkInterceptionPatterns', { title: 'Route requests', group: 'route', }],
['BrowserContext.routeAPIRequestsFromHar', { internal: true, }],
['BrowserContext.unrouteAPIRequestsFromHar', { internal: true, }],
['BrowserContext.setWebSocketInterceptionPatterns', { title: 'Route WebSockets', group: 'route', }],
['BrowserContext.setOffline', { title: 'Set offline mode', }],
['BrowserContext.storageState', { title: 'Get storage state', group: 'configuration', }],
Expand Down
9 changes: 9 additions & 0 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9608,6 +9608,15 @@ export interface BrowserContext {
* @param options
*/
routeFromHAR(har: string, options?: {
/**
* If set to `true`, requests made via [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext)
* (such as [browserContext.request](https://playwright.dev/docs/api/class-browsercontext#browser-context-request) or
* [page.request](https://playwright.dev/docs/api/class-page#page-request)) are also served from the HAR file. By
* default these requests are sent to the network, matching the behavior prior to v1.62. Defaults to `false` for
* backward compatibility.
*/
interceptAPIRequests?: boolean;

/**
* - If set to 'abort' any request not found in the HAR file will be aborted.
* - If set to 'fallback' falls through to the next route handler in the handler chain.
Expand Down
4 changes: 3 additions & 1 deletion packages/playwright-core/src/client/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
await this._updateWebSocketInterceptionPatterns({ title: 'Route WebSockets' });
}

async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean, updateContent?: 'attach' | 'embed', updateMode?: 'minimal' | 'full' } = {}): Promise<void> {
async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean, updateContent?: 'attach' | 'embed', updateMode?: 'minimal' | 'full', interceptAPIRequests?: boolean } = {}): Promise<void> {
const localUtils = this._connection.localUtils();
if (!localUtils)
throw new Error('Route from har is not supported in thin clients');
Expand All @@ -398,6 +398,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
const harRouter = await HarRouter.create(localUtils, har, options.notFound || 'abort', { urlMatch: options.url });
this._harRouters.push(harRouter);
await harRouter.addContextRoute(this);
if (options.interceptAPIRequests)
await harRouter.addAPIRequestRoute(this);
}

private _disposeHarRouters() {
Expand Down
24 changes: 24 additions & 0 deletions packages/playwright-core/src/client/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,8 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, Channe
setGeolocation(params: BrowserContextSetGeolocationParams): Promise<BrowserContextSetGeolocationResult>;
setHTTPCredentials(params: BrowserContextSetHTTPCredentialsParams): Promise<BrowserContextSetHTTPCredentialsResult>;
setNetworkInterceptionPatterns(params: BrowserContextSetNetworkInterceptionPatternsParams): Promise<BrowserContextSetNetworkInterceptionPatternsResult>;
routeAPIRequestsFromHar(params: BrowserContextRouteAPIRequestsFromHarParams): Promise<BrowserContextRouteAPIRequestsFromHarResult>;
unrouteAPIRequestsFromHar(params: BrowserContextUnrouteAPIRequestsFromHarParams): Promise<BrowserContextUnrouteAPIRequestsFromHarResult>;
setWebSocketInterceptionPatterns(params: BrowserContextSetWebSocketInterceptionPatternsParams): Promise<BrowserContextSetWebSocketInterceptionPatternsResult>;
setOffline(params: BrowserContextSetOfflineParams): Promise<BrowserContextSetOfflineResult>;
storageState(params: BrowserContextStorageStateParams): Promise<BrowserContextStorageStateResult>;
Expand Down Expand Up @@ -1577,6 +1579,28 @@ export type BrowserContextSetNetworkInterceptionPatternsOptions = {

};
export type BrowserContextSetNetworkInterceptionPatternsResult = void;
export type BrowserContextRouteAPIRequestsFromHarParams = {
harId: string,
urlGlob?: string,
urlRegexSource?: string,
urlRegexFlags?: string,
notFound: 'abort' | 'fallback',
};
export type BrowserContextRouteAPIRequestsFromHarOptions = {
urlGlob?: string,
urlRegexSource?: string,
urlRegexFlags?: string,
};
export type BrowserContextRouteAPIRequestsFromHarResult = {
registrationId: string,
};
export type BrowserContextUnrouteAPIRequestsFromHarParams = {
registrationId: string,
};
export type BrowserContextUnrouteAPIRequestsFromHarOptions = {

};
export type BrowserContextUnrouteAPIRequestsFromHarResult = void;
export type BrowserContextSetWebSocketInterceptionPatternsParams = {
patterns: {
glob?: string,
Expand Down
17 changes: 17 additions & 0 deletions packages/playwright-core/src/client/harRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import { debugLogger } from '@utils/debugLogger';
import { isRegExp, isString } from '@isomorphic/rtti';

import type { BrowserContext } from './browserContext';
import type { LocalUtils } from './localUtils';
Expand All @@ -29,6 +30,7 @@ export class HarRouter {
private _harId: string;
private _notFoundAction: HarNotFoundAction;
private _options: { urlMatch?: URLMatch; baseURL?: string; };
private _apiRequestRegistrations: { context: BrowserContext, registrationId: string }[] = [];

static async create(localUtils: LocalUtils, file: string, notFoundAction: HarNotFoundAction, options: { urlMatch?: URLMatch }): Promise<HarRouter> {
const { harId, error } = await localUtils.harOpen({ file });
Expand Down Expand Up @@ -117,11 +119,26 @@ export class HarRouter {
await page.route(this._options.urlMatch || '**/*', route => this._handle(route));
}

async addAPIRequestRoute(context: BrowserContext) {
const urlMatch = this._options.urlMatch;
const { registrationId } = await context._channel.routeAPIRequestsFromHar({
harId: this._harId,
urlGlob: isString(urlMatch) ? urlMatch : undefined,
urlRegexSource: isRegExp(urlMatch) ? urlMatch.source : undefined,
urlRegexFlags: isRegExp(urlMatch) ? urlMatch.flags : undefined,
notFound: this._notFoundAction,
});
this._apiRequestRegistrations.push({ context, registrationId });
Comment on lines +123 to +131

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a way to use a RouteHandler like addContextRoute/addPageRoute since it seems like _handle might already be able to do a lot of what you're adding in the backend (i.e. loading and looking up in a .har)?

@stkevintan stkevintan Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RouteHandler/_handle fulfills a browser Route, which only exists for traffic that reaches the browser/CDP. APIRequestContext issues requests directly over Node http/https (_sendRequest) and never creates a Route, so _handle can't intercept it — this why #11502 added har recording/tracing ability to APIRequestContext separately.

}

async [Symbol.asyncDispose]() {
await this.dispose();
}

dispose() {
this._localUtils.harClose({ harId: this._harId }).catch(() => {});
for (const { context, registrationId } of this._apiRequestRegistrations)
context._channel.unrouteAPIRequestsFromHar({ registrationId }).catch(() => {});
this._apiRequestRegistrations = [];
}
}
14 changes: 14 additions & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,20 @@ scheme.BrowserContextSetNetworkInterceptionPatternsParams = tObject({
})),
});
scheme.BrowserContextSetNetworkInterceptionPatternsResult = tOptional(tObject({}));
scheme.BrowserContextRouteAPIRequestsFromHarParams = tObject({
harId: tString,
urlGlob: tOptional(tString),
urlRegexSource: tOptional(tString),
urlRegexFlags: tOptional(tString),
notFound: tEnum(['abort', 'fallback']),
});
scheme.BrowserContextRouteAPIRequestsFromHarResult = tObject({
registrationId: tString,
});
scheme.BrowserContextUnrouteAPIRequestsFromHarParams = tObject({
registrationId: tString,
});
scheme.BrowserContextUnrouteAPIRequestsFromHarResult = tOptional(tObject({}));
scheme.BrowserContextSetWebSocketInterceptionPatternsParams = tObject({
patterns: tArray(tObject({
glob: tOptional(tString),
Expand Down
32 changes: 32 additions & 0 deletions packages/playwright-core/src/server/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@ import type { Browser, BrowserOptions } from './browser';
import type { ConsoleMessage } from './console';
import type { Download } from './download';
import type * as frames from './frames';
import type { HarBackend } from './harBackend';
import type { PageError } from './page';
import type { Progress } from './progress';
import type { ClientCertificatesProxy } from './socksClientCertificatesInterceptor';
import type { SerializedStorage } from '@injected/storageScript';
import type * as types from './types';
import type { URLMatch } from '@isomorphic/urlMatch';
import type * as channels from './channels';

const BrowserContextEvent = {
Expand Down Expand Up @@ -120,6 +122,7 @@ export abstract class BrowserContext<EM extends EventMap = EventMap> extends Sdk
private _playwrightBindingExposed?: Promise<void>;
readonly dialogManager: DialogManager;
private _consoleApiExposed = false;
private _harForAPIRequests: HarForAPIRequestsRegistration[] = [];

constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) {
super(browser, 'browser-context');
Expand Down Expand Up @@ -749,8 +752,37 @@ export abstract class BrowserContext<EM extends EventMap = EventMap> extends Sdk
async notifyRoutesInFlightAboutRemovedHandler(handler: network.RouteHandler): Promise<void> {
await Promise.all([...this._routesInFlight].map(route => route.removeHandler(handler)));
}

routeAPIRequestsFromHar(options: { harBackend: HarBackend, urlMatch: URLMatch | undefined, notFound: 'abort' | 'fallback', baseURL: string | undefined }): { dispose: () => void } {
const registration: HarForAPIRequestsRegistration = {
harBackend: options.harBackend,
urlMatch: options.urlMatch,
notFound: options.notFound,
baseURL: options.baseURL,
};
// Give priority to the newest registration, mirroring BrowserContext.route/Page.route.
this._harForAPIRequests.unshift(registration);
return {
dispose: () => {
const index = this._harForAPIRequests.indexOf(registration);
if (index !== -1)
this._harForAPIRequests.splice(index, 1);
},
};
}

harForAPIRequests(): readonly HarForAPIRequestsRegistration[] {
return this._harForAPIRequests;
}
}

export type HarForAPIRequestsRegistration = {
harBackend: HarBackend;
urlMatch: URLMatch | undefined;
notFound: 'abort' | 'fallback';
baseURL: string | undefined;
};

export function validateBrowserContextOptions(options: types.BrowserContextOptions, browserOptions: BrowserOptions) {
if (options.noDefaultViewport && options.deviceScaleFactor !== undefined)
throw new Error(`"deviceScaleFactor" option is not supported with null "viewport"`);
Expand Down
24 changes: 24 additions & 0 deletions packages/playwright-core/src/server/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1347,6 +1347,8 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, Channe
setGeolocation(params: BrowserContextSetGeolocationParams, progress: Progress): Promise<BrowserContextSetGeolocationResult>;
setHTTPCredentials(params: BrowserContextSetHTTPCredentialsParams, progress: Progress): Promise<BrowserContextSetHTTPCredentialsResult>;
setNetworkInterceptionPatterns(params: BrowserContextSetNetworkInterceptionPatternsParams, progress: Progress): Promise<BrowserContextSetNetworkInterceptionPatternsResult>;
routeAPIRequestsFromHar(params: BrowserContextRouteAPIRequestsFromHarParams, progress: Progress): Promise<BrowserContextRouteAPIRequestsFromHarResult>;
unrouteAPIRequestsFromHar(params: BrowserContextUnrouteAPIRequestsFromHarParams, progress: Progress): Promise<BrowserContextUnrouteAPIRequestsFromHarResult>;
setWebSocketInterceptionPatterns(params: BrowserContextSetWebSocketInterceptionPatternsParams, progress: Progress): Promise<BrowserContextSetWebSocketInterceptionPatternsResult>;
setOffline(params: BrowserContextSetOfflineParams, progress: Progress): Promise<BrowserContextSetOfflineResult>;
storageState(params: BrowserContextStorageStateParams, progress: Progress): Promise<BrowserContextStorageStateResult>;
Expand Down Expand Up @@ -1580,6 +1582,28 @@ export type BrowserContextSetNetworkInterceptionPatternsOptions = {

};
export type BrowserContextSetNetworkInterceptionPatternsResult = void;
export type BrowserContextRouteAPIRequestsFromHarParams = {
harId: string,
urlGlob?: string,
urlRegexSource?: string,
urlRegexFlags?: string,
notFound: 'abort' | 'fallback',
};
export type BrowserContextRouteAPIRequestsFromHarOptions = {
urlGlob?: string,
urlRegexSource?: string,
urlRegexFlags?: string,
};
export type BrowserContextRouteAPIRequestsFromHarResult = {
registrationId: string,
};
export type BrowserContextUnrouteAPIRequestsFromHarParams = {
registrationId: string,
};
export type BrowserContextUnrouteAPIRequestsFromHarOptions = {

};
export type BrowserContextUnrouteAPIRequestsFromHarResult = void;
export type BrowserContextSetWebSocketInterceptionPatternsParams = {
patterns: {
glob?: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,13 @@ import type { Request, Response, RouteHandler } from '../network';
import type { InitScript, Page, PageError } from '../page';
import type { Disposable } from '../disposable';
import type { DispatcherScope } from './dispatcher';
import type { LocalUtilsDispatcher } from './localUtilsDispatcher';
import type * as channels from '../channels';
import type { Progress } from '../progress';
import type { URLMatch } from '@isomorphic/urlMatch';

type HarForAPIRequestsDisposable = Disposable & { registrationId: string };

export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel {
_type_BrowserContext = true;
private _context: BrowserContext;
Expand Down Expand Up @@ -335,6 +338,39 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
this._routeWebSocketInitScript = await WebSocketRouteDispatcher.install(progress, this.connection, this._context);
}

async routeAPIRequestsFromHar(params: channels.BrowserContextRouteAPIRequestsFromHarParams, progress: Progress): Promise<channels.BrowserContextRouteAPIRequestsFromHarResult> {
// Reuse the HarBackend that was already opened via localUtils.harOpen for the page-side
// route, rather than opening a second backend for the same HAR file. The backend is owned
// by LocalUtils and closed via harClose, so this registration must not dispose it.
const localUtils = [...this.connection._dispatcherByGuid.values()].find(d => d._type === 'LocalUtils') as LocalUtilsDispatcher | undefined;
const harBackend = localUtils?.harBackendForId(params.harId);
if (!harBackend)
throw new Error('Internal error: har was not opened');
const urlMatch: URLMatch | undefined =
params.urlRegexSource !== undefined && params.urlRegexFlags !== undefined ? new RegExp(params.urlRegexSource, params.urlRegexFlags) :
params.urlGlob !== undefined ? params.urlGlob : undefined;
const registrationId = createGuid();
const registration = this._context.routeAPIRequestsFromHar({
harBackend,
urlMatch,
notFound: params.notFound,
baseURL: this._context._options.baseURL,
});
this._disposables.push({
registrationId,
dispose: async () => registration.dispose(),
} as HarForAPIRequestsDisposable);
return { registrationId };
}

async unrouteAPIRequestsFromHar(params: channels.BrowserContextUnrouteAPIRequestsFromHarParams, progress: Progress): Promise<void> {
const index = this._disposables.findIndex(d => (d as HarForAPIRequestsDisposable).registrationId === params.registrationId);
if (index === -1)
return;
const [disposable] = this._disposables.splice(index, 1);
await progress.race(disposable.dispose());
}

async storageState(params: channels.BrowserContextStorageStateParams, progress: Progress): Promise<channels.BrowserContextStorageStateResult> {
return await this._context.storageState(progress, params.indexedDB, params.credentials);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ export class LocalUtilsDispatcher extends Dispatcher<SdkObject, channels.LocalUt
localUtils.harClose(this._harBackends, params);
}

harBackendForId(harId: string): HarBackend | undefined {
return this._harBackends.get(harId);
}

async harUnzip(params: channels.LocalUtilsHarUnzipParams, progress: Progress): Promise<void> {
return await localUtils.harUnzip(progress, params);
}
Expand Down
Loading
Loading