feat: response interception (#7122)
This commit is contained in:
parent
6f9d2ba53d
commit
c0cd2d4579
|
|
@ -220,6 +220,31 @@ Optional response body as raw bytes.
|
||||||
File path to respond with. The content type will be inferred from file extension. If `path` is a relative path, then it
|
File path to respond with. The content type will be inferred from file extension. If `path` is a relative path, then it
|
||||||
is resolved relative to the current working directory.
|
is resolved relative to the current working directory.
|
||||||
|
|
||||||
|
## async method: Route.intercept
|
||||||
|
- returns: <[Response]>
|
||||||
|
|
||||||
|
Continues route's request with optional overrides and intercepts response.
|
||||||
|
|
||||||
|
### option: Route.intercept.url
|
||||||
|
- `url` <[string]>
|
||||||
|
|
||||||
|
If set changes the request URL. New URL must have same protocol as original one.
|
||||||
|
|
||||||
|
### option: Route.intercept.method
|
||||||
|
- `method` <[string]>
|
||||||
|
|
||||||
|
If set changes the request method (e.g. GET or POST)
|
||||||
|
|
||||||
|
### option: Route.intercept.postData
|
||||||
|
- `postData` <[string]|[Buffer]>
|
||||||
|
|
||||||
|
If set changes the post data of request
|
||||||
|
|
||||||
|
### option: Route.intercept.headers
|
||||||
|
- `headers` <[Object]<[string], [string]>>
|
||||||
|
|
||||||
|
If set changes the request HTTP headers. Header values will be converted to a string.
|
||||||
|
|
||||||
## method: Route.request
|
## method: Route.request
|
||||||
- returns: <[Request]>
|
- returns: <[Request]>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import { Events } from './events';
|
||||||
import { Page } from './page';
|
import { Page } from './page';
|
||||||
import { Waiter } from './waiter';
|
import { Waiter } from './waiter';
|
||||||
import * as api from '../../types/types';
|
import * as api from '../../types/types';
|
||||||
|
import { Serializable } from '../../types/structs';
|
||||||
|
|
||||||
export type NetworkCookie = {
|
export type NetworkCookie = {
|
||||||
name: string,
|
name: string,
|
||||||
|
|
@ -170,6 +171,80 @@ export class Request extends ChannelOwner<channels.RequestChannel, channels.Requ
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class InterceptedResponse implements api.Response {
|
||||||
|
private readonly _route: Route;
|
||||||
|
private readonly _initializer: channels.InterceptedResponse;
|
||||||
|
private readonly _request: Request;
|
||||||
|
private readonly _headers: Headers;
|
||||||
|
|
||||||
|
constructor(route: Route, initializer: channels.InterceptedResponse) {
|
||||||
|
this._route = route;
|
||||||
|
this._initializer = initializer;
|
||||||
|
this._headers = headersArrayToObject(initializer.headers, true /* lowerCase */);
|
||||||
|
this._request = Request.from(initializer.request);
|
||||||
|
}
|
||||||
|
|
||||||
|
async securityDetails(): Promise<{ issuer?: string | undefined; protocol?: string | undefined; subjectName?: string | undefined; validFrom?: number | undefined; validTo?: number | undefined; } | null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async serverAddr(): Promise<{ ipAddress: string; port: number; } | null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async finished(): Promise<Error | null> {
|
||||||
|
const response = await this._request.response();
|
||||||
|
if (!response)
|
||||||
|
return null;
|
||||||
|
return await response.finished();
|
||||||
|
}
|
||||||
|
|
||||||
|
frame(): api.Frame {
|
||||||
|
return this._request.frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
ok(): boolean {
|
||||||
|
return this._initializer.status === 0 || (this._initializer.status >= 200 && this._initializer.status <= 299);
|
||||||
|
}
|
||||||
|
|
||||||
|
url(): string {
|
||||||
|
return this._request.url();
|
||||||
|
}
|
||||||
|
|
||||||
|
status(): number {
|
||||||
|
return this._initializer.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusText(): string {
|
||||||
|
return this._initializer.statusText;
|
||||||
|
}
|
||||||
|
|
||||||
|
headers(): Headers {
|
||||||
|
return { ...this._headers };
|
||||||
|
}
|
||||||
|
|
||||||
|
async body(): Promise<Buffer> {
|
||||||
|
return this._route._responseBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
async text(): Promise<string> {
|
||||||
|
const content = await this.body();
|
||||||
|
return content.toString('utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
async json(): Promise<object> {
|
||||||
|
const content = await this.text();
|
||||||
|
return JSON.parse(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
request(): Request {
|
||||||
|
return this._request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type InterceptResponse = true;
|
||||||
|
type NotInterceptResponse = false;
|
||||||
|
|
||||||
export class Route extends ChannelOwner<channels.RouteChannel, channels.RouteInitializer> implements api.Route {
|
export class Route extends ChannelOwner<channels.RouteChannel, channels.RouteInitializer> implements api.Route {
|
||||||
static from(route: channels.RouteChannel): Route {
|
static from(route: channels.RouteChannel): Route {
|
||||||
return (route as any)._object;
|
return (route as any)._object;
|
||||||
|
|
@ -228,15 +303,35 @@ export class Route extends ChannelOwner<channels.RouteChannel, channels.RouteIni
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async intercept(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer, interceptResponse?: boolean } = {}) : Promise<api.Response> {
|
||||||
|
return await this._continue('route.intercept', options, true);
|
||||||
|
}
|
||||||
|
|
||||||
async continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer } = {}) {
|
async continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer } = {}) {
|
||||||
return this._wrapApiCall('route.continue', async (channel: channels.RouteChannel) => {
|
await this._continue('route.continue', options, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _continue(apiName: string, options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: NotInterceptResponse) : Promise<null>;
|
||||||
|
async _continue(apiName: string, options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: InterceptResponse) : Promise<api.Response>;
|
||||||
|
async _continue(apiName: string, options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: boolean) : Promise<null|api.Response> {
|
||||||
|
return await this._wrapApiCall(apiName, async (channel: channels.RouteChannel) => {
|
||||||
const postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
|
const postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
|
||||||
await channel.continue({
|
const result = await channel.continue({
|
||||||
url: options.url,
|
url: options.url,
|
||||||
method: options.method,
|
method: options.method,
|
||||||
headers: options.headers ? headersObjectToArray(options.headers) : undefined,
|
headers: options.headers ? headersObjectToArray(options.headers) : undefined,
|
||||||
postData: postDataBuffer ? postDataBuffer.toString('base64') : undefined,
|
postData: postDataBuffer ? postDataBuffer.toString('base64') : undefined,
|
||||||
|
interceptResponse,
|
||||||
});
|
});
|
||||||
|
if (result.response)
|
||||||
|
return new InterceptedResponse(this, result.response);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _responseBody(): Promise<Buffer> {
|
||||||
|
return this._wrapApiCall('response.body', async (channel: channels.RouteChannel) => {
|
||||||
|
return Buffer.from((await channel.responseBody()).binary, 'base64');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Request, Response, Route, WebSocket } from '../server/network';
|
import { InterceptedResponse, Request, Response, Route, WebSocket } from '../server/network';
|
||||||
import * as channels from '../protocol/channels';
|
import * as channels from '../protocol/channels';
|
||||||
import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher';
|
import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher';
|
||||||
import { FrameDispatcher } from './frameDispatcher';
|
import { FrameDispatcher } from './frameDispatcher';
|
||||||
|
|
@ -97,10 +97,6 @@ export class RouteDispatcher extends Dispatcher<Route, channels.RouteInitializer
|
||||||
return result || new RouteDispatcher(scope, route);
|
return result || new RouteDispatcher(scope, route);
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromNullable(scope: DispatcherScope, route: Route | null): RouteDispatcher | undefined {
|
|
||||||
return route ? RouteDispatcher.from(scope, route) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor(scope: DispatcherScope, route: Route) {
|
private constructor(scope: DispatcherScope, route: Route) {
|
||||||
super(scope, route, 'Route', {
|
super(scope, route, 'Route', {
|
||||||
// Context route can point to a non-reported request.
|
// Context route can point to a non-reported request.
|
||||||
|
|
@ -108,13 +104,28 @@ export class RouteDispatcher extends Dispatcher<Route, channels.RouteInitializer
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async continue(params: channels.RouteContinueParams): Promise<void> {
|
async responseBody(params?: channels.RouteResponseBodyParams, metadata?: channels.Metadata): Promise<channels.RouteResponseBodyResult> {
|
||||||
await this._object.continue({
|
return { binary: (await this._object.responseBody()).toString('base64') };
|
||||||
|
}
|
||||||
|
|
||||||
|
async continue(params: channels.RouteContinueParams, metadata?: channels.Metadata): Promise<channels.RouteContinueResult> {
|
||||||
|
const response = await this._object.continue({
|
||||||
url: params.url,
|
url: params.url,
|
||||||
method: params.method,
|
method: params.method,
|
||||||
headers: params.headers,
|
headers: params.headers,
|
||||||
postData: params.postData ? Buffer.from(params.postData, 'base64') : undefined,
|
postData: params.postData ? Buffer.from(params.postData, 'base64') : undefined,
|
||||||
|
interceptResponse: params.interceptResponse
|
||||||
});
|
});
|
||||||
|
const result: channels.RouteContinueResult = {};
|
||||||
|
if (response) {
|
||||||
|
result.response = {
|
||||||
|
request: RequestDispatcher.from(this._scope, response.request()),
|
||||||
|
status: response.status(),
|
||||||
|
statusText: response.statusText(),
|
||||||
|
headers: response.headers(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fulfill(params: channels.RouteFulfillParams): Promise<void> {
|
async fulfill(params: channels.RouteFulfillParams): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,16 @@ export type SerializedError = {
|
||||||
value?: SerializedValue,
|
value?: SerializedValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type InterceptedResponse = {
|
||||||
|
request: RequestChannel,
|
||||||
|
status: number,
|
||||||
|
statusText: string,
|
||||||
|
headers: {
|
||||||
|
name: string,
|
||||||
|
value: string,
|
||||||
|
}[],
|
||||||
|
};
|
||||||
|
|
||||||
// ----------- Playwright -----------
|
// ----------- Playwright -----------
|
||||||
export type PlaywrightInitializer = {
|
export type PlaywrightInitializer = {
|
||||||
chromium: BrowserTypeChannel,
|
chromium: BrowserTypeChannel,
|
||||||
|
|
@ -2307,6 +2317,7 @@ export interface RouteChannel extends Channel {
|
||||||
abort(params: RouteAbortParams, metadata?: Metadata): Promise<RouteAbortResult>;
|
abort(params: RouteAbortParams, metadata?: Metadata): Promise<RouteAbortResult>;
|
||||||
continue(params: RouteContinueParams, metadata?: Metadata): Promise<RouteContinueResult>;
|
continue(params: RouteContinueParams, metadata?: Metadata): Promise<RouteContinueResult>;
|
||||||
fulfill(params: RouteFulfillParams, metadata?: Metadata): Promise<RouteFulfillResult>;
|
fulfill(params: RouteFulfillParams, metadata?: Metadata): Promise<RouteFulfillResult>;
|
||||||
|
responseBody(params?: RouteResponseBodyParams, metadata?: Metadata): Promise<RouteResponseBodyResult>;
|
||||||
}
|
}
|
||||||
export type RouteAbortParams = {
|
export type RouteAbortParams = {
|
||||||
errorCode?: string,
|
errorCode?: string,
|
||||||
|
|
@ -2320,14 +2331,18 @@ export type RouteContinueParams = {
|
||||||
method?: string,
|
method?: string,
|
||||||
headers?: NameValue[],
|
headers?: NameValue[],
|
||||||
postData?: Binary,
|
postData?: Binary,
|
||||||
|
interceptResponse?: boolean,
|
||||||
};
|
};
|
||||||
export type RouteContinueOptions = {
|
export type RouteContinueOptions = {
|
||||||
url?: string,
|
url?: string,
|
||||||
method?: string,
|
method?: string,
|
||||||
headers?: NameValue[],
|
headers?: NameValue[],
|
||||||
postData?: Binary,
|
postData?: Binary,
|
||||||
|
interceptResponse?: boolean,
|
||||||
|
};
|
||||||
|
export type RouteContinueResult = {
|
||||||
|
response?: InterceptedResponse,
|
||||||
};
|
};
|
||||||
export type RouteContinueResult = void;
|
|
||||||
export type RouteFulfillParams = {
|
export type RouteFulfillParams = {
|
||||||
status?: number,
|
status?: number,
|
||||||
headers?: NameValue[],
|
headers?: NameValue[],
|
||||||
|
|
@ -2341,6 +2356,11 @@ export type RouteFulfillOptions = {
|
||||||
isBase64?: boolean,
|
isBase64?: boolean,
|
||||||
};
|
};
|
||||||
export type RouteFulfillResult = void;
|
export type RouteFulfillResult = void;
|
||||||
|
export type RouteResponseBodyParams = {};
|
||||||
|
export type RouteResponseBodyOptions = {};
|
||||||
|
export type RouteResponseBodyResult = {
|
||||||
|
binary: Binary,
|
||||||
|
};
|
||||||
|
|
||||||
export type ResourceTiming = {
|
export type ResourceTiming = {
|
||||||
startTime: number,
|
startTime: number,
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,21 @@ SerializedError:
|
||||||
value: SerializedValue?
|
value: SerializedValue?
|
||||||
|
|
||||||
|
|
||||||
|
InterceptedResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
request: Request
|
||||||
|
status: number
|
||||||
|
statusText: string
|
||||||
|
headers:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
|
||||||
|
|
||||||
LaunchOptions:
|
LaunchOptions:
|
||||||
type: mixin
|
type: mixin
|
||||||
properties:
|
properties:
|
||||||
|
|
@ -1868,6 +1883,9 @@ Route:
|
||||||
type: array?
|
type: array?
|
||||||
items: NameValue
|
items: NameValue
|
||||||
postData: binary?
|
postData: binary?
|
||||||
|
interceptResponse: boolean?
|
||||||
|
returns:
|
||||||
|
response: InterceptedResponse?
|
||||||
|
|
||||||
fulfill:
|
fulfill:
|
||||||
parameters:
|
parameters:
|
||||||
|
|
@ -1879,6 +1897,10 @@ Route:
|
||||||
body: string?
|
body: string?
|
||||||
isBase64: boolean?
|
isBase64: boolean?
|
||||||
|
|
||||||
|
responseBody:
|
||||||
|
returns:
|
||||||
|
binary: binary
|
||||||
|
|
||||||
|
|
||||||
ResourceTiming:
|
ResourceTiming:
|
||||||
type: object
|
type: object
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,15 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
})),
|
})),
|
||||||
value: tOptional(tType('SerializedValue')),
|
value: tOptional(tType('SerializedValue')),
|
||||||
});
|
});
|
||||||
|
scheme.InterceptedResponse = tObject({
|
||||||
|
request: tChannel('Request'),
|
||||||
|
status: tNumber,
|
||||||
|
statusText: tString,
|
||||||
|
headers: tArray(tObject({
|
||||||
|
name: tString,
|
||||||
|
value: tString,
|
||||||
|
})),
|
||||||
|
});
|
||||||
scheme.PlaywrightSetForwardedPortsParams = tObject({
|
scheme.PlaywrightSetForwardedPortsParams = tObject({
|
||||||
ports: tArray(tNumber),
|
ports: tArray(tNumber),
|
||||||
});
|
});
|
||||||
|
|
@ -924,6 +933,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
method: tOptional(tString),
|
method: tOptional(tString),
|
||||||
headers: tOptional(tArray(tType('NameValue'))),
|
headers: tOptional(tArray(tType('NameValue'))),
|
||||||
postData: tOptional(tBinary),
|
postData: tOptional(tBinary),
|
||||||
|
interceptResponse: tOptional(tBoolean),
|
||||||
});
|
});
|
||||||
scheme.RouteFulfillParams = tObject({
|
scheme.RouteFulfillParams = tObject({
|
||||||
status: tOptional(tNumber),
|
status: tOptional(tNumber),
|
||||||
|
|
@ -931,6 +941,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
body: tOptional(tString),
|
body: tOptional(tString),
|
||||||
isBase64: tOptional(tBoolean),
|
isBase64: tOptional(tBoolean),
|
||||||
});
|
});
|
||||||
|
scheme.RouteResponseBodyParams = tOptional(tObject({}));
|
||||||
scheme.ResourceTiming = tObject({
|
scheme.ResourceTiming = tObject({
|
||||||
startTime: tNumber,
|
startTime: tNumber,
|
||||||
domainLookupStart: tNumber,
|
domainLookupStart: tNumber,
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ export class CRNetworkManager {
|
||||||
this._client.send('Network.setCacheDisabled', { cacheDisabled: true }),
|
this._client.send('Network.setCacheDisabled', { cacheDisabled: true }),
|
||||||
this._client.send('Fetch.enable', {
|
this._client.send('Fetch.enable', {
|
||||||
handleAuthRequests: true,
|
handleAuthRequests: true,
|
||||||
patterns: [{urlPattern: '*'}],
|
patterns: [{urlPattern: '*', requestStage: 'Request'}, {urlPattern: '*', requestStage: 'Response'}],
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -177,6 +177,19 @@ export class CRNetworkManager {
|
||||||
if (event.request.url.startsWith('data:'))
|
if (event.request.url.startsWith('data:'))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
||||||
|
if (event.responseStatusCode || event.responseHeaders || event.responseErrorReason) {
|
||||||
|
const request = this._requestIdToRequest.get(event.networkId);
|
||||||
|
if (!request || !request._onInterceptedResponse) {
|
||||||
|
this._client._sendMayFail('Fetch.continueRequest', {
|
||||||
|
requestId: event.requestId
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
request._onInterceptedResponse!(event);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const requestId = event.networkId;
|
const requestId = event.networkId;
|
||||||
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(requestId);
|
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(requestId);
|
||||||
if (requestWillBeSentEvent) {
|
if (requestWillBeSentEvent) {
|
||||||
|
|
@ -394,9 +407,10 @@ class InterceptableRequest implements network.RouteDelegate {
|
||||||
_requestId: string;
|
_requestId: string;
|
||||||
_interceptionId: string | null;
|
_interceptionId: string | null;
|
||||||
_documentId: string | undefined;
|
_documentId: string | undefined;
|
||||||
private _client: CRSession;
|
private readonly _client: CRSession;
|
||||||
_timestamp: number;
|
_timestamp: number;
|
||||||
_wallTime: number;
|
_wallTime: number;
|
||||||
|
_onInterceptedResponse: ((event: Protocol.Fetch.requestPausedPayload) => void) | null = null;
|
||||||
|
|
||||||
constructor(options: {
|
constructor(options: {
|
||||||
client: CRSession;
|
client: CRSession;
|
||||||
|
|
@ -429,7 +443,13 @@ class InterceptableRequest implements network.RouteDelegate {
|
||||||
this.request = new network.Request(allowInterception ? this : null, frame, redirectedFrom, documentId, url, type, method, postDataBuffer, headersObjectToArray(headers));
|
this.request = new network.Request(allowInterception ? this : null, frame, redirectedFrom, documentId, url, type, method, postDataBuffer, headersObjectToArray(headers));
|
||||||
}
|
}
|
||||||
|
|
||||||
async continue(overrides: types.NormalizedContinueOverrides) {
|
async responseBody(): Promise<Buffer> {
|
||||||
|
const response = await this._client.send('Fetch.getResponseBody', { requestId: this._interceptionId! });
|
||||||
|
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
async continue(overrides: types.NormalizedContinueOverrides): Promise<network.InterceptedResponse|null> {
|
||||||
|
const interceptPromise = overrides.interceptResponse ? new Promise<Protocol.Fetch.requestPausedPayload>(resolve => this._onInterceptedResponse = resolve) : null;
|
||||||
// In certain cases, protocol will return error if the request was already canceled
|
// In certain cases, protocol will return error if the request was already canceled
|
||||||
// or the page was closed. We should tolerate these errors.
|
// or the page was closed. We should tolerate these errors.
|
||||||
await this._client._sendMayFail('Fetch.continueRequest', {
|
await this._client._sendMayFail('Fetch.continueRequest', {
|
||||||
|
|
@ -439,6 +459,10 @@ class InterceptableRequest implements network.RouteDelegate {
|
||||||
method: overrides.method,
|
method: overrides.method,
|
||||||
postData: overrides.postData ? overrides.postData.toString('base64') : undefined
|
postData: overrides.postData ? overrides.postData.toString('base64') : undefined
|
||||||
});
|
});
|
||||||
|
if (!interceptPromise)
|
||||||
|
return null;
|
||||||
|
const event = await interceptPromise;
|
||||||
|
return new network.InterceptedResponse(this.request, event.responseStatusCode!, event.responseErrorReason!, event.responseHeaders!);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fulfill(response: types.NormalizedFulfillResponse) {
|
async fulfill(response: types.NormalizedFulfillResponse) {
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@ export class CRPage implements PageDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateRequestInterception(): Promise<void> {
|
async updateRequestInterception(): Promise<void> {
|
||||||
await this._forAllFrameSessions(frame => frame._updateRequestInterception(false));
|
await this._forAllFrameSessions(frame => frame._updateRequestInterception());
|
||||||
}
|
}
|
||||||
|
|
||||||
async setFileChooserIntercepted(enabled: boolean) {
|
async setFileChooserIntercepted(enabled: boolean) {
|
||||||
|
|
@ -521,7 +521,7 @@ class FrameSession {
|
||||||
promises.push(emulateTimezone(this._client, options.timezoneId));
|
promises.push(emulateTimezone(this._client, options.timezoneId));
|
||||||
promises.push(this._updateGeolocation(true));
|
promises.push(this._updateGeolocation(true));
|
||||||
promises.push(this._updateExtraHTTPHeaders(true));
|
promises.push(this._updateExtraHTTPHeaders(true));
|
||||||
promises.push(this._updateRequestInterception(true));
|
promises.push(this._updateRequestInterception());
|
||||||
promises.push(this._updateOffline(true));
|
promises.push(this._updateOffline(true));
|
||||||
promises.push(this._updateHttpCredentials(true));
|
promises.push(this._updateHttpCredentials(true));
|
||||||
promises.push(this._updateEmulateMedia(true));
|
promises.push(this._updateEmulateMedia(true));
|
||||||
|
|
@ -1007,7 +1007,7 @@ class FrameSession {
|
||||||
await this._client.send('Emulation.setEmulatedMedia', { media: this._page._state.mediaType || '', features });
|
await this._client.send('Emulation.setEmulatedMedia', { media: this._page._state.mediaType || '', features });
|
||||||
}
|
}
|
||||||
|
|
||||||
async _updateRequestInterception(initial: boolean): Promise<void> {
|
async _updateRequestInterception(): Promise<void> {
|
||||||
await this._networkManager.setRequestInterception(this._page._needsRequestInterception());
|
await this._networkManager.setRequestInterception(this._page._needsRequestInterception());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,11 @@ class InterceptableRequest implements network.RouteDelegate {
|
||||||
payload.url, internalCauseToResourceType[payload.internalCause] || causeToResourceType[payload.cause] || 'other', payload.method, postDataBuffer, payload.headers);
|
payload.url, internalCauseToResourceType[payload.internalCause] || causeToResourceType[payload.cause] || 'other', payload.method, postDataBuffer, payload.headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
async continue(overrides: types.NormalizedContinueOverrides) {
|
responseBody(): Promise<Buffer> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async continue(overrides: types.NormalizedContinueOverrides): Promise<network.InterceptedResponse|null> {
|
||||||
await this._session.sendMayFail('Network.resumeInterceptedRequest', {
|
await this._session.sendMayFail('Network.resumeInterceptedRequest', {
|
||||||
requestId: this._id,
|
requestId: this._id,
|
||||||
url: overrides.url,
|
url: overrides.url,
|
||||||
|
|
@ -194,6 +198,9 @@ class InterceptableRequest implements network.RouteDelegate {
|
||||||
headers: overrides.headers,
|
headers: overrides.headers,
|
||||||
postData: overrides.postData ? Buffer.from(overrides.postData).toString('base64') : undefined
|
postData: overrides.postData ? Buffer.from(overrides.postData).toString('base64') : undefined
|
||||||
});
|
});
|
||||||
|
if (overrides.interceptResponse)
|
||||||
|
throw new Error('Response interception not implemented');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fulfill(response: types.NormalizedFulfillResponse) {
|
async fulfill(response: types.NormalizedFulfillResponse) {
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,7 @@ export class Route extends SdkObject {
|
||||||
private readonly _request: Request;
|
private readonly _request: Request;
|
||||||
private readonly _delegate: RouteDelegate;
|
private readonly _delegate: RouteDelegate;
|
||||||
private _handled = false;
|
private _handled = false;
|
||||||
|
private _response: InterceptedResponse | null = null;
|
||||||
|
|
||||||
constructor(request: Request, delegate: RouteDelegate) {
|
constructor(request: Request, delegate: RouteDelegate) {
|
||||||
super(request.frame(), 'route');
|
super(request.frame(), 'route');
|
||||||
|
|
@ -225,26 +226,44 @@ export class Route extends SdkObject {
|
||||||
await this._delegate.abort(errorCode);
|
await this._delegate.abort(errorCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fulfill(response: { status?: number, headers?: types.HeadersArray, body?: string, isBase64?: boolean }) {
|
async fulfill(overrides: { status?: number, headers?: types.HeadersArray, body?: string, isBase64?: boolean }) {
|
||||||
assert(!this._handled, 'Route is already handled!');
|
assert(!this._handled, 'Route is already handled!');
|
||||||
this._handled = true;
|
this._handled = true;
|
||||||
|
let body = overrides.body;
|
||||||
|
let isBase64 = overrides.isBase64 || false;
|
||||||
|
if (!body) {
|
||||||
|
if (this._response) {
|
||||||
|
body = (await this._delegate.responseBody(true)).toString('utf8');
|
||||||
|
isBase64 = false;
|
||||||
|
} else {
|
||||||
|
body = '';
|
||||||
|
isBase64 = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
await this._delegate.fulfill({
|
await this._delegate.fulfill({
|
||||||
status: response.status === undefined ? 200 : response.status,
|
status: overrides.status || this._response?.status() || 200,
|
||||||
headers: response.headers || [],
|
headers: overrides.headers || this._response?.headers() || [],
|
||||||
body: response.body || '',
|
body,
|
||||||
isBase64: response.isBase64 || false,
|
isBase64,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async continue(overrides: types.NormalizedContinueOverrides = {}) {
|
async continue(overrides: types.NormalizedContinueOverrides = {}): Promise<InterceptedResponse|null> {
|
||||||
assert(!this._handled, 'Route is already handled!');
|
assert(!this._handled, 'Route is already handled!');
|
||||||
|
assert(!this._response, 'Cannot call continue after response interception!');
|
||||||
if (overrides.url) {
|
if (overrides.url) {
|
||||||
const newUrl = new URL(overrides.url);
|
const newUrl = new URL(overrides.url);
|
||||||
const oldUrl = new URL(this._request.url());
|
const oldUrl = new URL(this._request.url());
|
||||||
if (oldUrl.protocol !== newUrl.protocol)
|
if (oldUrl.protocol !== newUrl.protocol)
|
||||||
throw new Error('New URL must have same protocol as overridden URL');
|
throw new Error('New URL must have same protocol as overridden URL');
|
||||||
}
|
}
|
||||||
await this._delegate.continue(overrides);
|
this._response = await this._delegate.continue(overrides);
|
||||||
|
return this._response
|
||||||
|
}
|
||||||
|
|
||||||
|
async responseBody(): Promise<Buffer> {
|
||||||
|
assert(!this._handled, 'Route is already handled!');
|
||||||
|
return this._delegate.responseBody(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -385,6 +404,37 @@ export class Response extends SdkObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class InterceptedResponse extends SdkObject {
|
||||||
|
private readonly _request: Request;
|
||||||
|
private readonly _status: number;
|
||||||
|
private readonly _statusText: string;
|
||||||
|
private readonly _headers: types.HeadersArray;
|
||||||
|
|
||||||
|
constructor(request: Request, status: number, statusText: string, headers: types.HeadersArray) {
|
||||||
|
super(request.frame(), 'interceptedResponse');
|
||||||
|
this._request = request;
|
||||||
|
this._status = status;
|
||||||
|
this._statusText = statusText;
|
||||||
|
this._headers = headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
status(): number {
|
||||||
|
return this._status;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusText(): string {
|
||||||
|
return this._statusText;
|
||||||
|
}
|
||||||
|
|
||||||
|
headers(): types.HeadersArray {
|
||||||
|
return this._headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
request(): Request {
|
||||||
|
return this._request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class WebSocket extends SdkObject {
|
export class WebSocket extends SdkObject {
|
||||||
private _url: string;
|
private _url: string;
|
||||||
|
|
||||||
|
|
@ -424,7 +474,8 @@ export class WebSocket extends SdkObject {
|
||||||
export interface RouteDelegate {
|
export interface RouteDelegate {
|
||||||
abort(errorCode: string): Promise<void>;
|
abort(errorCode: string): Promise<void>;
|
||||||
fulfill(response: types.NormalizedFulfillResponse): Promise<void>;
|
fulfill(response: types.NormalizedFulfillResponse): Promise<void>;
|
||||||
continue(overrides: types.NormalizedContinueOverrides): Promise<void>;
|
continue(overrides: types.NormalizedContinueOverrides): Promise<InterceptedResponse|null>;
|
||||||
|
responseBody(forFulfill: boolean): Promise<Buffer>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes.
|
// List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes.
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,15 @@ export type NormalizedContinueOverrides = {
|
||||||
method?: string,
|
method?: string,
|
||||||
headers?: HeadersArray,
|
headers?: HeadersArray,
|
||||||
postData?: Buffer,
|
postData?: Buffer,
|
||||||
|
interceptResponse?: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NormalizedResponseContinueOverrides = {
|
||||||
|
status?: number,
|
||||||
|
statusText?: string,
|
||||||
|
headers?: HeadersArray,
|
||||||
|
body?: string,
|
||||||
|
isBase64?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NetworkCookie = {
|
export type NetworkCookie = {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ import * as types from '../types';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { WKSession } from './wkConnection';
|
import { WKSession } from './wkConnection';
|
||||||
import { assert, headersObjectToArray, headersArrayToObject } from '../../utils/utils';
|
import { assert, headersObjectToArray, headersArrayToObject } from '../../utils/utils';
|
||||||
|
import { InterceptedResponse } from '../network';
|
||||||
|
import { WKPage } from './wkPage';
|
||||||
|
|
||||||
const errorReasons: { [reason: string]: Protocol.Network.ResourceErrorType } = {
|
const errorReasons: { [reason: string]: Protocol.Network.ResourceErrorType } = {
|
||||||
'aborted': 'Cancellation',
|
'aborted': 'Cancellation',
|
||||||
|
|
@ -45,6 +47,8 @@ export class WKInterceptableRequest implements network.RouteDelegate {
|
||||||
readonly _requestId: string;
|
readonly _requestId: string;
|
||||||
_interceptedCallback: () => void = () => {};
|
_interceptedCallback: () => void = () => {};
|
||||||
private _interceptedPromise: Promise<unknown>;
|
private _interceptedPromise: Promise<unknown>;
|
||||||
|
_responseInterceptedCallback: ((r: Protocol.Network.Response) => void) | undefined;
|
||||||
|
private _responseInterceptedPromise: Promise<Protocol.Network.Response> | undefined;
|
||||||
readonly _allowInterception: boolean;
|
readonly _allowInterception: boolean;
|
||||||
_timestamp: number;
|
_timestamp: number;
|
||||||
_wallTime: number;
|
_wallTime: number;
|
||||||
|
|
@ -64,6 +68,14 @@ export class WKInterceptableRequest implements network.RouteDelegate {
|
||||||
this._interceptedPromise = new Promise<void>(f => this._interceptedCallback = f);
|
this._interceptedPromise = new Promise<void>(f => this._interceptedCallback = f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async responseBody(forFulfill: boolean): Promise<Buffer> {
|
||||||
|
// Empty buffer will result in the response being used.
|
||||||
|
if (forFulfill)
|
||||||
|
return Buffer.from('');
|
||||||
|
const response = await this._session.send('Network.getResponseBody', { requestId: this._requestId });
|
||||||
|
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
async abort(errorCode: string) {
|
async abort(errorCode: string) {
|
||||||
const errorType = errorReasons[errorCode];
|
const errorType = errorReasons[errorCode];
|
||||||
assert(errorType, 'Unknown error code: ' + errorCode);
|
assert(errorType, 'Unknown error code: ' + errorCode);
|
||||||
|
|
@ -86,7 +98,9 @@ export class WKInterceptableRequest implements network.RouteDelegate {
|
||||||
const contentType = headers['content-type'];
|
const contentType = headers['content-type'];
|
||||||
if (contentType)
|
if (contentType)
|
||||||
mimeType = contentType.split(';')[0].trim();
|
mimeType = contentType.split(';')[0].trim();
|
||||||
await this._session.sendMayFail('Network.interceptRequestWithResponse', {
|
|
||||||
|
const isResponseIntercepted = await this._responseInterceptedPromise;
|
||||||
|
await this._session.sendMayFail(isResponseIntercepted ? 'Network.interceptWithResponse' :'Network.interceptRequestWithResponse', {
|
||||||
requestId: this._requestId,
|
requestId: this._requestId,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
statusText: network.STATUS_TEXTS[String(response.status)],
|
statusText: network.STATUS_TEXTS[String(response.status)],
|
||||||
|
|
@ -97,7 +111,11 @@ export class WKInterceptableRequest implements network.RouteDelegate {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async continue(overrides: types.NormalizedContinueOverrides) {
|
async continue(overrides: types.NormalizedContinueOverrides): Promise<network.InterceptedResponse|null> {
|
||||||
|
if (overrides.interceptResponse) {
|
||||||
|
await (this.request.frame()._page._delegate as WKPage)._ensureResponseInterceptionEnabled();
|
||||||
|
this._responseInterceptedPromise = new Promise(f => this._responseInterceptedCallback = f);
|
||||||
|
}
|
||||||
await this._interceptedPromise;
|
await this._interceptedPromise;
|
||||||
// In certain cases, protocol will return error if the request was already canceled
|
// In certain cases, protocol will return error if the request was already canceled
|
||||||
// or the page was closed. We should tolerate these errors.
|
// or the page was closed. We should tolerate these errors.
|
||||||
|
|
@ -108,6 +126,10 @@ export class WKInterceptableRequest implements network.RouteDelegate {
|
||||||
headers: overrides.headers ? headersArrayToObject(overrides.headers, false /* lowerCase */) : undefined,
|
headers: overrides.headers ? headersArrayToObject(overrides.headers, false /* lowerCase */) : undefined,
|
||||||
postData: overrides.postData ? Buffer.from(overrides.postData).toString('base64') : undefined
|
postData: overrides.postData ? Buffer.from(overrides.postData).toString('base64') : undefined
|
||||||
});
|
});
|
||||||
|
if (!this._responseInterceptedPromise)
|
||||||
|
return null;
|
||||||
|
const responsePayload = await this._responseInterceptedPromise;
|
||||||
|
return new InterceptedResponse(this.request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers));
|
||||||
}
|
}
|
||||||
|
|
||||||
createResponse(responsePayload: Protocol.Network.Response): network.Response {
|
createResponse(responsePayload: Protocol.Network.Response): network.Response {
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ export class WKPage implements PageDelegate {
|
||||||
private _lastConsoleMessage: { derivedType: string, text: string, handles: JSHandle[]; count: number, location: types.ConsoleMessageLocation; } | null = null;
|
private _lastConsoleMessage: { derivedType: string, text: string, handles: JSHandle[]; count: number, location: types.ConsoleMessageLocation; } | null = null;
|
||||||
|
|
||||||
private readonly _requestIdToResponseReceivedPayloadEvent = new Map<string, Protocol.Network.responseReceivedPayload>();
|
private readonly _requestIdToResponseReceivedPayloadEvent = new Map<string, Protocol.Network.responseReceivedPayload>();
|
||||||
|
_needsResponseInterception: boolean = false;
|
||||||
// Holds window features for the next popup being opened via window.open,
|
// Holds window features for the next popup being opened via window.open,
|
||||||
// until the popup page proxy arrives.
|
// until the popup page proxy arrives.
|
||||||
private _nextWindowOpenPopupFeatures?: string[];
|
private _nextWindowOpenPopupFeatures?: string[];
|
||||||
|
|
@ -176,6 +177,8 @@ export class WKPage implements PageDelegate {
|
||||||
if (this._page._needsRequestInterception()) {
|
if (this._page._needsRequestInterception()) {
|
||||||
promises.push(session.send('Network.setInterceptionEnabled', { enabled: true }));
|
promises.push(session.send('Network.setInterceptionEnabled', { enabled: true }));
|
||||||
promises.push(session.send('Network.addInterception', { url: '.*', stage: 'request', isRegex: true }));
|
promises.push(session.send('Network.addInterception', { url: '.*', stage: 'request', isRegex: true }));
|
||||||
|
if (this._needsResponseInterception)
|
||||||
|
promises.push(session.send('Network.addInterception', { url: '.*', stage: 'response', isRegex: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const contextOptions = this._browserContext._options;
|
const contextOptions = this._browserContext._options;
|
||||||
|
|
@ -367,7 +370,8 @@ export class WKPage implements PageDelegate {
|
||||||
helper.addEventListener(this._pageProxySession, 'Dialog.javascriptDialogOpening', event => this._onDialog(event)),
|
helper.addEventListener(this._pageProxySession, 'Dialog.javascriptDialogOpening', event => this._onDialog(event)),
|
||||||
helper.addEventListener(this._session, 'Page.fileChooserOpened', event => this._onFileChooserOpened(event)),
|
helper.addEventListener(this._session, 'Page.fileChooserOpened', event => this._onFileChooserOpened(event)),
|
||||||
helper.addEventListener(this._session, 'Network.requestWillBeSent', e => this._onRequestWillBeSent(this._session, e)),
|
helper.addEventListener(this._session, 'Network.requestWillBeSent', e => this._onRequestWillBeSent(this._session, e)),
|
||||||
helper.addEventListener(this._session, 'Network.requestIntercepted', e => this._onRequestIntercepted(e)),
|
helper.addEventListener(this._session, 'Network.requestIntercepted', e => this._onRequestIntercepted(this._session, e)),
|
||||||
|
helper.addEventListener(this._session, 'Network.responseIntercepted', e => this._onResponseIntercepted(this._session, e)),
|
||||||
helper.addEventListener(this._session, 'Network.responseReceived', e => this._onResponseReceived(e)),
|
helper.addEventListener(this._session, 'Network.responseReceived', e => this._onResponseReceived(e)),
|
||||||
helper.addEventListener(this._session, 'Network.loadingFinished', e => this._onLoadingFinished(e)),
|
helper.addEventListener(this._session, 'Network.loadingFinished', e => this._onLoadingFinished(e)),
|
||||||
helper.addEventListener(this._session, 'Network.loadingFailed', e => this._onLoadingFailed(e)),
|
helper.addEventListener(this._session, 'Network.loadingFailed', e => this._onLoadingFailed(e)),
|
||||||
|
|
@ -380,7 +384,6 @@ export class WKPage implements PageDelegate {
|
||||||
helper.addEventListener(this._session, 'Network.webSocketFrameError', e => this._page._frameManager.webSocketError(e.requestId, e.errorMessage)),
|
helper.addEventListener(this._session, 'Network.webSocketFrameError', e => this._page._frameManager.webSocketError(e.requestId, e.errorMessage)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _updateState<T extends keyof Protocol.CommandParameters>(
|
private async _updateState<T extends keyof Protocol.CommandParameters>(
|
||||||
method: T,
|
method: T,
|
||||||
params?: Protocol.CommandParameters[T]
|
params?: Protocol.CommandParameters[T]
|
||||||
|
|
@ -656,12 +659,22 @@ export class WKPage implements PageDelegate {
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _ensureResponseInterceptionEnabled() {
|
||||||
|
if (this._needsResponseInterception)
|
||||||
|
return;
|
||||||
|
this._needsResponseInterception = true;
|
||||||
|
await this.updateRequestInterception();
|
||||||
|
}
|
||||||
|
|
||||||
async updateRequestInterception(): Promise<void> {
|
async updateRequestInterception(): Promise<void> {
|
||||||
const enabled = this._page._needsRequestInterception();
|
const enabled = this._page._needsRequestInterception();
|
||||||
await Promise.all([
|
const promises = [
|
||||||
this._updateState('Network.setInterceptionEnabled', { enabled }),
|
this._updateState('Network.setInterceptionEnabled', { enabled }),
|
||||||
this._updateState('Network.addInterception', { url: '.*', stage: 'request', isRegex: true })
|
this._updateState('Network.addInterception', { url: '.*', stage: 'request', isRegex: true }),
|
||||||
]);
|
];
|
||||||
|
if (this._needsResponseInterception)
|
||||||
|
this._updateState('Network.addInterception', { url: '.*', stage: 'response', isRegex: true })
|
||||||
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateOffline() {
|
async updateOffline() {
|
||||||
|
|
@ -962,21 +975,30 @@ export class WKPage implements PageDelegate {
|
||||||
this._page._frameManager.requestFinished(request.request);
|
this._page._frameManager.requestFinished(request.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRequestIntercepted(event: Protocol.Network.requestInterceptedPayload) {
|
_onRequestIntercepted(session: WKSession, event: Protocol.Network.requestInterceptedPayload) {
|
||||||
const request = this._requestIdToRequest.get(event.requestId);
|
const request = this._requestIdToRequest.get(event.requestId);
|
||||||
if (!request) {
|
if (!request) {
|
||||||
this._session.sendMayFail('Network.interceptRequestWithError', {errorType: 'Cancellation', requestId: event.requestId});
|
session.sendMayFail('Network.interceptRequestWithError', {errorType: 'Cancellation', requestId: event.requestId});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!request._allowInterception) {
|
if (!request._allowInterception) {
|
||||||
// Intercepted, although we do not intend to allow interception.
|
// Intercepted, although we do not intend to allow interception.
|
||||||
// Just continue.
|
// Just continue.
|
||||||
this._session.sendMayFail('Network.interceptWithRequest', { requestId: request._requestId });
|
session.sendMayFail('Network.interceptWithRequest', { requestId: request._requestId });
|
||||||
} else {
|
} else {
|
||||||
request._interceptedCallback();
|
request._interceptedCallback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onResponseIntercepted(session: WKSession, event: Protocol.Network.responseInterceptedPayload) {
|
||||||
|
const request = this._requestIdToRequest.get(event.requestId);
|
||||||
|
if (!request || !request._responseInterceptedCallback) {
|
||||||
|
session.sendMayFail('Network.interceptContinue', { requestId: event.requestId, stage: 'response' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
request._responseInterceptedCallback(event.response);
|
||||||
|
}
|
||||||
|
|
||||||
_onResponseReceived(event: Protocol.Network.responseReceivedPayload) {
|
_onResponseReceived(event: Protocol.Network.responseReceivedPayload) {
|
||||||
const request = this._requestIdToRequest.get(event.requestId);
|
const request = this._requestIdToRequest.get(event.requestId);
|
||||||
// FileUpload sends a response without a matching request.
|
// FileUpload sends a response without a matching request.
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,8 @@ export class WKProvisionalPage {
|
||||||
|
|
||||||
this._sessionListeners = [
|
this._sessionListeners = [
|
||||||
helper.addEventListener(session, 'Network.requestWillBeSent', overrideFrameId(e => wkPage._onRequestWillBeSent(session, e))),
|
helper.addEventListener(session, 'Network.requestWillBeSent', overrideFrameId(e => wkPage._onRequestWillBeSent(session, e))),
|
||||||
helper.addEventListener(session, 'Network.requestIntercepted', overrideFrameId(e => wkPage._onRequestIntercepted(e))),
|
helper.addEventListener(session, 'Network.requestIntercepted', overrideFrameId(e => wkPage._onRequestIntercepted(session, e))),
|
||||||
|
helper.addEventListener(session, 'Network.responseIntercepted', overrideFrameId(e => wkPage._onResponseIntercepted(session, e))),
|
||||||
helper.addEventListener(session, 'Network.responseReceived', overrideFrameId(e => wkPage._onResponseReceived(e))),
|
helper.addEventListener(session, 'Network.responseReceived', overrideFrameId(e => wkPage._onResponseReceived(e))),
|
||||||
helper.addEventListener(session, 'Network.loadingFinished', overrideFrameId(e => wkPage._onLoadingFinished(e))),
|
helper.addEventListener(session, 'Network.loadingFinished', overrideFrameId(e => wkPage._onLoadingFinished(e))),
|
||||||
helper.addEventListener(session, 'Network.loadingFailed', overrideFrameId(e => wkPage._onLoadingFailed(e))),
|
helper.addEventListener(session, 'Network.loadingFailed', overrideFrameId(e => wkPage._onLoadingFailed(e))),
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,10 @@ if (browserName !== 'chromium') {
|
||||||
if (browserName === 'webkit')
|
if (browserName === 'webkit')
|
||||||
api.delete('browserContext.clearPermissions');
|
api.delete('browserContext.clearPermissions');
|
||||||
|
|
||||||
|
// Response interception is not implemented in Firefox yet.
|
||||||
|
if (browserName === 'firefox')
|
||||||
|
api.delete('route.intercept');
|
||||||
|
|
||||||
const coverageDir = path.join(__dirname, '..', 'coverage-report');
|
const coverageDir = path.join(__dirname, '..', 'coverage-report');
|
||||||
|
|
||||||
const coveredMethods = new Set();
|
const coveredMethods = new Set();
|
||||||
|
|
|
||||||
154
tests/page/page-request-intercept.spec.ts
Normal file
154
tests/page/page-request-intercept.spec.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2018 Google Inc. All rights reserved.
|
||||||
|
* Modifications copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { fail } from 'assert';
|
||||||
|
import type { Route } from '../../index';
|
||||||
|
import { test as it, expect } from './pageTest';
|
||||||
|
|
||||||
|
it('should fulfill intercepted response', async ({page, server, browserName}) => {
|
||||||
|
it.fixme(browserName === 'firefox');
|
||||||
|
await page.route('**/*', async route => {
|
||||||
|
await route.intercept({});
|
||||||
|
await route.fulfill({
|
||||||
|
status: 201,
|
||||||
|
headers: {
|
||||||
|
foo: 'bar'
|
||||||
|
},
|
||||||
|
contentType: 'text/plain',
|
||||||
|
body: 'Yo, page!'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const response = await page.goto(server.PREFIX + '/empty.html');
|
||||||
|
expect(response.status()).toBe(201);
|
||||||
|
expect(response.headers().foo).toBe('bar');
|
||||||
|
expect(response.headers()['content-type']).toBe('text/plain');
|
||||||
|
expect(await page.evaluate(() => document.body.textContent)).toBe('Yo, page!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw on continue after intercept', async ({page, server, browserName}) => {
|
||||||
|
it.fixme(browserName === 'firefox');
|
||||||
|
|
||||||
|
let routeCallback;
|
||||||
|
const routePromise = new Promise<Route>(f => routeCallback = f);
|
||||||
|
await page.route('**', routeCallback);
|
||||||
|
|
||||||
|
page.goto(server.EMPTY_PAGE).catch(e => {});
|
||||||
|
const route = await routePromise;
|
||||||
|
await route.intercept();
|
||||||
|
try {
|
||||||
|
await route.continue();
|
||||||
|
fail('did not throw');
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toContain('Cannot call continue after response interception!')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support fulfill after intercept', async ({page, server, browserName}) => {
|
||||||
|
it.fixme(browserName === 'firefox');
|
||||||
|
const requestPromise = server.waitForRequest('/empty.html');
|
||||||
|
await page.route('**', async route => {
|
||||||
|
await route.intercept();
|
||||||
|
await route.fulfill();
|
||||||
|
});
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
const request = await requestPromise;
|
||||||
|
expect(request.url).toBe('/empty.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should support request overrides', async ({page, server, browserName}) => {
|
||||||
|
it.fixme(browserName === 'firefox');
|
||||||
|
const requestPromise = server.waitForRequest('/empty.html');
|
||||||
|
await page.route('**/foo', async route => {
|
||||||
|
await route.intercept({
|
||||||
|
url: server.EMPTY_PAGE,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'foo': 'bar'},
|
||||||
|
postData: 'my data',
|
||||||
|
});
|
||||||
|
await route.fulfill();
|
||||||
|
});
|
||||||
|
await page.goto(server.PREFIX + '/foo');
|
||||||
|
const request = await requestPromise;
|
||||||
|
expect(request.method).toBe('POST');
|
||||||
|
expect(request.url).toBe('/empty.html');
|
||||||
|
expect(request.headers['foo']).toBe('bar');
|
||||||
|
expect((await request.postBody).toString('utf8')).toBe('my data');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give access to the intercepted response', async ({page, server, browserName}) => {
|
||||||
|
it.fixme(browserName === 'firefox');
|
||||||
|
// it.fixme(browserName === 'webkit');
|
||||||
|
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
|
||||||
|
let routeCallback;
|
||||||
|
const routePromise = new Promise<Route>(f => routeCallback = f);
|
||||||
|
await page.route('**/title.html', routeCallback);
|
||||||
|
|
||||||
|
const evalPromise = page.evaluate(url => fetch(url), server.PREFIX + '/title.html').catch(console.log);
|
||||||
|
|
||||||
|
const route = await routePromise;
|
||||||
|
const response = await route.intercept();
|
||||||
|
|
||||||
|
expect(response.status()).toBe(200);
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
expect(response.url()).toBe(server.PREFIX + '/title.html');
|
||||||
|
expect(response.headers()['content-type']).toBe('text/html; charset=utf-8');
|
||||||
|
|
||||||
|
await Promise.all([route.fulfill(), evalPromise]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give access to the intercepted response body', async ({page, server, browserName}) => {
|
||||||
|
it.fixme(browserName === 'firefox');
|
||||||
|
it.fixme(browserName === 'webkit');
|
||||||
|
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
|
||||||
|
let routeCallback;
|
||||||
|
const routePromise = new Promise<Route>(f => routeCallback = f);
|
||||||
|
await page.route('**/simple.json', routeCallback);
|
||||||
|
|
||||||
|
const evalPromise = page.evaluate(url => fetch(url), server.PREFIX + '/simple.json').catch(console.log);
|
||||||
|
|
||||||
|
const route = await routePromise;
|
||||||
|
const response = await route.intercept();
|
||||||
|
|
||||||
|
expect((await response.text())).toBe('{"foo": "bar"}\n');
|
||||||
|
|
||||||
|
await Promise.all([route.fulfill(), evalPromise]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be abortable after interception', async ({page, server, browserName}) => {
|
||||||
|
it.fixme(browserName === 'firefox');
|
||||||
|
it.fixme(browserName === 'webkit');
|
||||||
|
|
||||||
|
await page.route(/\.css$/, async route => {
|
||||||
|
await route.intercept();
|
||||||
|
await route.abort();
|
||||||
|
});
|
||||||
|
let failed = false;
|
||||||
|
page.on('requestfailed', request => {
|
||||||
|
if (request.url().includes('.css'))
|
||||||
|
failed = true;
|
||||||
|
});
|
||||||
|
const response = await page.goto(server.PREFIX + '/one-style.html');
|
||||||
|
expect(response.ok()).toBe(true);
|
||||||
|
expect(response.request().failure()).toBe(null);
|
||||||
|
expect(failed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
26
types/types.d.ts
vendored
26
types/types.d.ts
vendored
|
|
@ -10546,6 +10546,32 @@ export interface Route {
|
||||||
status?: number;
|
status?: number;
|
||||||
}): Promise<void>;
|
}): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Continues route's request with optional overrides and intercepts response.
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
intercept(options?: {
|
||||||
|
/**
|
||||||
|
* If set changes the request HTTP headers. Header values will be converted to a string.
|
||||||
|
*/
|
||||||
|
headers?: { [key: string]: string; };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set changes the request method (e.g. GET or POST)
|
||||||
|
*/
|
||||||
|
method?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set changes the post data of request
|
||||||
|
*/
|
||||||
|
postData?: string|Buffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set changes the request URL. New URL must have same protocol as original one.
|
||||||
|
*/
|
||||||
|
url?: string;
|
||||||
|
}): Promise<Response>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A request to be routed.
|
* A request to be routed.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue