chore(webdriver): support priority for request interception (#12191)

This commit is contained in:
Nikolay Vitkov 2024-04-02 13:20:36 +02:00 committed by GitHub
parent 59bffce972
commit 08bc8542ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 326 additions and 317 deletions

View File

@ -10,7 +10,7 @@ Aborts a request.
```typescript ```typescript
class HTTPRequest { class HTTPRequest {
abstract abort(errorCode?: ErrorCode, priority?: number): Promise<void>; abort(errorCode?: ErrorCode, priority?: number): Promise<void>;
} }
``` ```

View File

@ -10,7 +10,7 @@ The most recent reason for aborting the request
```typescript ```typescript
class HTTPRequest { class HTTPRequest {
abstract abortErrorReason(): Protocol.Network.ErrorReason | null; abortErrorReason(): Protocol.Network.ErrorReason | null;
} }
``` ```

View File

@ -10,7 +10,7 @@ Continues request with optional request overrides.
```typescript ```typescript
class HTTPRequest { class HTTPRequest {
abstract continue( continue(
overrides?: ContinueRequestOverrides, overrides?: ContinueRequestOverrides,
priority?: number priority?: number
): Promise<void>; ): Promise<void>;

View File

@ -10,7 +10,7 @@ The `ContinueRequestOverrides` that will be used if the interception is allowed
```typescript ```typescript
class HTTPRequest { class HTTPRequest {
abstract continueRequestOverrides(): ContinueRequestOverrides; continueRequestOverrides(): ContinueRequestOverrides;
} }
``` ```

View File

@ -10,7 +10,7 @@ Adds an async request handler to the processing queue. Deferred handlers are not
```typescript ```typescript
class HTTPRequest { class HTTPRequest {
abstract enqueueInterceptAction( enqueueInterceptAction(
pendingHandler: () => void | PromiseLike<unknown> pendingHandler: () => void | PromiseLike<unknown>
): void; ): void;
} }

View File

@ -10,7 +10,7 @@ Awaits pending interception handlers and then decides how to fulfill the request
```typescript ```typescript
class HTTPRequest { class HTTPRequest {
abstract finalizeInterceptions(): Promise<void>; finalizeInterceptions(): Promise<void>;
} }
``` ```

View File

@ -14,7 +14,7 @@ InterceptResolutionAction is one of: `abort`, `respond`, `continue`, `disabled`,
```typescript ```typescript
class HTTPRequest { class HTTPRequest {
abstract interceptResolutionState(): InterceptResolutionState; interceptResolutionState(): InterceptResolutionState;
} }
``` ```

View File

@ -10,7 +10,7 @@ Is `true` if the intercept resolution has already been handled, `false` otherwis
```typescript ```typescript
class HTTPRequest { class HTTPRequest {
abstract isInterceptResolutionHandled(): boolean; isInterceptResolutionHandled(): boolean;
} }
``` ```

View File

@ -10,7 +10,7 @@ Fulfills a request with the given response.
```typescript ```typescript
class HTTPRequest { class HTTPRequest {
abstract respond( respond(
response: Partial<ResponseForRequest>, response: Partial<ResponseForRequest>,
priority?: number priority?: number
): Promise<void>; ): Promise<void>;

View File

@ -10,7 +10,7 @@ The `ResponseForRequest` that gets used if the interception is allowed to respon
```typescript ```typescript
class HTTPRequest { class HTTPRequest {
abstract responseForRequest(): Partial<ResponseForRequest> | null; responseForRequest(): Partial<ResponseForRequest> | null;
} }
``` ```

View File

@ -7,6 +7,7 @@ import type {Protocol} from 'devtools-protocol';
import type {ProtocolError} from '../common/Errors.js'; import type {ProtocolError} from '../common/Errors.js';
import {debugError} from '../common/util.js'; import {debugError} from '../common/util.js';
import {assert} from '../util/assert.js';
import type {CDPSession} from './CDPSession.js'; import type {CDPSession} from './CDPSession.js';
import type {Frame} from './Frame.js'; import type {Frame} from './Frame.js';
@ -120,6 +121,29 @@ export abstract class HTTPRequest {
*/ */
_redirectChain: HTTPRequest[] = []; _redirectChain: HTTPRequest[] = [];
/**
* @internal
*/
protected interception: {
enabled: boolean;
handled: boolean;
handlers: Array<() => void | PromiseLike<any>>;
resolutionState: InterceptResolutionState;
requestOverrides: ContinueRequestOverrides;
response: Partial<ResponseForRequest> | null;
abortReason: Protocol.Network.ErrorReason | null;
} = {
enabled: false,
handled: false,
handlers: [],
resolutionState: {
action: InterceptResolutionAction.None,
},
requestOverrides: {},
response: null,
abortReason: null,
};
/** /**
* Warning! Using this client can break Puppeteer. Use with caution. * Warning! Using this client can break Puppeteer. Use with caution.
* *
@ -142,18 +166,27 @@ export abstract class HTTPRequest {
* if the interception is allowed to continue (ie, `abort()` and * if the interception is allowed to continue (ie, `abort()` and
* `respond()` aren't called). * `respond()` aren't called).
*/ */
abstract continueRequestOverrides(): ContinueRequestOverrides; continueRequestOverrides(): ContinueRequestOverrides {
assert(this.interception.enabled, 'Request Interception is not enabled!');
return this.interception.requestOverrides;
}
/** /**
* The `ResponseForRequest` that gets used if the * The `ResponseForRequest` that gets used if the
* interception is allowed to respond (ie, `abort()` is not called). * interception is allowed to respond (ie, `abort()` is not called).
*/ */
abstract responseForRequest(): Partial<ResponseForRequest> | null; responseForRequest(): Partial<ResponseForRequest> | null {
assert(this.interception.enabled, 'Request Interception is not enabled!');
return this.interception.response;
}
/** /**
* The most recent reason for aborting the request * The most recent reason for aborting the request
*/ */
abstract abortErrorReason(): Protocol.Network.ErrorReason | null; abortErrorReason(): Protocol.Network.ErrorReason | null {
assert(this.interception.enabled, 'Request Interception is not enabled!');
return this.interception.abortReason;
}
/** /**
* An InterceptResolutionState object describing the current resolution * An InterceptResolutionState object describing the current resolution
@ -166,13 +199,23 @@ export abstract class HTTPRequest {
* InterceptResolutionAction is one of: `abort`, `respond`, `continue`, * InterceptResolutionAction is one of: `abort`, `respond`, `continue`,
* `disabled`, `none`, or `already-handled`. * `disabled`, `none`, or `already-handled`.
*/ */
abstract interceptResolutionState(): InterceptResolutionState; interceptResolutionState(): InterceptResolutionState {
if (!this.interception.enabled) {
return {action: InterceptResolutionAction.Disabled};
}
if (this.interception.handled) {
return {action: InterceptResolutionAction.AlreadyHandled};
}
return {...this.interception.resolutionState};
}
/** /**
* Is `true` if the intercept resolution has already been handled, * Is `true` if the intercept resolution has already been handled,
* `false` otherwise. * `false` otherwise.
*/ */
abstract isInterceptResolutionHandled(): boolean; isInterceptResolutionHandled(): boolean {
return this.interception.handled;
}
/** /**
* Adds an async request handler to the processing queue. * Adds an async request handler to the processing queue.
@ -180,15 +223,51 @@ export abstract class HTTPRequest {
* but they are guaranteed to resolve before the request interception * but they are guaranteed to resolve before the request interception
* is finalized. * is finalized.
*/ */
abstract enqueueInterceptAction( enqueueInterceptAction(
pendingHandler: () => void | PromiseLike<unknown> pendingHandler: () => void | PromiseLike<unknown>
): void; ): void {
this.interception.handlers.push(pendingHandler);
}
/**
* @internal
*/
abstract _abort(
errorReason: Protocol.Network.ErrorReason | null
): Promise<void>;
/**
* @internal
*/
abstract _respond(response: Partial<ResponseForRequest>): Promise<void>;
/**
* @internal
*/
abstract _continue(overrides: ContinueRequestOverrides): Promise<void>;
/** /**
* Awaits pending interception handlers and then decides how to fulfill * Awaits pending interception handlers and then decides how to fulfill
* the request interception. * the request interception.
*/ */
abstract finalizeInterceptions(): Promise<void>; async finalizeInterceptions(): Promise<void> {
await this.interception.handlers.reduce((promiseChain, interceptAction) => {
return promiseChain.then(interceptAction);
}, Promise.resolve());
this.interception.handlers = []; // TODO: verify this is correct top let gc run
const {action} = this.interceptResolutionState();
switch (action) {
case 'abort':
return await this._abort(this.interception.abortReason);
case 'respond':
if (this.interception.response === null) {
throw new Error('Response is missing for the interception');
}
return await this._respond(this.interception.response);
case 'continue':
return await this._continue(this.interception.requestOverrides);
}
}
/** /**
* Contains the request's resource type as it was perceived by the rendering * Contains the request's resource type as it was perceived by the rendering
@ -326,10 +405,42 @@ export abstract class HTTPRequest {
* *
* Exception is immediately thrown if the request interception is not enabled. * Exception is immediately thrown if the request interception is not enabled.
*/ */
abstract continue( async continue(
overrides?: ContinueRequestOverrides, overrides: ContinueRequestOverrides = {},
priority?: number priority?: number
): Promise<void>; ): Promise<void> {
// Request interception is not supported for data: urls.
if (this.url().startsWith('data:')) {
return;
}
assert(this.interception.enabled, 'Request Interception is not enabled!');
assert(!this.interception.handled, 'Request is already handled!');
if (priority === undefined) {
return await this._continue(overrides);
}
this.interception.requestOverrides = overrides;
if (
this.interception.resolutionState.priority === undefined ||
priority > this.interception.resolutionState.priority
) {
this.interception.resolutionState = {
action: InterceptResolutionAction.Continue,
priority,
};
return;
}
if (priority === this.interception.resolutionState.priority) {
if (
this.interception.resolutionState.action === 'abort' ||
this.interception.resolutionState.action === 'respond'
) {
return;
}
this.interception.resolutionState.action =
InterceptResolutionAction.Continue;
}
return;
}
/** /**
* Fulfills a request with the given response. * Fulfills a request with the given response.
@ -363,10 +474,38 @@ export abstract class HTTPRequest {
* *
* Exception is immediately thrown if the request interception is not enabled. * Exception is immediately thrown if the request interception is not enabled.
*/ */
abstract respond( async respond(
response: Partial<ResponseForRequest>, response: Partial<ResponseForRequest>,
priority?: number priority?: number
): Promise<void>; ): Promise<void> {
// Mocking responses for dataURL requests is not currently supported.
if (this.url().startsWith('data:')) {
return;
}
assert(this.interception.enabled, 'Request Interception is not enabled!');
assert(!this.interception.handled, 'Request is already handled!');
if (priority === undefined) {
return await this._respond(response);
}
this.interception.response = response;
if (
this.interception.resolutionState.priority === undefined ||
priority > this.interception.resolutionState.priority
) {
this.interception.resolutionState = {
action: InterceptResolutionAction.Respond,
priority,
};
return;
}
if (priority === this.interception.resolutionState.priority) {
if (this.interception.resolutionState.action === 'abort') {
return;
}
this.interception.resolutionState.action =
InterceptResolutionAction.Respond;
}
}
/** /**
* Aborts a request. * Aborts a request.
@ -382,7 +521,33 @@ export abstract class HTTPRequest {
* {@link Page.setRequestInterception}. If it is not enabled, this method will * {@link Page.setRequestInterception}. If it is not enabled, this method will
* throw an exception immediately. * throw an exception immediately.
*/ */
abstract abort(errorCode?: ErrorCode, priority?: number): Promise<void>; async abort(
errorCode: ErrorCode = 'failed',
priority?: number
): Promise<void> {
// Request interception is not supported for data: urls.
if (this.url().startsWith('data:')) {
return;
}
const errorReason = errorReasons[errorCode];
assert(errorReason, 'Unknown error code: ' + errorCode);
assert(this.interception.enabled, 'Request Interception is not enabled!');
assert(!this.interception.handled, 'Request is already handled!');
if (priority === undefined) {
return await this._abort(errorReason);
}
this.interception.abortReason = errorReason;
if (
this.interception.resolutionState.priority === undefined ||
priority >= this.interception.resolutionState.priority
) {
this.interception.resolutionState = {
action: InterceptResolutionAction.Abort,
priority,
};
return;
}
}
} }
/** /**
@ -517,6 +682,23 @@ export const STATUS_TEXTS: Record<string, string> = {
'511': 'Network Authentication Required', '511': 'Network Authentication Required',
} as const; } as const;
const errorReasons: Record<ErrorCode, Protocol.Network.ErrorReason> = {
aborted: 'Aborted',
accessdenied: 'AccessDenied',
addressunreachable: 'AddressUnreachable',
blockedbyclient: 'BlockedByClient',
blockedbyresponse: 'BlockedByResponse',
connectionaborted: 'ConnectionAborted',
connectionclosed: 'ConnectionClosed',
connectionfailed: 'ConnectionFailed',
connectionrefused: 'ConnectionRefused',
connectionreset: 'ConnectionReset',
internetdisconnected: 'InternetDisconnected',
namenotresolved: 'NameNotResolved',
timedout: 'TimedOut',
failed: 'Failed',
} as const;
/** /**
* @internal * @internal
*/ */

View File

@ -121,6 +121,7 @@ export class BidiFrame extends Frame {
request.once('error', () => { request.once('error', () => {
this.page().trustedEmitter.emit(PageEvent.RequestFailed, httpRequest); this.page().trustedEmitter.emit(PageEvent.RequestFailed, httpRequest);
}); });
void httpRequest.finalizeInterceptions();
}); });
this.browsingContext.on('navigation', ({navigation}) => { this.browsingContext.on('navigation', ({navigation}) => {

View File

@ -48,6 +48,8 @@ export class BidiHTTPRequest extends HTTPRequest {
super(); super();
requests.set(request, this); requests.set(request, this);
this.interception.enabled = request.isBlocked;
this.#request = request; this.#request = request;
this.#frame = frame; this.#frame = frame;
this.id = request.id; this.id = request.id;
@ -60,6 +62,7 @@ export class BidiHTTPRequest extends HTTPRequest {
#initialize() { #initialize() {
this.#request.on('redirect', request => { this.#request.on('redirect', request => {
this.#redirect = BidiHTTPRequest.from(request, this.#frame); this.#redirect = BidiHTTPRequest.from(request, this.#frame);
void this.#redirect.finalizeInterceptions();
}); });
this.#request.once('success', data => { this.#request.once('success', data => {
this.#response = BidiHTTPResponse.from(data, this); this.#response = BidiHTTPResponse.from(data, this);
@ -132,33 +135,15 @@ export class BidiHTTPRequest extends HTTPRequest {
return redirects; return redirects;
} }
override enqueueInterceptAction(
pendingHandler: () => void | PromiseLike<unknown>
): void {
// Execute the handler when interception is not supported
void pendingHandler();
}
override frame(): BidiFrame | null { override frame(): BidiFrame | null {
return this.#frame ?? null; return this.#frame ?? null;
} }
override continueRequestOverrides(): never { override async _continue(
throw new UnsupportedOperation();
}
override async continue(
overrides: ContinueRequestOverrides = {} overrides: ContinueRequestOverrides = {}
): Promise<void> { ): Promise<void> {
if (!this.#request.isBlocked) {
throw new Error('Request Interception is not enabled!');
}
// Request interception is not supported for data: urls.
if (this.url().startsWith('data:')) {
return;
}
const headers: Bidi.Network.Header[] = getBidiHeaders(overrides.headers); const headers: Bidi.Network.Header[] = getBidiHeaders(overrides.headers);
this.interception.handled = true;
return await this.#request return await this.#request
.continueRequest({ .continueRequest({
@ -173,53 +158,24 @@ export class BidiHTTPRequest extends HTTPRequest {
headers: headers.length > 0 ? headers : undefined, headers: headers.length > 0 ? headers : undefined,
}) })
.catch(error => { .catch(error => {
this.interception.handled = false;
return handleError(error); return handleError(error);
}); });
} }
override responseForRequest(): never { override async _abort(): Promise<void> {
throw new UnsupportedOperation(); this.interception.handled = true;
return await this.#request.failRequest().catch(error => {
this.interception.handled = false;
throw error;
});
} }
override abortErrorReason(): never { override async _respond(
throw new UnsupportedOperation();
}
override interceptResolutionState(): never {
throw new UnsupportedOperation();
}
override isInterceptResolutionHandled(): never {
throw new UnsupportedOperation();
}
override finalizeInterceptions(): never {
throw new UnsupportedOperation();
}
override async abort(): Promise<void> {
if (!this.#request.isBlocked) {
throw new Error('Request Interception is not enabled!');
}
// Request interception is not supported for data: urls.
if (this.url().startsWith('data:')) {
return;
}
return await this.#request.failRequest();
}
override async respond(
response: Partial<ResponseForRequest>, response: Partial<ResponseForRequest>,
_priority?: number _priority?: number
): Promise<void> { ): Promise<void> {
if (!this.#request.isBlocked) { this.interception.handled = true;
throw new Error('Request Interception is not enabled!');
}
// Request interception is not supported for data: urls.
if (this.url().startsWith('data:')) {
return;
}
const responseBody: string | undefined = const responseBody: string | undefined =
response.body && response.body instanceof Uint8Array response.body && response.body instanceof Uint8Array
? response.body.toString('base64') ? response.body.toString('base64')
@ -254,7 +210,8 @@ export class BidiHTTPRequest extends HTTPRequest {
} }
const status = response.status || 200; const status = response.status || 200;
return await this.#request.provideResponse({ return await this.#request
.provideResponse({
statusCode: status, statusCode: status,
headers: headers.length > 0 ? headers : undefined, headers: headers.length > 0 ? headers : undefined,
reasonPhrase: STATUS_TEXTS[status], reasonPhrase: STATUS_TEXTS[status],
@ -264,6 +221,10 @@ export class BidiHTTPRequest extends HTTPRequest {
value: responseBody, value: responseBody,
} }
: undefined, : undefined,
})
.catch(error => {
this.interception.handled = false;
throw error;
}); });
} }
} }

View File

@ -9,18 +9,14 @@ import type {CDPSession} from '../api/CDPSession.js';
import type {Frame} from '../api/Frame.js'; import type {Frame} from '../api/Frame.js';
import { import {
type ContinueRequestOverrides, type ContinueRequestOverrides,
type ErrorCode,
headersArray, headersArray,
HTTPRequest, HTTPRequest,
InterceptResolutionAction,
type InterceptResolutionState,
type ResourceType, type ResourceType,
type ResponseForRequest, type ResponseForRequest,
STATUS_TEXTS, STATUS_TEXTS,
handleError, handleError,
} from '../api/HTTPRequest.js'; } from '../api/HTTPRequest.js';
import {debugError, isString} from '../common/util.js'; import {debugError, isString} from '../common/util.js';
import {assert} from '../util/assert.js';
import type {CdpHTTPResponse} from './HTTPResponse.js'; import type {CdpHTTPResponse} from './HTTPResponse.js';
@ -34,8 +30,7 @@ export class CdpHTTPRequest extends HTTPRequest {
#client: CDPSession; #client: CDPSession;
#isNavigationRequest: boolean; #isNavigationRequest: boolean;
#allowInterception: boolean;
#interceptionHandled = false;
#url: string; #url: string;
#resourceType: ResourceType; #resourceType: ResourceType;
@ -44,13 +39,6 @@ export class CdpHTTPRequest extends HTTPRequest {
#postData?: string; #postData?: string;
#headers: Record<string, string> = {}; #headers: Record<string, string> = {};
#frame: Frame | null; #frame: Frame | null;
#continueRequestOverrides: ContinueRequestOverrides;
#responseForRequest: Partial<ResponseForRequest> | null = null;
#abortErrorReason: Protocol.Network.ErrorReason | null = null;
#interceptResolutionState: InterceptResolutionState = {
action: InterceptResolutionAction.None,
};
#interceptHandlers: Array<() => void | PromiseLike<any>>;
#initiator?: Protocol.Network.Initiator; #initiator?: Protocol.Network.Initiator;
override get client(): CDPSession { override get client(): CDPSession {
@ -96,7 +84,6 @@ export class CdpHTTPRequest extends HTTPRequest {
this.#isNavigationRequest = this.#isNavigationRequest =
data.requestId === data.loaderId && data.type === 'Document'; data.requestId === data.loaderId && data.type === 'Document';
this._interceptionId = interceptionId; this._interceptionId = interceptionId;
this.#allowInterception = allowInterception;
this.#url = data.request.url; this.#url = data.request.url;
this.#resourceType = (data.type || 'other').toLowerCase() as ResourceType; this.#resourceType = (data.type || 'other').toLowerCase() as ResourceType;
this.#method = data.request.method; this.#method = data.request.method;
@ -104,10 +91,10 @@ export class CdpHTTPRequest extends HTTPRequest {
this.#hasPostData = data.request.hasPostData ?? false; this.#hasPostData = data.request.hasPostData ?? false;
this.#frame = frame; this.#frame = frame;
this._redirectChain = redirectChain; this._redirectChain = redirectChain;
this.#continueRequestOverrides = {};
this.#interceptHandlers = [];
this.#initiator = data.initiator; this.#initiator = data.initiator;
this.interception.enabled = allowInterception;
for (const [key, value] of Object.entries(data.request.headers)) { for (const [key, value] of Object.entries(data.request.headers)) {
this.#headers[key.toLowerCase()] = value; this.#headers[key.toLowerCase()] = value;
} }
@ -117,59 +104,6 @@ export class CdpHTTPRequest extends HTTPRequest {
return this.#url; return this.#url;
} }
override continueRequestOverrides(): ContinueRequestOverrides {
assert(this.#allowInterception, 'Request Interception is not enabled!');
return this.#continueRequestOverrides;
}
override responseForRequest(): Partial<ResponseForRequest> | null {
assert(this.#allowInterception, 'Request Interception is not enabled!');
return this.#responseForRequest;
}
override abortErrorReason(): Protocol.Network.ErrorReason | null {
assert(this.#allowInterception, 'Request Interception is not enabled!');
return this.#abortErrorReason;
}
override interceptResolutionState(): InterceptResolutionState {
if (!this.#allowInterception) {
return {action: InterceptResolutionAction.Disabled};
}
if (this.#interceptionHandled) {
return {action: InterceptResolutionAction.AlreadyHandled};
}
return {...this.#interceptResolutionState};
}
override isInterceptResolutionHandled(): boolean {
return this.#interceptionHandled;
}
enqueueInterceptAction(
pendingHandler: () => void | PromiseLike<unknown>
): void {
this.#interceptHandlers.push(pendingHandler);
}
override async finalizeInterceptions(): Promise<void> {
await this.#interceptHandlers.reduce((promiseChain, interceptAction) => {
return promiseChain.then(interceptAction);
}, Promise.resolve());
const {action} = this.interceptResolutionState();
switch (action) {
case 'abort':
return await this.#abort(this.#abortErrorReason);
case 'respond':
if (this.#responseForRequest === null) {
throw new Error('Response is missing for the interception');
}
return await this.#respond(this.#responseForRequest);
case 'continue':
return await this.#continue(this.#continueRequestOverrides);
}
}
override resourceType(): ResourceType { override resourceType(): ResourceType {
return this.#resourceType; return this.#resourceType;
} }
@ -231,46 +165,12 @@ export class CdpHTTPRequest extends HTTPRequest {
}; };
} }
override async continue( /**
overrides: ContinueRequestOverrides = {}, * @internal
priority?: number */
): Promise<void> { async _continue(overrides: ContinueRequestOverrides = {}): Promise<void> {
// Request interception is not supported for data: urls.
if (this.#url.startsWith('data:')) {
return;
}
assert(this.#allowInterception, 'Request Interception is not enabled!');
assert(!this.#interceptionHandled, 'Request is already handled!');
if (priority === undefined) {
return await this.#continue(overrides);
}
this.#continueRequestOverrides = overrides;
if (
this.#interceptResolutionState.priority === undefined ||
priority > this.#interceptResolutionState.priority
) {
this.#interceptResolutionState = {
action: InterceptResolutionAction.Continue,
priority,
};
return;
}
if (priority === this.#interceptResolutionState.priority) {
if (
this.#interceptResolutionState.action === 'abort' ||
this.#interceptResolutionState.action === 'respond'
) {
return;
}
this.#interceptResolutionState.action =
InterceptResolutionAction.Continue;
}
return;
}
async #continue(overrides: ContinueRequestOverrides = {}): Promise<void> {
const {url, method, postData, headers} = overrides; const {url, method, postData, headers} = overrides;
this.#interceptionHandled = true; this.interception.handled = true;
const postDataBinaryBase64 = postData const postDataBinaryBase64 = postData
? Buffer.from(postData).toString('base64') ? Buffer.from(postData).toString('base64')
@ -290,45 +190,13 @@ export class CdpHTTPRequest extends HTTPRequest {
headers: headers ? headersArray(headers) : undefined, headers: headers ? headersArray(headers) : undefined,
}) })
.catch(error => { .catch(error => {
this.#interceptionHandled = false; this.interception.handled = false;
return handleError(error); return handleError(error);
}); });
} }
override async respond( async _respond(response: Partial<ResponseForRequest>): Promise<void> {
response: Partial<ResponseForRequest>, this.interception.handled = true;
priority?: number
): Promise<void> {
// Mocking responses for dataURL requests is not currently supported.
if (this.#url.startsWith('data:')) {
return;
}
assert(this.#allowInterception, 'Request Interception is not enabled!');
assert(!this.#interceptionHandled, 'Request is already handled!');
if (priority === undefined) {
return await this.#respond(response);
}
this.#responseForRequest = response;
if (
this.#interceptResolutionState.priority === undefined ||
priority > this.#interceptResolutionState.priority
) {
this.#interceptResolutionState = {
action: InterceptResolutionAction.Respond,
priority,
};
return;
}
if (priority === this.#interceptResolutionState.priority) {
if (this.#interceptResolutionState.action === 'abort') {
return;
}
this.#interceptResolutionState.action = InterceptResolutionAction.Respond;
}
}
async #respond(response: Partial<ResponseForRequest>): Promise<void> {
this.#interceptionHandled = true;
const responseBody: Buffer | null = const responseBody: Buffer | null =
response.body && isString(response.body) response.body && isString(response.body)
@ -371,43 +239,15 @@ export class CdpHTTPRequest extends HTTPRequest {
body: responseBody ? responseBody.toString('base64') : undefined, body: responseBody ? responseBody.toString('base64') : undefined,
}) })
.catch(error => { .catch(error => {
this.#interceptionHandled = false; this.interception.handled = false;
return handleError(error); return handleError(error);
}); });
} }
override async abort( async _abort(
errorCode: ErrorCode = 'failed',
priority?: number
): Promise<void> {
// Request interception is not supported for data: urls.
if (this.#url.startsWith('data:')) {
return;
}
const errorReason = errorReasons[errorCode];
assert(errorReason, 'Unknown error code: ' + errorCode);
assert(this.#allowInterception, 'Request Interception is not enabled!');
assert(!this.#interceptionHandled, 'Request is already handled!');
if (priority === undefined) {
return await this.#abort(errorReason);
}
this.#abortErrorReason = errorReason;
if (
this.#interceptResolutionState.priority === undefined ||
priority >= this.#interceptResolutionState.priority
) {
this.#interceptResolutionState = {
action: InterceptResolutionAction.Abort,
priority,
};
return;
}
}
async #abort(
errorReason: Protocol.Network.ErrorReason | null errorReason: Protocol.Network.ErrorReason | null
): Promise<void> { ): Promise<void> {
this.#interceptionHandled = true; this.interception.handled = true;
if (this._interceptionId === undefined) { if (this._interceptionId === undefined) {
throw new Error( throw new Error(
'HTTPRequest is missing _interceptionId needed for Fetch.failRequest' 'HTTPRequest is missing _interceptionId needed for Fetch.failRequest'
@ -421,20 +261,3 @@ export class CdpHTTPRequest extends HTTPRequest {
.catch(handleError); .catch(handleError);
} }
} }
const errorReasons: Record<ErrorCode, Protocol.Network.ErrorReason> = {
aborted: 'Aborted',
accessdenied: 'AccessDenied',
addressunreachable: 'AddressUnreachable',
blockedbyclient: 'BlockedByClient',
blockedbyresponse: 'BlockedByResponse',
connectionaborted: 'ConnectionAborted',
connectionclosed: 'ConnectionClosed',
connectionfailed: 'ConnectionFailed',
connectionrefused: 'ConnectionRefused',
connectionreset: 'ConnectionReset',
internetdisconnected: 'InternetDisconnected',
namenotresolved: 'NameNotResolved',
timedout: 'TimedOut',
failed: 'Failed',
} as const;

View File

@ -174,13 +174,6 @@
"expectations": ["SKIP"], "expectations": ["SKIP"],
"comment": "Chrome-specific test" "comment": "Chrome-specific test"
}, },
{
"testIdPattern": "[requestinterception-experimental.spec] *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["SKIP"],
"comment": "TODO: add a comment explaining why this expectation is required (include links to issues)"
},
{ {
"testIdPattern": "[screencast.spec] *", "testIdPattern": "[screencast.spec] *",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -812,12 +805,6 @@
"expectations": ["SKIP"], "expectations": ["SKIP"],
"comment": "TODO: add a comment explaining why this expectation is required (include links to issues)" "comment": "TODO: add a comment explaining why this expectation is required (include links to issues)"
}, },
{
"testIdPattern": "[requestinterception-experimental.spec] *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[requestinterception-experimental.spec] *", "testIdPattern": "[requestinterception-experimental.spec] *",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -825,6 +812,13 @@
"expectations": ["FAIL", "SKIP"], "expectations": ["FAIL", "SKIP"],
"comment": "TODO: add a comment explaining why this expectation is required (include links to issues)" "comment": "TODO: add a comment explaining why this expectation is required (include links to issues)"
}, },
{
"testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Page.setRequestInterception should navigate to URL with hash and fire requests without hash",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"],
"comment": "BiDi spec expect the request to not trim the hash"
},
{ {
"testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Request.continue *", "testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Request.continue *",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -839,6 +833,13 @@
"expectations": ["SKIP"], "expectations": ["SKIP"],
"comment": "TODO: Needs full support for continueResponse in Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1853887" "comment": "TODO: Needs full support for continueResponse in Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1853887"
}, },
{
"testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Request.respond should redirect",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"],
"comment": "Puppeteer request.redirectChain issue"
},
{ {
"testIdPattern": "[requestinterception.spec] *", "testIdPattern": "[requestinterception.spec] *",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -3403,13 +3404,6 @@
"expectations": ["FAIL"], "expectations": ["FAIL"],
"comment": "TODO: add a comment explaining why this expectation is required (include links to issues)" "comment": "TODO: add a comment explaining why this expectation is required (include links to issues)"
}, },
{
"testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Page.setRequestInterception should be able to access the error reason",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["SKIP"],
"comment": "TODO: Needs Puppeteer support for BidiHTTPRequest.abortErrorReason"
},
{ {
"testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Page.setRequestInterception should be able to fetch dataURL and fire dataURL requests", "testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Page.setRequestInterception should be able to fetch dataURL and fire dataURL requests",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -3439,11 +3433,11 @@
"comment": "TODO: Needs support for enabling cache in BiDi without CDP https://github.com/w3c/webdriver-bidi/issues/582" "comment": "TODO: Needs support for enabling cache in BiDi without CDP https://github.com/w3c/webdriver-bidi/issues/582"
}, },
{ {
"testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Page.setRequestInterception should cooperatively abort by priority", "testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Page.setRequestInterception should cache if cache enabled",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"], "parameters": ["chrome", "webDriverBiDi"],
"expectations": ["SKIP"], "expectations": ["FAIL"],
"comment": "TODO: Needs full support for continueRequest in Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1850680" "comment": "TODO: Fixed in the next chromium-bidi release"
}, },
{ {
"testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Page.setRequestInterception should cooperatively continue by priority", "testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Page.setRequestInterception should cooperatively continue by priority",
@ -3480,13 +3474,6 @@
"expectations": ["SKIP"], "expectations": ["SKIP"],
"comment": "TODO: Needs support for data URIs in Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1805176" "comment": "TODO: Needs support for data URIs in Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1805176"
}, },
{
"testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Page.setRequestInterception should navigate to URL with hash and fire requests without hash",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"],
"comment": "TODO: Needs investigation, not clear why the test expects the hash to be removed"
},
{ {
"testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Page.setRequestInterception should not cache if cache disabled", "testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Page.setRequestInterception should not cache if cache disabled",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -3570,6 +3557,13 @@
"parameters": ["firefox", "webDriverBiDi"], "parameters": ["firefox", "webDriverBiDi"],
"expectations": ["PASS"] "expectations": ["PASS"]
}, },
{
"testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Request.respond should redirect",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["SKIP"],
"comment": "Firefox does not support headers in provideResponse"
},
{ {
"testIdPattern": "[requestinterception-experimental.spec] request interception \"after each\" hook in \"request interception\"", "testIdPattern": "[requestinterception-experimental.spec] request interception \"after each\" hook in \"request interception\"",
"platforms": ["win32"], "platforms": ["win32"],
@ -4163,6 +4157,55 @@
"expectations": ["SKIP"], "expectations": ["SKIP"],
"comment": "TODO: add a comment explaining why this expectation is required (include links to issues)" "comment": "TODO: add a comment explaining why this expectation is required (include links to issues)"
}, },
{
"testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Page.setRequestInterception should be abortable with custom error codes",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "headless", "webDriverBiDi"],
"expectations": ["FAIL"],
"comment": "`HTTPRequest.resourceType()` has no eqivalent in BiDi spec"
},
{
"testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Page.setRequestInterception should intercept",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "headless", "webDriverBiDi"],
"expectations": ["FAIL"],
"comment": "`request.postData()` has no eqivalent in BiDi spec"
},
{
"testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Page.setRequestInterception should send referer",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "headless", "webDriverBiDi"],
"expectations": ["FAIL"],
"comment": "`setExtraHTTPHeaders` not implemented"
},
{
"testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Page.setRequestInterception should show custom HTTP headers",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "headless", "webDriverBiDi"],
"expectations": ["FAIL"],
"comment": "`setExtraHTTPHeaders` not implemented"
},
{
"testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Page.setRequestInterception should work with custom referer headers",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "headless", "webDriverBiDi"],
"expectations": ["FAIL"],
"comment": "`setExtraHTTPHeaders` not implemented"
},
{
"testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Page.setRequestInterception should work with redirects",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "headless", "webDriverBiDi"],
"expectations": ["FAIL"],
"comment": "`HTTPRequest.resourceType()` has no eqivalent in BiDi spec"
},
{
"testIdPattern": "[requestinterception-experimental.spec] cooperative request interception Page.setRequestInterception should work with redirects for subresources",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "headless", "webDriverBiDi"],
"expectations": ["FAIL"],
"comment": "`HTTPRequest.resourceType()` has no eqivalent in BiDi spec"
},
{ {
"testIdPattern": "[requestinterception.spec] request interception Page.setRequestInterception should be abortable", "testIdPattern": "[requestinterception.spec] request interception Page.setRequestInterception should be abortable",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],

View File

@ -23,8 +23,7 @@ describe('cooperative request interception', function () {
describe('Page.setRequestInterception', function () { describe('Page.setRequestInterception', function () {
const expectedActions: ActionResult[] = ['abort', 'continue', 'respond']; const expectedActions: ActionResult[] = ['abort', 'continue', 'respond'];
while (expectedActions.length > 0) { for (const expectedAction of expectedActions) {
const expectedAction = expectedActions.pop();
it(`should cooperatively ${expectedAction} by priority`, async () => { it(`should cooperatively ${expectedAction} by priority`, async () => {
const {page, server} = await getTestState(); const {page, server} = await getTestState();