diff options
Diffstat (limited to '')
-rw-r--r-- | remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPRequest.ts | 226 |
1 files changed, 212 insertions, 14 deletions
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPRequest.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPRequest.ts index d72f088686..674abc61f2 100644 --- a/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPRequest.ts +++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPRequest.ts @@ -5,6 +5,10 @@ */ import type {Protocol} from 'devtools-protocol'; +import type {ProtocolError} from '../common/Errors.js'; +import {debugError} from '../common/util.js'; +import {assert} from '../util/assert.js'; + import type {CDPSession} from './CDPSession.js'; import type {Frame} from './Frame.js'; import type {HTTPResponse} from './HTTPResponse.js'; @@ -118,6 +122,29 @@ export abstract class 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. * * @experimental @@ -139,18 +166,27 @@ export abstract class HTTPRequest { * if the interception is allowed to continue (ie, `abort()` and * `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 * 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 */ - 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 @@ -163,13 +199,23 @@ export abstract class HTTPRequest { * InterceptResolutionAction is one of: `abort`, `respond`, `continue`, * `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, * `false` otherwise. */ - abstract isInterceptResolutionHandled(): boolean; + isInterceptResolutionHandled(): boolean { + return this.interception.handled; + } /** * Adds an async request handler to the processing queue. @@ -177,15 +223,51 @@ export abstract class HTTPRequest { * but they are guaranteed to resolve before the request interception * is finalized. */ - abstract enqueueInterceptAction( + enqueueInterceptAction( 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 * 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 @@ -323,10 +405,42 @@ export abstract class HTTPRequest { * * Exception is immediately thrown if the request interception is not enabled. */ - abstract continue( - overrides?: ContinueRequestOverrides, + async continue( + overrides: ContinueRequestOverrides = {}, 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. @@ -360,10 +474,38 @@ export abstract class HTTPRequest { * * Exception is immediately thrown if the request interception is not enabled. */ - abstract respond( + async respond( response: Partial<ResponseForRequest>, 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. @@ -379,7 +521,33 @@ export abstract class HTTPRequest { * {@link Page.setRequestInterception}. If it is not enabled, this method will * 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; + } + } } /** @@ -513,3 +681,33 @@ export const STATUS_TEXTS: Record<string, string> = { '510': 'Not Extended', '511': 'Network Authentication Required', } 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 + */ +export function handleError(error: ProtocolError): void { + if (error.originalMessage.includes('Invalid header')) { + throw error; + } + // In certain cases, protocol will return error if the request was + // already canceled or the page was closed. We should tolerate these + // errors. + debugError(error); +} |