From c944e2643d4a59bef3740610f8f2f0a6c9305cc8 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Wed, 24 Nov 2021 12:56:54 -0800 Subject: [PATCH 01/21] feat: expose intercept resolution state --- docs/api.md | 244 +++++++++++++++--- src/common/HTTPRequest.ts | 83 +++--- test/requestinterception-experimental.spec.ts | 24 +- 3 files changed, 282 insertions(+), 69 deletions(-) diff --git a/docs/api.md b/docs/api.md index af9567d0efda2..8b59b0b1f58e9 100644 --- a/docs/api.md +++ b/docs/api.md @@ -181,7 +181,8 @@ * [page.setJavaScriptEnabled(enabled)](#pagesetjavascriptenabledenabled) * [page.setOfflineMode(enabled)](#pagesetofflinemodeenabled) * [page.setRequestInterception(value)](#pagesetrequestinterceptionvalue) - - [Cooperative Intercept Mode and Legacy Intercept Mode](#cooperative-intercept-mode-and-legacy-intercept-mode) + - [Multiple Intercept Handlers and Asynchronous Resolutions](#multiple-intercept-handlers-and-asynchronous-resolutions) + - [Cooperative Intercept Mode](#cooperative-intercept-mode) - [Upgrading to Cooperative Mode for package maintainers](#upgrading-to-cooperative-mode-for-package-maintainers) * [page.setUserAgent(userAgent[, userAgentMetadata])](#pagesetuseragentuseragent-useragentmetadata) * [page.setViewport(viewport)](#pagesetviewportviewport) @@ -344,6 +345,8 @@ * [httpRequest.frame()](#httprequestframe) * [httpRequest.headers()](#httprequestheaders) * [httpRequest.initiator()](#httprequestinitiator) + * [httpRequest.interceptResolutionState()](#httprequestinterceptresolutionstate) + * [httpRequest.isInterceptResolutionHandled()](#httprequestisinterceptresolutionhandled) * [httpRequest.isNavigationRequest()](#httprequestisnavigationrequest) * [httpRequest.method()](#httprequestmethod) * [httpRequest.postData()](#httprequestpostdata) @@ -2363,6 +2366,7 @@ const puppeteer = require('puppeteer'); const page = await browser.newPage(); await page.setRequestInterception(true); page.on('request', (interceptedRequest) => { + if (interceptedRequest.isInterceptResolutionHandled()) return; if ( interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg') @@ -2375,17 +2379,133 @@ const puppeteer = require('puppeteer'); })(); ``` -##### Cooperative Intercept Mode and Legacy Intercept Mode +##### Multiple Intercept Handlers and Asynchronous Resolutions -`request.respond`, `request.abort`, and `request.continue` can accept an optional `priority` to activate Cooperative Intercept Mode. In Cooperative Mode, all intercept handlers are guaranteed to run and all async handlers are awaited. The interception is resolved to the highest-priority resolution. Here are the rules of Cooperative Mode: +By default Puppeteer will raise a `Request is already handled!` exception if `request.abort`, `request.continue`, or `request.respond` are called after any of them have already been called. +Always assume that an unknown handler may have already called `abort/continue/respond`. Even if your handler is the only one you registered, +3rd party packages may register their own handlers. It is therefore +important to always check the resolution status using [request.isInterceptResolutionHandled](#httprequestisinterceptresolutionhandled) +before calling `abort/continue/respond`. + +Importantly, the intercept resolution may get handled while your handler is awaiting asynchronous operations. Therefore, `request.isInterceptResolutionHandled` must be run **synchronously** before calling `abort/continue/respond` since the return value may change +while awaiting asynchronous operations to resolve. Always check it synchronously before calling `abort/continue/respond`. + +This example demonstrates two synchronous handlers working together: + +```js +/* +This first handler will succeed in calling request.continue because the request interception has never been resolved. +*/ +page.on('request', (interceptedRequest) => { + if (interceptedRequest.isInterceptResolutionHandled()) return; + interceptedRequest.continue(); +}); + +/* +This second handler will return before calling request.abort because request.continue was already +called by the first handler. +*/ +page.on('request', (interceptedRequest) => { + if (interceptedRequest.isInterceptResolutionHandled()) return; + interceptedRequest.abort(); +}); +``` + +This example demonstrates asynchronous handlers working together: + +```js +/* +This first handler will succeed in calling request.continue because the request interception has never been resolved. +*/ +page.on('request', (interceptedRequest) => { + // The interception has not been handled yet. Control will pass through this guard. + if (interceptedRequest.isInterceptResolutionHandled()) return; + + // It is not strictly necessary to return a promise, but doing so will allow Puppeteer to await this handler. + return new Prmomise( resolve=>{ + // Continue after 500ms + setTimeout(()=>{ + // Inside, check synchronously to verify that the intercept wasn't handled already. + // It might have been handled during the 500ms while the other handler awaited an async op of its own. + if (interceptedRequest.isInterceptResolutionHandled()) { + resolve(); + return; + } + interceptedRequest.continue(); + resolve(); + }, 500); + }) +}); +page.on('request', async (interceptedRequest) => { + // The interception has not been handled yet. Control will pass through this guard. + if (interceptedRequest.isInterceptResolutionHandled()) return; + + await someLongAsyncOperation() + // The interception *MIGHT* have been handled by the first handler, we can't be sure. + // Therefore, we must check again before calling continue() or we risk Puppeteer raising an exception. + if (interceptedRequest.isInterceptResolutionHandled()) return; + interceptedRequest.continue(); +}); +``` + +For finer-grained introspection (see Cooperative Intercept Mode below), you may also call [request.interceptResolutionState](#httprequestinterceptresolutionstate) synchronously before using `abort/continue/respond`. + +Here is the example above rewritten using `request.interceptResolutionState` + +```js +/* +This first handler will succeed in calling request.continue because the request interception has never been resolved. +*/ +page.on('request', (interceptedRequest) => { + // The interception has not been handled yet. Control will pass through this guard. + const { strategy } = interceptedRequest.interceptResolutionState(); + if (strategy === 'already-handled') return; + + // It is not strictly necessary to return a promise, but doing so will allow Puppeteer to await this handler. + return new Promise( resolve=> { + // Continue after 500ms + setTimeout(()=>{ + // Inside, check synchronously to verify that the intercept wasn't handled already. + // It might have been handled during the 500ms while the other handler awaited an async op of its own. + const { strategy } = interceptedRequest.interceptResolutionState(); + if (strategy === 'already-handled') { + resolve(); + return; + }; + interceptedRequest.continue(); + resolve(); + }, 500); + }) +}); +page.on('request', async (interceptedRequest) => { + // The interception has not been handled yet. Control will pass through this guard. + if (interceptedRequest.interceptResolutionState().strategy === 'already-handled') return; + + await someLongAsyncOperation() + // The interception *MIGHT* have been handled by the first handler, we can't be sure. + // Therefore, we must check again before calling continue() or we risk Puppeteer raising an exception. + if (interceptedRequest.interceptResolutionState().strategy === 'already-handled') return; + interceptedRequest.continue(); +}); +``` + +##### Cooperative Intercept Mode + +`request.abort`, `request.continue`, and `request.respond` can accept an optional `priority` to work in Cooperative Mode. When all +handlers are using Cooperative Mode, Puppeteer guarantees that all intercept handlers will run and be awaited in order of registration. The interception is resolved to the highest-priority resolution. Here are the rules of Cooperative Mode: + +- All resolutions must supply a numeric `priority` argument to `abort/continue/respond`. +- If any resolution does not supply a numeric `priority`, Legacy Mode is active and Cooperative Mode is inactive. - Async handlers finish before intercept resolution is finalized. - The highest priority interception resolution "wins", i.e. the interception is ultimately aborted/responded/continued according to which resolution was given the highest priority. - In the event of a tie, `abort` > `respond` > `continue`. For standardization, when specifying a Cooperative Mode priority use `0` unless you have a clear reason to use a higher priority. This gracefully prefers `respond` over `continue` and `abort` over `respond`. If you do intentionally want to use a different priority, higher priorities win over lower priorities. Negative priorities are allowed. For example, `continue({}, 4)` would win over `continue({}, -2)`. -To preserve backward compatibility, any handler resolving the intercept without specifying `priority` (Legacy Mode) causes immediate resolution. For Cooperative Mode to work, all resolutions must use a `priority`. +To preserve backward compatibility, any handler resolving the intercept without specifying `priority` (Legacy Mode) causes immediate resolution. For Cooperative Mode to work, all resolutions must use a `priority`. In practice, this means you must still test for +`request.isInterceptResolutionHandled` because a handler beyond your control may have called `abort/continue/respond` without a +priority (Legacy Mode). In this example, Legacy Mode prevails and the request is aborted immediately because at least one handler omits `priority` when resolving the intercept: @@ -2393,17 +2513,16 @@ In this example, Legacy Mode prevails and the request is aborted immediately bec // Final outcome: immediate abort() page.setRequestInterception(true); page.on('request', (request) => { - // Legacy Mode: interception is aborted immediately. + if(request.isInterceptResolutionHandled()) return + +// Legacy Mode: interception is aborted immediately. request.abort('failed'); }); page.on('request', (request) => { - // ['already-handled'], meaning a legacy resolution has taken place - console.log(request.interceptResolution()); + if(request.isInterceptResolutionHandled()) return + // Control will never reach this point because the request was already aborted in Legacy Mode // Cooperative Mode: votes for continue at priority 0. - // Ultimately throws an exception after all handlers have finished - // running and Cooperative Mode resolutions are evaluated becasue - // abort() was called using Legacy Mode. request.continue({}, 0); }); ``` @@ -2414,19 +2533,27 @@ In this example, Legacy Mode prevails and the request is continued because at le // Final outcome: immediate continue() page.setRequestInterception(true); page.on('request', (request) => { + if(request.isInterceptResolutionHandled()) return + // Cooperative Mode: votes to abort at priority 0. - // Ultimately throws an exception after all handlers have finished - // running and Cooperative Mode resolutions are evaluated becasue - // continue() was called using Legacy Mode. request.abort('failed', 0); }); page.on('request', (request) => { - // ['abort', 0], meaning an abort @ 0 is the current winning resolution - console.log(request.interceptResolution()); + if(request.isInterceptResolutionHandled()) return + + // Control reaches this point because the request was cooperatively aborted which postpones resolution. + + // { strategy: 'abort', priority: 0 }, because abort @ 0 is the current winning resolution + console.log(request.interceptResolutionState()); // Legacy Mode: intercept continues immediately. request.continue({}); }); +page.on('request', (request) => { + // { strategy: 'already-handled' }, because continue in Legacy Mode was called + console.log(request.interceptResolutionState()); +}); + ``` In this example, Cooperative Mode is active because all handlers specify a `priority`. `continue()` wins because it has a higher priority than `abort()`. @@ -2435,16 +2562,20 @@ In this example, Cooperative Mode is active because all handlers specify a `prio // Final outcome: cooperative continue() @ 5 page.setRequestInterception(true); page.on('request', (request) => { + if(request.isInterceptResolutionHandled()) return + // Cooperative Mode: votes to abort at priority 10 request.abort('failed', 0); }); page.on('request', (request) => { + if(request.isInterceptResolutionHandled()) return + // Cooperative Mode: votes to continue at priority 5 request.continue(request.continueRequestOverrides(), 5); }); page.on('request', (request) => { - // ['continue', 5], because continue @ 5 > abort @ 0 - console.log(request.interceptResolution()); + // { strategy: 'continue', priority: 5 }, because continue @ 5 > abort @ 0 + console.log(request.interceptResolutionState()); }); ``` @@ -2454,24 +2585,32 @@ In this example, Cooperative Mode is active because all handlers specify `priori // Final outcome: cooperative respond() @ 15 page.setRequestInterception(true); page.on('request', (request) => { + if(request.isInterceptResolutionHandled()) return + // Cooperative Mode: votes to abort at priority 10 request.abort('failed', 10); }); page.on('request', (request) => { + if(request.isInterceptResolutionHandled()) return + // Cooperative Mode: votes to continue at priority 15 request.continue(request.continueRequestOverrides(), 15); }); page.on('request', (request) => { + if(request.isInterceptResolutionHandled()) return + // Cooperative Mode: votes to respond at priority 15 request.respond(request.responseForRequest(), 15); }); page.on('request', (request) => { + if(request.isInterceptResolutionHandled()) return + // Cooperative Mode: votes to respond at priority 12 request.respond(request.responseForRequest(), 12); }); page.on('request', (request) => { - // ['respond', 15], because respond @ 15 > continue @ 15 > respond @ 12 > abort @ 10 - console.log(request.interceptResolution()); + // { strategy: 'respond', priority: 15 }, because respond @ 15 > continue @ 15 > respond @ 12 > abort @ 10 + console.log(request.interceptResolutionState()); }); ``` @@ -2481,6 +2620,7 @@ If you are package maintainer and your package uses intercept handlers, you can ```ts page.on('request', (interceptedRequest) => { + if(request.isInterceptResolutionHandled()) return if ( interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg') @@ -2494,6 +2634,7 @@ To use Cooperative Mode, upgrade `continue()` and `abort()`: ```ts page.on('request', (interceptedRequest) => { + if(request.isInterceptResolutionHandled()) return if ( interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg') @@ -2511,20 +2652,21 @@ With those simple upgrades, your handler now uses Cooperative Mode instead. However, we recommend a slightly more robust solution because the above introduces two subtle issues: -1. **Backward compatibility.** Cooperative Mode resolves interceptions only if no Legacy Mode resolution has taken place. If any handler uses a Legacy Mode resolution (ie, does not specify a priority), that handler will resolve the interception immediately even if your handler runs first. This could cause disconcerting behavior for your users because suddenly your handler is not resolving the interception and a different handler is taking priority when all they did was upgrade your package. +1. **Backward compatibility.** If any handler still uses a Legacy Mode resolution (ie, does not specify a priority), that handler will resolve the interception immediately even if your handler runs first. This could cause disconcerting behavior for your users because suddenly your handler is not resolving the interception and a different handler is taking priority when all the user did was upgrade your package. 2. **Hard-coded priority.** Your package user has no ability to specify the default resolution priority for your handlers. This can become important when the user wishes to manipulate the priorities based on use case. For example, one user might want your package to take a high priority while another user might want it to take a low priority. -To resolve both of these issues, our recommended approach is to export a `setInterceptResolutionStrategy()` from your package. The user can then call `setInterceptResolutionStrategy()` to explicitly activate Cooperative Mode in your package so they aren't surprised by changes in how the interception is resolved. They can also optionally specify a custom priority using `setInterceptResolutionStrategy(priority)` that works for their use case: +To resolve both of these issues, our recommended approach is to export a `setInterceptResolutionConfig()` from your package. The user can then call `setInterceptResolutionConfig()` to explicitly activate Cooperative Mode in your package so they aren't surprised by changes in how the interception is resolved. They can also optionally specify a custom priority using `setInterceptResolutionConfig(priority)` that works for their use case: ```ts // Defaults to undefined which preserves Legacy Mode behavior let _priority = undefined; // Export a module configuration function -export const setInterceptResolutionStrategy = (defaultPriority = 0) => - (_priority = defaultPriority); +export const setInterceptResolutionConfig = (priority = 0) => + (_priority = priority); page.on('request', (interceptedRequest) => { + if(request.isInterceptResolutionHandled()) return if ( interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg') @@ -2541,7 +2683,7 @@ page.on('request', (interceptedRequest) => { If your package calls for more fine-grained control resolution priorities, use a config pattern like this: ```ts -interface ResolutionStrategy { +interface InterceptResolutionConfig { abortPriority: number; continuePriority: number; } @@ -2549,27 +2691,28 @@ interface ResolutionStrategy { // This strategy supports multiple priorities based on situational // differences. You could, for example, create a strategy that // allowed separate priorities for PNG vs JPG. -const DEFAULT_STRATEGY: ResolutionStrategy = { +const DEFAULT_CONFIG: InterceptResolutionConfig = { abortPriority: 0, continuePriority: 0, }; // Defaults to undefined which preserves Legacy Mode behavior -let _strategy: Partial = {}; +let _config: Partial = {}; -export const setInterceptResolutionStrategy = (strategy: ResolutionStrategy) => - (_strategy = { ...DEFAULT_STRATEGY, ...strategy }); +export const setInterceptResolutionConfig = (config: InterceptResolutionConfig) => + (_config = { ...DEFAULT_CONFIG, ...config }); page.on('request', (interceptedRequest) => { + if(request.isInterceptResolutionHandled()) return if ( interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg') ) - interceptedRequest.abort('failed', _strategy.abortPriority); + interceptedRequest.abort('failed', _config.abortPriority); else interceptedRequest.continue( interceptedRequest.continueRequestOverrides(), - _strategy.continuePriority + _config.continuePriority ); }); ``` @@ -4823,6 +4966,47 @@ When in Cooperative Mode, awaits pending interception handlers and then decides - `url` Initiator URL, set for `parser`, `script` and `SignedExchange` type. - `lineNumber` 0 based initiator line number, set for `parser` and `script`. +#### httpRequest.interceptResolutionState() + +- returns: <[InterceptResolutionState]> + - `strategy` <[InterceptResolutionStrategy]> Current resolution strategy. Possible values: `abort`, `respond`, `continue`, + `disabled`, `none`, and `already-handled` + - `priority` The current priority of the winning strategy. + +`InterceptResolutionStrategy` is one of: + +- `abort` - The request will be aborted if no higher priority arises. +- `respond` - The request will be responded if no higher priority arises. +- `continue` - The request will be continued if no higher priority arises. +- `disabled` - Request interception is not currently enabled (see `page.setRequestInterception`). +- `none` - `abort/continue/respond` have not been called yet. +- `already-handled` - The interception has already been handled in Legacy Mode by a call to `abort/continue/respond` with + a `priority` of `undefined`. Subsequent calls to `abort/continue/respond` will throw an exception. + +This example will `continue` a request at a slightly higher priority than the current strategy if the interception has not +already handled and is not already being continued. + +```js +page.on('request', (interceptedRequest) => { + const { strategy, priority } = interceptedRequest.interceptResolutionState(); + if (strategy === 'already-handled') return; + if (strategy === 'continue') return; + + // Change the strategy to `continue` and bump the priority so `continue` becomes the new winner + interceptedRequest.continue( + interceptedRequest.continueRequestOverrides(), + priority + 1 + ); +}); +``` + +#### httpRequest.isInterceptResolutionHandled() + +- returns: <[boolean]> + +Whether this request's interception has been handled (i.e., `abort`, `continue`, or `respond` has already been called +with a `priority` of `undefined`). + #### httpRequest.isNavigationRequest() - returns: <[boolean]> diff --git a/src/common/HTTPRequest.ts b/src/common/HTTPRequest.ts index 0d451594e8def..5543149e77838 100644 --- a/src/common/HTTPRequest.ts +++ b/src/common/HTTPRequest.ts @@ -36,6 +36,14 @@ export interface ContinueRequestOverrides { headers?: Record; } +/** + * @public + */ +export interface InterceptResolutionState { + strategy: InterceptResolutionStrategy; + priority?: number; +} + /** * Required response data to fulfill a request with. * @@ -137,9 +145,8 @@ export class HTTPRequest { private _continueRequestOverrides: ContinueRequestOverrides; private _responseForRequest: Partial; private _abortErrorReason: Protocol.Network.ErrorReason; - private _currentStrategy: InterceptResolutionStrategy; - private _currentPriority: number | undefined; - private _interceptActions: Array<() => void | PromiseLike>; + private _interceptResolutionState: InterceptResolutionState; + private _interceptHandlers: Array<() => void | PromiseLike>; private _initiator: Protocol.Network.Initiator; /** @@ -166,9 +173,8 @@ export class HTTPRequest { this._frame = frame; this._redirectChain = redirectChain; this._continueRequestOverrides = {}; - this._currentStrategy = 'none'; - this._currentPriority = undefined; - this._interceptActions = []; + this._interceptResolutionState = { strategy: 'none' }; + this._interceptHandlers = []; this._initiator = event.initiator; for (const key of Object.keys(event.request.headers)) @@ -211,13 +217,23 @@ export class HTTPRequest { /** * @returns An array of the current intercept resolution strategy and priority - * `[strategy,priority]`. Strategy is one of: `abort`, `respond`, `continue`, + * `[InterceptResolutionStrategy,priority]`. + * + * InterceptResolutionStrategy is one of: `abort`, `respond`, `continue`, * `disabled`, `none`, or `already-handled`. */ - private interceptResolution(): [InterceptResolutionStrategy, number?] { - if (!this._allowInterception) return ['disabled']; - if (this._interceptionHandled) return ['alreay-handled']; - return [this._currentStrategy, this._currentPriority]; + interceptResolutionState(): InterceptResolutionState { + if (!this._allowInterception) return { strategy: 'disabled' }; + if (this._interceptionHandled) return { strategy: 'already-handled' }; + return { ...this._interceptResolutionState }; + } + + /** + * @returns `true` if the intercept resolution has already been handled, + * `false` otherwise. + */ + isInterceptResolutionHandled(): boolean { + return this._interceptionHandled; } /** @@ -229,7 +245,7 @@ export class HTTPRequest { enqueueInterceptAction( pendingHandler: () => void | PromiseLike ): void { - this._interceptActions.push(pendingHandler); + this._interceptHandlers.push(pendingHandler); } /** @@ -237,12 +253,12 @@ export class HTTPRequest { * the request interception. */ async finalizeInterceptions(): Promise { - await this._interceptActions.reduce( + await this._interceptHandlers.reduce( (promiseChain, interceptAction) => promiseChain.then(interceptAction), Promise.resolve() ); - const [resolution] = this.interceptResolution(); - switch (resolution) { + const { strategy } = this.interceptResolutionState(); + switch (strategy) { case 'abort': return this._abort(this._abortErrorReason); case 'respond': @@ -411,21 +427,20 @@ export class HTTPRequest { } this._continueRequestOverrides = overrides; if ( - priority > this._currentPriority || - this._currentPriority === undefined + priority > this._interceptResolutionState.priority || + this._interceptResolutionState.priority === undefined ) { - this._currentStrategy = 'continue'; - this._currentPriority = priority; + this._interceptResolutionState = { strategy: 'continue', priority }; return; } - if (priority === this._currentPriority) { + if (priority === this._interceptResolutionState.priority) { if ( - this._currentStrategy === 'abort' || - this._currentStrategy === 'respond' + this._interceptResolutionState.strategy === 'abort' || + this._interceptResolutionState.strategy === 'respond' ) { return; } - this._currentStrategy = 'continue'; + this._interceptResolutionState.strategy = 'continue'; } return; } @@ -498,18 +513,17 @@ export class HTTPRequest { } this._responseForRequest = response; if ( - priority > this._currentPriority || - this._currentPriority === undefined + priority > this._interceptResolutionState.priority || + this._interceptResolutionState.priority === undefined ) { - this._currentStrategy = 'respond'; - this._currentPriority = priority; + this._interceptResolutionState = { strategy: 'respond', priority }; return; } - if (priority === this._currentPriority) { - if (this._currentStrategy === 'abort') { + if (priority === this._interceptResolutionState.priority) { + if (this._interceptResolutionState.strategy === 'abort') { return; } - this._currentStrategy = 'respond'; + this._interceptResolutionState.strategy = 'respond'; } } @@ -577,11 +591,10 @@ export class HTTPRequest { } this._abortErrorReason = errorReason; if ( - priority >= this._currentPriority || - this._currentPriority === undefined + priority >= this._interceptResolutionState.priority || + this._interceptResolutionState.priority === undefined ) { - this._currentStrategy = 'abort'; - this._currentPriority = priority; + this._interceptResolutionState = { strategy: 'abort', priority }; return; } } @@ -608,7 +621,7 @@ export type InterceptResolutionStrategy = | 'continue' | 'disabled' | 'none' - | 'alreay-handled'; + | 'already-handled'; /** * @public diff --git a/test/requestinterception-experimental.spec.ts b/test/requestinterception-experimental.spec.ts index e73d1102f55ca..60081621ee46a 100644 --- a/test/requestinterception-experimental.spec.ts +++ b/test/requestinterception-experimental.spec.ts @@ -119,10 +119,10 @@ describe('request interception', function () { await page.setRequestInterception(true); page.on('request', (request) => request.continue({}, 0)); await page.setContent(` -
- -
- `); +
+ +
+ `); await Promise.all([ page.$eval('form', (form: HTMLFormElement) => form.submit()), page.waitForNavigation(), @@ -845,6 +845,22 @@ describe('request interception', function () { 'Yo, page!' ); }); + it('should indicate already-handled if an intercept has been handled', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.continue(); + }); + page.on('request', (request) => { + expect(request.isInterceptResolutionHandled()).toBeTruthy(); + }); + page.on('request', (request) => { + const { strategy } = request.interceptResolutionState(); + expect(strategy).toBe('already-handled'); + }); + await page.goto(server.EMPTY_PAGE); + }); }); }); From fc2dc2d64d4b55fdae306e95b8f22a570694d169 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Wed, 24 Nov 2021 13:01:31 -0800 Subject: [PATCH 02/21] chore: comment fix --- src/common/HTTPRequest.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/common/HTTPRequest.ts b/src/common/HTTPRequest.ts index 5543149e77838..ff10463d27a90 100644 --- a/src/common/HTTPRequest.ts +++ b/src/common/HTTPRequest.ts @@ -216,8 +216,12 @@ export class HTTPRequest { } /** - * @returns An array of the current intercept resolution strategy and priority - * `[InterceptResolutionStrategy,priority]`. + * @returns An InterceptResolutionState object describing the current resolution + * strategy and priority. + * + * InterceptResolutionState contains: + * strategy: InterceptResolutionStrategy + * priority?: number * * InterceptResolutionStrategy is one of: `abort`, `respond`, `continue`, * `disabled`, `none`, or `already-handled`. From ce3303c42b04e1083d917a74afb695df0739ad8d Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 25 Nov 2021 02:10:40 -0800 Subject: [PATCH 03/21] chore: strategy to action --- docs/api.md | 40 +++++++++---------- src/common/HTTPRequest.ts | 39 ++++++++++-------- test/requestinterception-experimental.spec.ts | 12 +++--- 3 files changed, 49 insertions(+), 42 deletions(-) diff --git a/docs/api.md b/docs/api.md index 8b59b0b1f58e9..776504fba0456 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2459,8 +2459,8 @@ This first handler will succeed in calling request.continue because the request */ page.on('request', (interceptedRequest) => { // The interception has not been handled yet. Control will pass through this guard. - const { strategy } = interceptedRequest.interceptResolutionState(); - if (strategy === 'already-handled') return; + const { action } = interceptedRequest.interceptResolutionState(); + if (action === 'already-handled') return; // It is not strictly necessary to return a promise, but doing so will allow Puppeteer to await this handler. return new Promise( resolve=> { @@ -2468,8 +2468,8 @@ page.on('request', (interceptedRequest) => { setTimeout(()=>{ // Inside, check synchronously to verify that the intercept wasn't handled already. // It might have been handled during the 500ms while the other handler awaited an async op of its own. - const { strategy } = interceptedRequest.interceptResolutionState(); - if (strategy === 'already-handled') { + const { action } = interceptedRequest.interceptResolutionState(); + if (action === 'already-handled') { resolve(); return; }; @@ -2480,12 +2480,12 @@ page.on('request', (interceptedRequest) => { }); page.on('request', async (interceptedRequest) => { // The interception has not been handled yet. Control will pass through this guard. - if (interceptedRequest.interceptResolutionState().strategy === 'already-handled') return; + if (interceptedRequest.interceptResolutionState().action === 'already-handled') return; await someLongAsyncOperation() // The interception *MIGHT* have been handled by the first handler, we can't be sure. // Therefore, we must check again before calling continue() or we risk Puppeteer raising an exception. - if (interceptedRequest.interceptResolutionState().strategy === 'already-handled') return; + if (interceptedRequest.interceptResolutionState().action === 'already-handled') return; interceptedRequest.continue(); }); ``` @@ -2543,14 +2543,14 @@ page.on('request', (request) => { // Control reaches this point because the request was cooperatively aborted which postpones resolution. - // { strategy: 'abort', priority: 0 }, because abort @ 0 is the current winning resolution + // { action: 'abort', priority: 0 }, because abort @ 0 is the current winning resolution console.log(request.interceptResolutionState()); // Legacy Mode: intercept continues immediately. request.continue({}); }); page.on('request', (request) => { - // { strategy: 'already-handled' }, because continue in Legacy Mode was called + // { action: 'already-handled' }, because continue in Legacy Mode was called console.log(request.interceptResolutionState()); }); @@ -2574,7 +2574,7 @@ page.on('request', (request) => { request.continue(request.continueRequestOverrides(), 5); }); page.on('request', (request) => { - // { strategy: 'continue', priority: 5 }, because continue @ 5 > abort @ 0 + // { action: 'continue', priority: 5 }, because continue @ 5 > abort @ 0 console.log(request.interceptResolutionState()); }); ``` @@ -2609,7 +2609,7 @@ page.on('request', (request) => { request.respond(request.responseForRequest(), 12); }); page.on('request', (request) => { - // { strategy: 'respond', priority: 15 }, because respond @ 15 > continue @ 15 > respond @ 12 > abort @ 10 + // { action: 'respond', priority: 15 }, because respond @ 15 > continue @ 15 > respond @ 12 > abort @ 10 console.log(request.interceptResolutionState()); }); ``` @@ -2688,8 +2688,8 @@ interface InterceptResolutionConfig { continuePriority: number; } -// This strategy supports multiple priorities based on situational -// differences. You could, for example, create a strategy that +// This approach supports multiple priorities based on situational +// differences. You could, for example, create a config that // allowed separate priorities for PNG vs JPG. const DEFAULT_CONFIG: InterceptResolutionConfig = { abortPriority: 0, @@ -4969,11 +4969,11 @@ When in Cooperative Mode, awaits pending interception handlers and then decides #### httpRequest.interceptResolutionState() - returns: <[InterceptResolutionState]> - - `strategy` <[InterceptResolutionStrategy]> Current resolution strategy. Possible values: `abort`, `respond`, `continue`, + - `action` <[InterceptResolutionAction]> Current resolution action. Possible values: `abort`, `respond`, `continue`, `disabled`, `none`, and `already-handled` - - `priority` The current priority of the winning strategy. + - `priority` The current priority of the winning action. -`InterceptResolutionStrategy` is one of: +`InterceptResolutionAction` is one of: - `abort` - The request will be aborted if no higher priority arises. - `respond` - The request will be responded if no higher priority arises. @@ -4983,16 +4983,16 @@ When in Cooperative Mode, awaits pending interception handlers and then decides - `already-handled` - The interception has already been handled in Legacy Mode by a call to `abort/continue/respond` with a `priority` of `undefined`. Subsequent calls to `abort/continue/respond` will throw an exception. -This example will `continue` a request at a slightly higher priority than the current strategy if the interception has not +This example will `continue` a request at a slightly higher priority than the current action if the interception has not already handled and is not already being continued. ```js page.on('request', (interceptedRequest) => { - const { strategy, priority } = interceptedRequest.interceptResolutionState(); - if (strategy === 'already-handled') return; - if (strategy === 'continue') return; + const { action, priority } = interceptedRequest.interceptResolutionState(); + if (action === 'already-handled') return; + if (action === 'continue') return; - // Change the strategy to `continue` and bump the priority so `continue` becomes the new winner + // Change the action to `continue` and bump the priority so `continue` becomes the new winner interceptedRequest.continue( interceptedRequest.continueRequestOverrides(), priority + 1 diff --git a/src/common/HTTPRequest.ts b/src/common/HTTPRequest.ts index ff10463d27a90..59467dc1ec6f6 100644 --- a/src/common/HTTPRequest.ts +++ b/src/common/HTTPRequest.ts @@ -40,7 +40,7 @@ export interface ContinueRequestOverrides { * @public */ export interface InterceptResolutionState { - strategy: InterceptResolutionStrategy; + action: InterceptResolutionAction; priority?: number; } @@ -173,7 +173,7 @@ export class HTTPRequest { this._frame = frame; this._redirectChain = redirectChain; this._continueRequestOverrides = {}; - this._interceptResolutionState = { strategy: 'none' }; + this._interceptResolutionState = { action: 'none' }; this._interceptHandlers = []; this._initiator = event.initiator; @@ -220,15 +220,15 @@ export class HTTPRequest { * strategy and priority. * * InterceptResolutionState contains: - * strategy: InterceptResolutionStrategy + * strategy: InterceptResolutionAction * priority?: number * - * InterceptResolutionStrategy is one of: `abort`, `respond`, `continue`, + * InterceptResolutionAction is one of: `abort`, `respond`, `continue`, * `disabled`, `none`, or `already-handled`. */ interceptResolutionState(): InterceptResolutionState { - if (!this._allowInterception) return { strategy: 'disabled' }; - if (this._interceptionHandled) return { strategy: 'already-handled' }; + if (!this._allowInterception) return { action: 'disabled' }; + if (this._interceptionHandled) return { action: 'already-handled' }; return { ...this._interceptResolutionState }; } @@ -261,7 +261,7 @@ export class HTTPRequest { (promiseChain, interceptAction) => promiseChain.then(interceptAction), Promise.resolve() ); - const { strategy } = this.interceptResolutionState(); + const { action: strategy } = this.interceptResolutionState(); switch (strategy) { case 'abort': return this._abort(this._abortErrorReason); @@ -434,17 +434,17 @@ export class HTTPRequest { priority > this._interceptResolutionState.priority || this._interceptResolutionState.priority === undefined ) { - this._interceptResolutionState = { strategy: 'continue', priority }; + this._interceptResolutionState = { action: 'continue', priority }; return; } if (priority === this._interceptResolutionState.priority) { if ( - this._interceptResolutionState.strategy === 'abort' || - this._interceptResolutionState.strategy === 'respond' + this._interceptResolutionState.action === 'abort' || + this._interceptResolutionState.action === 'respond' ) { return; } - this._interceptResolutionState.strategy = 'continue'; + this._interceptResolutionState.action = 'continue'; } return; } @@ -520,14 +520,14 @@ export class HTTPRequest { priority > this._interceptResolutionState.priority || this._interceptResolutionState.priority === undefined ) { - this._interceptResolutionState = { strategy: 'respond', priority }; + this._interceptResolutionState = { action: 'respond', priority }; return; } if (priority === this._interceptResolutionState.priority) { - if (this._interceptResolutionState.strategy === 'abort') { + if (this._interceptResolutionState.action === 'abort') { return; } - this._interceptResolutionState.strategy = 'respond'; + this._interceptResolutionState.action = 'respond'; } } @@ -598,7 +598,7 @@ export class HTTPRequest { priority >= this._interceptResolutionState.priority || this._interceptResolutionState.priority === undefined ) { - this._interceptResolutionState = { strategy: 'abort', priority }; + this._interceptResolutionState = { action: 'abort', priority }; return; } } @@ -619,7 +619,7 @@ export class HTTPRequest { /** * @public */ -export type InterceptResolutionStrategy = +export type InterceptResolutionAction = | 'abort' | 'respond' | 'continue' @@ -627,6 +627,13 @@ export type InterceptResolutionStrategy = | 'none' | 'already-handled'; +/** + * @public + * + * Deprecate ASAP + */ +export type InterceptResolutionStrategy = InterceptResolutionAction; + /** * @public */ diff --git a/test/requestinterception-experimental.spec.ts b/test/requestinterception-experimental.spec.ts index 60081621ee46a..b47cc13b14e3f 100644 --- a/test/requestinterception-experimental.spec.ts +++ b/test/requestinterception-experimental.spec.ts @@ -119,10 +119,10 @@ describe('request interception', function () { await page.setRequestInterception(true); page.on('request', (request) => request.continue({}, 0)); await page.setContent(` -
- -
- `); +
+ +
+ `); await Promise.all([ page.$eval('form', (form: HTMLFormElement) => form.submit()), page.waitForNavigation(), @@ -856,8 +856,8 @@ describe('request interception', function () { expect(request.isInterceptResolutionHandled()).toBeTruthy(); }); page.on('request', (request) => { - const { strategy } = request.interceptResolutionState(); - expect(strategy).toBe('already-handled'); + const { action } = request.interceptResolutionState(); + expect(action).toBe('already-handled'); }); await page.goto(server.EMPTY_PAGE); }); From e51721e25fb185b0a36e19a80f8e31285fb7becd Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 25 Nov 2021 02:15:58 -0800 Subject: [PATCH 04/21] Update docs/api.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Fernández --- docs/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 776504fba0456..1be97107eaede 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2513,7 +2513,7 @@ In this example, Legacy Mode prevails and the request is aborted immediately bec // Final outcome: immediate abort() page.setRequestInterception(true); page.on('request', (request) => { - if(request.isInterceptResolutionHandled()) return + if (request.isInterceptResolutionHandled()) return // Legacy Mode: interception is aborted immediately. request.abort('failed'); From e1bcf8075e025b9ec42829ca1bee9d69611124a5 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 25 Nov 2021 02:16:28 -0800 Subject: [PATCH 05/21] Update docs/api.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Fernández --- docs/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 1be97107eaede..848deaf99a47f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2425,7 +2425,7 @@ page.on('request', (interceptedRequest) => { // It is not strictly necessary to return a promise, but doing so will allow Puppeteer to await this handler. return new Prmomise( resolve=>{ // Continue after 500ms - setTimeout(()=>{ + setTimeout(() => { // Inside, check synchronously to verify that the intercept wasn't handled already. // It might have been handled during the 500ms while the other handler awaited an async op of its own. if (interceptedRequest.isInterceptResolutionHandled()) { From 354505e51dd18a3048df7b7a7f4fae6b6ca294d8 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 25 Nov 2021 02:16:36 -0800 Subject: [PATCH 06/21] Update docs/api.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Fernández --- docs/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 848deaf99a47f..7b8d69ed8d043 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2519,7 +2519,7 @@ page.on('request', (request) => { request.abort('failed'); }); page.on('request', (request) => { - if(request.isInterceptResolutionHandled()) return + if (request.isInterceptResolutionHandled()) return // Control will never reach this point because the request was already aborted in Legacy Mode // Cooperative Mode: votes for continue at priority 0. From 3663436427134ee6da47e345b56e8719c9d62bd1 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 25 Nov 2021 02:16:41 -0800 Subject: [PATCH 07/21] Update docs/api.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Fernández --- docs/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 7b8d69ed8d043..17b559d51f164 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2515,7 +2515,7 @@ page.setRequestInterception(true); page.on('request', (request) => { if (request.isInterceptResolutionHandled()) return -// Legacy Mode: interception is aborted immediately. + // Legacy Mode: interception is aborted immediately. request.abort('failed'); }); page.on('request', (request) => { From 1284decb119156c313747f89812c92cb1cb98157 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 25 Nov 2021 02:16:48 -0800 Subject: [PATCH 08/21] Update docs/api.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Fernández --- docs/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 17b559d51f164..14af54141b0ca 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2423,7 +2423,7 @@ page.on('request', (interceptedRequest) => { if (interceptedRequest.isInterceptResolutionHandled()) return; // It is not strictly necessary to return a promise, but doing so will allow Puppeteer to await this handler. - return new Prmomise( resolve=>{ + return new Promise(resolve => { // Continue after 500ms setTimeout(() => { // Inside, check synchronously to verify that the intercept wasn't handled already. From 44b2cf32fb23db5bce2db45d1864e65ac6977a0f Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 25 Nov 2021 02:16:54 -0800 Subject: [PATCH 09/21] Update docs/api.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Fernández --- docs/api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api.md b/docs/api.md index 14af54141b0ca..a4f70dc9002a4 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2463,9 +2463,9 @@ page.on('request', (interceptedRequest) => { if (action === 'already-handled') return; // It is not strictly necessary to return a promise, but doing so will allow Puppeteer to await this handler. - return new Promise( resolve=> { + return new Promise(resolve => { // Continue after 500ms - setTimeout(()=>{ + setTimeout(() => { // Inside, check synchronously to verify that the intercept wasn't handled already. // It might have been handled during the 500ms while the other handler awaited an async op of its own. const { action } = interceptedRequest.interceptResolutionState(); From 91abf11cc672af7d7bc7dda59d2016616b6de6e6 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 25 Nov 2021 02:17:43 -0800 Subject: [PATCH 10/21] chore: formatting --- docs/api.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/api.md b/docs/api.md index 776504fba0456..1848725eae903 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2423,9 +2423,9 @@ page.on('request', (interceptedRequest) => { if (interceptedRequest.isInterceptResolutionHandled()) return; // It is not strictly necessary to return a promise, but doing so will allow Puppeteer to await this handler. - return new Prmomise( resolve=>{ + return new Promise( resolve => { // Continue after 500ms - setTimeout(()=>{ + setTimeout(() => { // Inside, check synchronously to verify that the intercept wasn't handled already. // It might have been handled during the 500ms while the other handler awaited an async op of its own. if (interceptedRequest.isInterceptResolutionHandled()) { @@ -2465,7 +2465,7 @@ page.on('request', (interceptedRequest) => { // It is not strictly necessary to return a promise, but doing so will allow Puppeteer to await this handler. return new Promise( resolve=> { // Continue after 500ms - setTimeout(()=>{ + setTimeout(() => { // Inside, check synchronously to verify that the intercept wasn't handled already. // It might have been handled during the 500ms while the other handler awaited an async op of its own. const { action } = interceptedRequest.interceptResolutionState(); @@ -2513,13 +2513,13 @@ In this example, Legacy Mode prevails and the request is aborted immediately bec // Final outcome: immediate abort() page.setRequestInterception(true); page.on('request', (request) => { - if(request.isInterceptResolutionHandled()) return + if (request.isInterceptResolutionHandled()) return -// Legacy Mode: interception is aborted immediately. + // Legacy Mode: interception is aborted immediately. request.abort('failed'); }); page.on('request', (request) => { - if(request.isInterceptResolutionHandled()) return + if (request.isInterceptResolutionHandled()) return // Control will never reach this point because the request was already aborted in Legacy Mode // Cooperative Mode: votes for continue at priority 0. @@ -2533,13 +2533,13 @@ In this example, Legacy Mode prevails and the request is continued because at le // Final outcome: immediate continue() page.setRequestInterception(true); page.on('request', (request) => { - if(request.isInterceptResolutionHandled()) return + if (request.isInterceptResolutionHandled()) return // Cooperative Mode: votes to abort at priority 0. request.abort('failed', 0); }); page.on('request', (request) => { - if(request.isInterceptResolutionHandled()) return + if (request.isInterceptResolutionHandled()) return // Control reaches this point because the request was cooperatively aborted which postpones resolution. @@ -2562,13 +2562,13 @@ In this example, Cooperative Mode is active because all handlers specify a `prio // Final outcome: cooperative continue() @ 5 page.setRequestInterception(true); page.on('request', (request) => { - if(request.isInterceptResolutionHandled()) return + if (request.isInterceptResolutionHandled()) return // Cooperative Mode: votes to abort at priority 10 request.abort('failed', 0); }); page.on('request', (request) => { - if(request.isInterceptResolutionHandled()) return + if (request.isInterceptResolutionHandled()) return // Cooperative Mode: votes to continue at priority 5 request.continue(request.continueRequestOverrides(), 5); @@ -2585,25 +2585,25 @@ In this example, Cooperative Mode is active because all handlers specify `priori // Final outcome: cooperative respond() @ 15 page.setRequestInterception(true); page.on('request', (request) => { - if(request.isInterceptResolutionHandled()) return + if (request.isInterceptResolutionHandled()) return // Cooperative Mode: votes to abort at priority 10 request.abort('failed', 10); }); page.on('request', (request) => { - if(request.isInterceptResolutionHandled()) return + if (request.isInterceptResolutionHandled()) return // Cooperative Mode: votes to continue at priority 15 request.continue(request.continueRequestOverrides(), 15); }); page.on('request', (request) => { - if(request.isInterceptResolutionHandled()) return + if (request.isInterceptResolutionHandled()) return // Cooperative Mode: votes to respond at priority 15 request.respond(request.responseForRequest(), 15); }); page.on('request', (request) => { - if(request.isInterceptResolutionHandled()) return + if (request.isInterceptResolutionHandled()) return // Cooperative Mode: votes to respond at priority 12 request.respond(request.responseForRequest(), 12); From a191f95b799e2af0a688fe202e50f8151aaab0fb Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 25 Nov 2021 02:22:41 -0800 Subject: [PATCH 11/21] chore: rename --- src/common/HTTPRequest.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/common/HTTPRequest.ts b/src/common/HTTPRequest.ts index 59467dc1ec6f6..bae80ea8f70b6 100644 --- a/src/common/HTTPRequest.ts +++ b/src/common/HTTPRequest.ts @@ -217,10 +217,10 @@ export class HTTPRequest { /** * @returns An InterceptResolutionState object describing the current resolution - * strategy and priority. + * action and priority. * * InterceptResolutionState contains: - * strategy: InterceptResolutionAction + * action: InterceptResolutionAction * priority?: number * * InterceptResolutionAction is one of: `abort`, `respond`, `continue`, @@ -261,8 +261,8 @@ export class HTTPRequest { (promiseChain, interceptAction) => promiseChain.then(interceptAction), Promise.resolve() ); - const { action: strategy } = this.interceptResolutionState(); - switch (strategy) { + const { action } = this.interceptResolutionState(); + switch (action) { case 'abort': return this._abort(this._abortErrorReason); case 'respond': From bf183bf424b31be7485da485be49f3ed00278e86 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 25 Nov 2021 02:39:23 -0800 Subject: [PATCH 12/21] chore: docs --- docs/api.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/api.md b/docs/api.md index fe4456364790a..e84e48a9c0062 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2388,8 +2388,7 @@ Always assume that an unknown handler may have already called `abort/continue/re important to always check the resolution status using [request.isInterceptResolutionHandled](#httprequestisinterceptresolutionhandled) before calling `abort/continue/respond`. -Importantly, the intercept resolution may get handled while your handler is awaiting asynchronous operations. Therefore, `request.isInterceptResolutionHandled` must be run **synchronously** before calling `abort/continue/respond` since the return value may change -while awaiting asynchronous operations to resolve. Always check it synchronously before calling `abort/continue/respond`. +Importantly, the intercept resolution may get handled by another listener while your handler is awaiting an asynchronous operation. Therefore, the return value of `request.isInterceptResolutionHandled` is only safe in a synchronous code block. Always execute `request.isInterceptResolutionHandled` and `abort/continue/respond` **synchronously** together. This example demonstrates two synchronous handlers working together: From 54c99ce8bbe5a3c20e5d07c97892eee09b86b058 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 25 Nov 2021 02:49:04 -0800 Subject: [PATCH 13/21] chore: docs --- docs/api.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/api.md b/docs/api.md index e84e48a9c0062..27eb58cbbd64d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2619,7 +2619,7 @@ If you are package maintainer and your package uses intercept handlers, you can ```ts page.on('request', (interceptedRequest) => { - if(request.isInterceptResolutionHandled()) return + if (request.isInterceptResolutionHandled()) return if ( interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg') @@ -2633,7 +2633,7 @@ To use Cooperative Mode, upgrade `continue()` and `abort()`: ```ts page.on('request', (interceptedRequest) => { - if(request.isInterceptResolutionHandled()) return + if (request.isInterceptResolutionHandled()) return if ( interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg') @@ -2665,7 +2665,7 @@ export const setInterceptResolutionConfig = (priority = 0) => (_priority = priority); page.on('request', (interceptedRequest) => { - if(request.isInterceptResolutionHandled()) return + if (request.isInterceptResolutionHandled()) return if ( interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg') @@ -2702,7 +2702,7 @@ export const setInterceptResolutionConfig = (config: InterceptResolutionConfig) (_config = { ...DEFAULT_CONFIG, ...config }); page.on('request', (interceptedRequest) => { - if(request.isInterceptResolutionHandled()) return + if (request.isInterceptResolutionHandled()) return if ( interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg') @@ -4902,7 +4902,9 @@ Exception is immediately thrown if the request interception is not enabled. ```js await page.setRequestInterception(true); page.on('request', (request) => { - // Override headers + if (request.isInterceptResolutionHandled()) return; + +// Override headers const headers = Object.assign({}, request.headers(), { foo: 'bar', // set "foo" header origin: undefined, // remove "origin" header @@ -5076,6 +5078,8 @@ An example of fulfilling all requests with 404 responses: ```js await page.setRequestInterception(true); page.on('request', (request) => { + if (request.isInterceptResolutionHandled()) return; + request.respond({ status: 404, contentType: 'text/plain', From 942bdfff948751050ba919ff2ceb07324ec3b132 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 25 Nov 2021 04:27:35 -0800 Subject: [PATCH 14/21] chore: docs & default priority --- docs/api.md | 110 ++++++++++++++++++++++++-------------- src/common/HTTPRequest.ts | 7 +++ 2 files changed, 77 insertions(+), 40 deletions(-) diff --git a/docs/api.md b/docs/api.md index 27eb58cbbd64d..1f76830bae49a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -183,7 +183,7 @@ * [page.setRequestInterception(value)](#pagesetrequestinterceptionvalue) - [Multiple Intercept Handlers and Asynchronous Resolutions](#multiple-intercept-handlers-and-asynchronous-resolutions) - [Cooperative Intercept Mode](#cooperative-intercept-mode) - - [Upgrading to Cooperative Mode for package maintainers](#upgrading-to-cooperative-mode-for-package-maintainers) + - [Upgrading to Cooperative Intercept Mode for package maintainers](#upgrading-to-cooperative-intercept-mode-for-package-maintainers) * [page.setUserAgent(userAgent[, userAgentMetadata])](#pagesetuseragentuseragent-useragentmetadata) * [page.setViewport(viewport)](#pagesetviewportviewport) * [page.tap(selector)](#pagetapselector) @@ -2491,18 +2491,18 @@ page.on('request', async (interceptedRequest) => { ##### Cooperative Intercept Mode -`request.abort`, `request.continue`, and `request.respond` can accept an optional `priority` to work in Cooperative Mode. When all -handlers are using Cooperative Mode, Puppeteer guarantees that all intercept handlers will run and be awaited in order of registration. The interception is resolved to the highest-priority resolution. Here are the rules of Cooperative Mode: +`request.abort`, `request.continue`, and `request.respond` can accept an optional `priority` to work in Cooperative Intercept Mode. When all +handlers are using Cooperative Intercept Mode, Puppeteer guarantees that all intercept handlers will run and be awaited in order of registration. The interception is resolved to the highest-priority resolution. Here are the rules of Cooperative Intercept Mode: - All resolutions must supply a numeric `priority` argument to `abort/continue/respond`. -- If any resolution does not supply a numeric `priority`, Legacy Mode is active and Cooperative Mode is inactive. +- If any resolution does not supply a numeric `priority`, Legacy Mode is active and Cooperative Intercept Mode is inactive. - Async handlers finish before intercept resolution is finalized. - The highest priority interception resolution "wins", i.e. the interception is ultimately aborted/responded/continued according to which resolution was given the highest priority. - In the event of a tie, `abort` > `respond` > `continue`. -For standardization, when specifying a Cooperative Mode priority use `0` unless you have a clear reason to use a higher priority. This gracefully prefers `respond` over `continue` and `abort` over `respond`. If you do intentionally want to use a different priority, higher priorities win over lower priorities. Negative priorities are allowed. For example, `continue({}, 4)` would win over `continue({}, -2)`. +For standardization, when specifying a Cooperative Intercept Mode priority use `0` or `DEFAULT_INTERCEPT_RESOLUTION_PRIORITY` (exported from `HTTPRequest`) unless you have a clear reason to use a higher priority. This gracefully prefers `respond` over `continue` and `abort` over `respond` and allows other handlers to work cooperatively. If you do intentionally want to use a different priority, higher priorities win over lower priorities. Negative priorities are allowed. For example, `continue({}, 4)` would win over `continue({}, -2)`. -To preserve backward compatibility, any handler resolving the intercept without specifying `priority` (Legacy Mode) causes immediate resolution. For Cooperative Mode to work, all resolutions must use a `priority`. In practice, this means you must still test for +To preserve backward compatibility, any handler resolving the intercept without specifying `priority` (Legacy Mode) causes immediate resolution. For Cooperative Intercept Mode to work, all resolutions must use a `priority`. In practice, this means you must still test for `request.isInterceptResolutionHandled` because a handler beyond your control may have called `abort/continue/respond` without a priority (Legacy Mode). @@ -2521,7 +2521,7 @@ page.on('request', (request) => { if (request.isInterceptResolutionHandled()) return // Control will never reach this point because the request was already aborted in Legacy Mode - // Cooperative Mode: votes for continue at priority 0. + // Cooperative Intercept Mode: votes for continue at priority 0. request.continue({}, 0); }); ``` @@ -2534,7 +2534,7 @@ page.setRequestInterception(true); page.on('request', (request) => { if (request.isInterceptResolutionHandled()) return - // Cooperative Mode: votes to abort at priority 0. + // Cooperative Intercept Mode: votes to abort at priority 0. request.abort('failed', 0); }); page.on('request', (request) => { @@ -2555,7 +2555,7 @@ page.on('request', (request) => { ``` -In this example, Cooperative Mode is active because all handlers specify a `priority`. `continue()` wins because it has a higher priority than `abort()`. +In this example, Cooperative Intercept Mode is active because all handlers specify a `priority`. `continue()` wins because it has a higher priority than `abort()`. ```ts // Final outcome: cooperative continue() @ 5 @@ -2563,13 +2563,13 @@ page.setRequestInterception(true); page.on('request', (request) => { if (request.isInterceptResolutionHandled()) return - // Cooperative Mode: votes to abort at priority 10 + // Cooperative Intercept Mode: votes to abort at priority 10 request.abort('failed', 0); }); page.on('request', (request) => { if (request.isInterceptResolutionHandled()) return - // Cooperative Mode: votes to continue at priority 5 + // Cooperative Intercept Mode: votes to continue at priority 5 request.continue(request.continueRequestOverrides(), 5); }); page.on('request', (request) => { @@ -2578,7 +2578,7 @@ page.on('request', (request) => { }); ``` -In this example, Cooperative Mode is active because all handlers specify `priority`. `respond()` wins because its priority ties with `continue()`, but `respond()` beats `continue()`. +In this example, Cooperative Intercept Mode is active because all handlers specify `priority`. `respond()` wins because its priority ties with `continue()`, but `respond()` beats `continue()`. ```ts // Final outcome: cooperative respond() @ 15 @@ -2586,25 +2586,25 @@ page.setRequestInterception(true); page.on('request', (request) => { if (request.isInterceptResolutionHandled()) return - // Cooperative Mode: votes to abort at priority 10 + // Cooperative Intercept Mode: votes to abort at priority 10 request.abort('failed', 10); }); page.on('request', (request) => { if (request.isInterceptResolutionHandled()) return - // Cooperative Mode: votes to continue at priority 15 + // Cooperative Intercept Mode: votes to continue at priority 15 request.continue(request.continueRequestOverrides(), 15); }); page.on('request', (request) => { if (request.isInterceptResolutionHandled()) return - // Cooperative Mode: votes to respond at priority 15 + // Cooperative Intercept Mode: votes to respond at priority 15 request.respond(request.responseForRequest(), 15); }); page.on('request', (request) => { if (request.isInterceptResolutionHandled()) return - // Cooperative Mode: votes to respond at priority 12 + // Cooperative Intercept Mode: votes to respond at priority 12 request.respond(request.responseForRequest(), 12); }); page.on('request', (request) => { @@ -2613,9 +2613,25 @@ page.on('request', (request) => { }); ``` -##### Upgrading to Cooperative Mode for package maintainers +##### Discussion: Cooperative reuest continuation -If you are package maintainer and your package uses intercept handlers, you can update your intercept handlers to use Cooperative Mode. Suppose you have the following existing handler: +Puppeteer requires `request.continue` to be called explicitly or the request will hang. Even if +your handler means to take no special action, ou 'opt out', `request.continue` must still be called. + +With the introduction of Cooperative Intercept Mode, two use cases arise for cooperative request continuations: +Unopinionated and Opinionated. + +The first case (common) is that your handler means to opt out of doing anything special the request. It has no opinion on further action and simply intends to continue by default and/or defer to other handlers that might have an opinion. But in case there are no other handlers, we must call `request.continue` to ensure that the request doesn't hang. + +We call this an **Unopinionated continuation** because the intent is to continue the request if nobody else has a better idea. Use `request.continue({...}, DEFAULT_INTERCEPT_RESOLUTION_PRIORITY)` (or `0`) for this type of continuation. + +The second case (uncommon) is that your handler actually does have an opinion and means to force continuation by overriding a lower-priority `abort` or `respond` issued elsewhere. We call this an **Opinionated continuation**. In these rare cases where you mean to specify an overriding continuation priority, use a custom priority. + +To summarize, reason through whether your use of `request.continue` is just meant to be default/bypass behavior vs falling within the intended use case of your handler. Consider using a custom priority for in-scope use cases, and a default priority otherwise. Be aware that your handler may have both Opinionated and Unopinionated cases. + +##### Upgrading to Cooperative Intercept Mode for package maintainers + +If you are package maintainer and your package uses intercept handlers, you can update your intercept handlers to use Cooperative Intercept Mode. Suppose you have the following existing handler: ```ts page.on('request', (interceptedRequest) => { @@ -2629,7 +2645,7 @@ page.on('request', (interceptedRequest) => { }); ``` -To use Cooperative Mode, upgrade `continue()` and `abort()`: +To use Cooperative Intercept Mode, upgrade `continue()` and `abort()`: ```ts page.on('request', (interceptedRequest) => { @@ -2647,14 +2663,14 @@ page.on('request', (interceptedRequest) => { }); ``` -With those simple upgrades, your handler now uses Cooperative Mode instead. +With those simple upgrades, your handler now uses Cooperative Intercept Mode instead. -However, we recommend a slightly more robust solution because the above introduces two subtle issues: +However, we recommend a slightly more robust solution because the above introduces several subtle issues: 1. **Backward compatibility.** If any handler still uses a Legacy Mode resolution (ie, does not specify a priority), that handler will resolve the interception immediately even if your handler runs first. This could cause disconcerting behavior for your users because suddenly your handler is not resolving the interception and a different handler is taking priority when all the user did was upgrade your package. 2. **Hard-coded priority.** Your package user has no ability to specify the default resolution priority for your handlers. This can become important when the user wishes to manipulate the priorities based on use case. For example, one user might want your package to take a high priority while another user might want it to take a low priority. -To resolve both of these issues, our recommended approach is to export a `setInterceptResolutionConfig()` from your package. The user can then call `setInterceptResolutionConfig()` to explicitly activate Cooperative Mode in your package so they aren't surprised by changes in how the interception is resolved. They can also optionally specify a custom priority using `setInterceptResolutionConfig(priority)` that works for their use case: +To resolve both of these issues, our recommended approach is to export a `setInterceptResolutionConfig()` from your package. The user can then call `setInterceptResolutionConfig()` to explicitly activate Cooperative Intercept Mode in your package so they aren't surprised by changes in how the interception is resolved. They can also optionally specify a custom priority using `setInterceptResolutionConfig(priority)` that works for their use case: ```ts // Defaults to undefined which preserves Legacy Mode behavior @@ -2664,6 +2680,10 @@ let _priority = undefined; export const setInterceptResolutionConfig = (priority = 0) => (_priority = priority); +/** + * Note that this handler uses `DEFAULT_INTERCEPT_RESOLUTION_PRIORITY` to "pass" on this request. It is important to use + * the default priority when your handler has no opinion on the request and the intent is to continue() by default. + */ page.on('request', (interceptedRequest) => { if (request.isInterceptResolutionHandled()) return if ( @@ -2674,25 +2694,25 @@ page.on('request', (interceptedRequest) => { else interceptedRequest.continue( interceptedRequest.continueRequestOverrides(), - _priority + DEFAULT_INTERCEPT_RESOLUTION_PRIORITY // Unopinionated continuation ); }); ``` -If your package calls for more fine-grained control resolution priorities, use a config pattern like this: +If your package calls for more fine-grained control over resolution priorities, use a config pattern like this: ```ts interface InterceptResolutionConfig { - abortPriority: number; - continuePriority: number; + abortPriority?: number; + continuePriority?: number; } // This approach supports multiple priorities based on situational // differences. You could, for example, create a config that // allowed separate priorities for PNG vs JPG. const DEFAULT_CONFIG: InterceptResolutionConfig = { - abortPriority: 0, - continuePriority: 0, + abortPriority: undefined, // Default to Legacy Mode + continuePriority: undefined, // Default to Legacy Mode }; // Defaults to undefined which preserves Legacy Mode behavior @@ -2706,17 +2726,24 @@ page.on('request', (interceptedRequest) => { if ( interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg') - ) - interceptedRequest.abort('failed', _config.abortPriority); - else - interceptedRequest.continue( - interceptedRequest.continueRequestOverrides(), - _config.continuePriority - ); + ) { + interceptedRequest.abort('failed', _config.abortPriority); + } + else + { + // Here we use a custom-configured priority to allow for Opinionated + // continuation. + // We would only want to allow this if we had a very clear reason why + // some use cases required Opinionated continuation. + interceptedRequest.continue( + interceptedRequest.continueRequestOverrides(), + _config.continuePriority // Why would we ever want priority!==0 here? + ); + } }); ``` -The above solution ensures backward compatibility while also allowing the user to adjust the importance of your package in the resolution chain when Cooperative Mode is being used. Your package continues to work as expected until the user has fully upgraded their code and all third party packages to use Cooperative Mode. If any handler or package still uses Legacy Mode, your package can still operate in Legacy Mode too. +The above solutions ensures backward compatibility while also allowing the user to adjust the importance of your package in the resolution chain when Cooperative Intercept Mode is being used. Your package continues to work as expected until the user has fully upgraded their code and all third party packages to use Cooperative Intercept Mode. If any handler or package still uses Legacy Mode, your package can still operate in Legacy Mode too. #### page.setUserAgent(userAgent[, userAgentMetadata]) @@ -4884,7 +4911,7 @@ Exception is immediately thrown if the request interception is not enabled. - returns: <[string]> of type [Protocol.Network.ErrorReason](https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-ErrorReason). -Returns the most recent reason for aborting set by the previous call to abort() in Cooperative Mode. +Returns the most recent reason for aborting set by the previous call to abort() in Cooperative Intercept Mode. #### httpRequest.continue([overrides], [priority]) @@ -4899,6 +4926,9 @@ Returns the most recent reason for aborting set by the previous call to abort() Continues request with optional request overrides. To use this, request interception should be enabled with `page.setRequestInterception`. Exception is immediately thrown if the request interception is not enabled. +Note: Pass `DEFAULT_INTERCEPT_RESOLUTION_PRIORITY` to continue a request if your intent is to bypass/defer the request because +your handler has no opinion about it. + ```js await page.setRequestInterception(true); page.on('request', (request) => { @@ -4921,7 +4951,7 @@ page.on('request', (request) => { - `postData` <[string]> If set changes the post data of request. - `headers` <[Object]> If set changes the request HTTP headers. Header values will be converted to a string. -Returns the most recent set of request overrides set with a previous call to continue() in Cooperative Mode. +Returns the most recent set of request overrides set with a previous call to continue() in Cooperative Intercept Mode. #### httpRequest.enqueueInterceptAction(pendingHandler) @@ -4949,7 +4979,7 @@ page.on('requestfailed', (request) => { - returns: <[Promise]> -When in Cooperative Mode, awaits pending interception handlers and then decides how to fulfill the request interception. +When in Cooperative Intercept Mode, awaits pending interception handlers and then decides how to fulfill the request interception. #### httpRequest.frame() @@ -5099,7 +5129,7 @@ page.on('request', (request) => { - returns: A matching [HTTPResponse] object, or `null` if the response has not been received yet. -Returns the current response object set by the previous call to respond() in Cooperative Mode. +Returns the current response object set by the previous call to respond() in Cooperative Intercept Mode. #### httpRequest.url() diff --git a/src/common/HTTPRequest.ts b/src/common/HTTPRequest.ts index bae80ea8f70b6..0551a72eb50d3 100644 --- a/src/common/HTTPRequest.ts +++ b/src/common/HTTPRequest.ts @@ -66,6 +66,13 @@ export interface ResponseForRequest { */ export type ResourceType = Lowercase; +/** + * The default cooperative request interception resolution priority + * + * @public + */ +export const DEFAULT_INTERCEPT_RESOLUTION_PRIORITY = 0; + interface CDPSession extends EventEmitter { send( method: T, From 53ddc0aa524813087504c43c600503ce9be2e0c5 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 25 Nov 2021 04:30:15 -0800 Subject: [PATCH 15/21] chore: typos --- docs/api.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 1f76830bae49a..75c71420ef082 100644 --- a/docs/api.md +++ b/docs/api.md @@ -183,6 +183,7 @@ * [page.setRequestInterception(value)](#pagesetrequestinterceptionvalue) - [Multiple Intercept Handlers and Asynchronous Resolutions](#multiple-intercept-handlers-and-asynchronous-resolutions) - [Cooperative Intercept Mode](#cooperative-intercept-mode) + - [Discussion: Cooperative request continuation](#discussion-cooperative-request-continuation) - [Upgrading to Cooperative Intercept Mode for package maintainers](#upgrading-to-cooperative-intercept-mode-for-package-maintainers) * [page.setUserAgent(userAgent[, userAgentMetadata])](#pagesetuseragentuseragent-useragentmetadata) * [page.setViewport(viewport)](#pagesetviewportviewport) @@ -2613,7 +2614,7 @@ page.on('request', (request) => { }); ``` -##### Discussion: Cooperative reuest continuation +##### Discussion: Cooperative Request Continuation Puppeteer requires `request.continue` to be called explicitly or the request will hang. Even if your handler means to take no special action, ou 'opt out', `request.continue` must still be called. From ae4b5c9a574a65cb7e70d986d819a8a13f32c711 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 25 Nov 2021 04:40:14 -0800 Subject: [PATCH 16/21] chore: typo --- docs/api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api.md b/docs/api.md index 75c71420ef082..6bd701b459219 100644 --- a/docs/api.md +++ b/docs/api.md @@ -183,7 +183,7 @@ * [page.setRequestInterception(value)](#pagesetrequestinterceptionvalue) - [Multiple Intercept Handlers and Asynchronous Resolutions](#multiple-intercept-handlers-and-asynchronous-resolutions) - [Cooperative Intercept Mode](#cooperative-intercept-mode) - - [Discussion: Cooperative request continuation](#discussion-cooperative-request-continuation) + - [Discussion: Cooperative Request Continuation](#discussion-cooperative-request-continuation) - [Upgrading to Cooperative Intercept Mode for package maintainers](#upgrading-to-cooperative-intercept-mode-for-package-maintainers) * [page.setUserAgent(userAgent[, userAgentMetadata])](#pagesetuseragentuseragent-useragentmetadata) * [page.setViewport(viewport)](#pagesetviewportviewport) @@ -2617,7 +2617,7 @@ page.on('request', (request) => { ##### Discussion: Cooperative Request Continuation Puppeteer requires `request.continue` to be called explicitly or the request will hang. Even if -your handler means to take no special action, ou 'opt out', `request.continue` must still be called. +your handler means to take no special action, or 'opt out', `request.continue` must still be called. With the introduction of Cooperative Intercept Mode, two use cases arise for cooperative request continuations: Unopinionated and Opinionated. From 500b8cb554a2d92e8e8cc1e924af4b419c811fe3 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 25 Nov 2021 05:01:49 -0800 Subject: [PATCH 17/21] chore: typo --- docs/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 6bd701b459219..6570b5f463823 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2744,7 +2744,7 @@ page.on('request', (interceptedRequest) => { }); ``` -The above solutions ensures backward compatibility while also allowing the user to adjust the importance of your package in the resolution chain when Cooperative Intercept Mode is being used. Your package continues to work as expected until the user has fully upgraded their code and all third party packages to use Cooperative Intercept Mode. If any handler or package still uses Legacy Mode, your package can still operate in Legacy Mode too. +The above solutions ensure backward compatibility while also allowing the user to adjust the importance of your package in the resolution chain when Cooperative Intercept Mode is being used. Your package continues to work as expected until the user has fully upgraded their code and all third party packages to use Cooperative Intercept Mode. If any handler or package still uses Legacy Mode, your package can still operate in Legacy Mode too. #### page.setUserAgent(userAgent[, userAgentMetadata]) From 98e281470efa56f44759cd1190c550b2628f8067 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 29 Nov 2021 13:39:52 -0800 Subject: [PATCH 18/21] chore: revert alreay-handled --- docs/api.md | 34 ++++++++++--------- src/common/HTTPRequest.ts | 6 ++-- test/requestinterception-experimental.spec.ts | 4 +-- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/docs/api.md b/docs/api.md index 6570b5f463823..49e6eacf8598c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2384,9 +2384,9 @@ const puppeteer = require('puppeteer'); By default Puppeteer will raise a `Request is already handled!` exception if `request.abort`, `request.continue`, or `request.respond` are called after any of them have already been called. -Always assume that an unknown handler may have already called `abort/continue/respond`. Even if your handler is the only one you registered, +Always assume that an unknown handler may have already called `abort/continue/respond`. Even if your handler is the only one you registered, 3rd party packages may register their own handlers. It is therefore -important to always check the resolution status using [request.isInterceptResolutionHandled](#httprequestisinterceptresolutionhandled) +important to always check the resolution status using [request.isInterceptResolutionHandled](#httprequestisinterceptresolutionhandled) before calling `abort/continue/respond`. Importantly, the intercept resolution may get handled by another listener while your handler is awaiting an asynchronous operation. Therefore, the return value of `request.isInterceptResolutionHandled` is only safe in a synchronous code block. Always execute `request.isInterceptResolutionHandled` and `abort/continue/respond` **synchronously** together. @@ -2403,7 +2403,7 @@ page.on('request', (interceptedRequest) => { }); /* -This second handler will return before calling request.abort because request.continue was already +This second handler will return before calling request.abort because request.continue was already called by the first handler. */ page.on('request', (interceptedRequest) => { @@ -2456,11 +2456,13 @@ Here is the example above rewritten using `request.interceptResolutionState` ```js /* This first handler will succeed in calling request.continue because the request interception has never been resolved. + +Note: `alreay-handled` is misspelled but likely won't be fixed until v13. https://github.com/puppeteer/puppeteer/pull/7780 */ page.on('request', (interceptedRequest) => { // The interception has not been handled yet. Control will pass through this guard. const { action } = interceptedRequest.interceptResolutionState(); - if (action === 'already-handled') return; + if (action === 'alreay-handled') return; // It is not strictly necessary to return a promise, but doing so will allow Puppeteer to await this handler. return new Promise(resolve => { @@ -2469,7 +2471,7 @@ page.on('request', (interceptedRequest) => { // Inside, check synchronously to verify that the intercept wasn't handled already. // It might have been handled during the 500ms while the other handler awaited an async op of its own. const { action } = interceptedRequest.interceptResolutionState(); - if (action === 'already-handled') { + if (action === 'alreay-handled') { resolve(); return; }; @@ -2480,12 +2482,12 @@ page.on('request', (interceptedRequest) => { }); page.on('request', async (interceptedRequest) => { // The interception has not been handled yet. Control will pass through this guard. - if (interceptedRequest.interceptResolutionState().action === 'already-handled') return; + if (interceptedRequest.interceptResolutionState().action === 'alreay-handled') return; await someLongAsyncOperation() // The interception *MIGHT* have been handled by the first handler, we can't be sure. // Therefore, we must check again before calling continue() or we risk Puppeteer raising an exception. - if (interceptedRequest.interceptResolutionState().action === 'already-handled') return; + if (interceptedRequest.interceptResolutionState().action === 'alreay-handled') return; interceptedRequest.continue(); }); ``` @@ -2550,7 +2552,7 @@ page.on('request', (request) => { request.continue({}); }); page.on('request', (request) => { - // { action: 'already-handled' }, because continue in Legacy Mode was called + // { action: 'alreay-handled' }, because continue in Legacy Mode was called console.log(request.interceptResolutionState()); }); @@ -2617,12 +2619,12 @@ page.on('request', (request) => { ##### Discussion: Cooperative Request Continuation Puppeteer requires `request.continue` to be called explicitly or the request will hang. Even if -your handler means to take no special action, or 'opt out', `request.continue` must still be called. +your handler means to take no special action, or 'opt out', `request.continue` must still be called. -With the introduction of Cooperative Intercept Mode, two use cases arise for cooperative request continuations: +With the introduction of Cooperative Intercept Mode, two use cases arise for cooperative request continuations: Unopinionated and Opinionated. -The first case (common) is that your handler means to opt out of doing anything special the request. It has no opinion on further action and simply intends to continue by default and/or defer to other handlers that might have an opinion. But in case there are no other handlers, we must call `request.continue` to ensure that the request doesn't hang. +The first case (common) is that your handler means to opt out of doing anything special the request. It has no opinion on further action and simply intends to continue by default and/or defer to other handlers that might have an opinion. But in case there are no other handlers, we must call `request.continue` to ensure that the request doesn't hang. We call this an **Unopinionated continuation** because the intent is to continue the request if nobody else has a better idea. Use `request.continue({...}, DEFAULT_INTERCEPT_RESOLUTION_PRIORITY)` (or `0`) for this type of continuation. @@ -2730,10 +2732,10 @@ page.on('request', (interceptedRequest) => { ) { interceptedRequest.abort('failed', _config.abortPriority); } - else + else { // Here we use a custom-configured priority to allow for Opinionated - // continuation. + // continuation. // We would only want to allow this if we had a very clear reason why // some use cases required Opinionated continuation. interceptedRequest.continue( @@ -5002,7 +5004,7 @@ When in Cooperative Intercept Mode, awaits pending interception handlers and the - returns: <[InterceptResolutionState]> - `action` <[InterceptResolutionAction]> Current resolution action. Possible values: `abort`, `respond`, `continue`, - `disabled`, `none`, and `already-handled` + `disabled`, `none`, and `alreay-handled` - `priority` The current priority of the winning action. `InterceptResolutionAction` is one of: @@ -5012,7 +5014,7 @@ When in Cooperative Intercept Mode, awaits pending interception handlers and the - `continue` - The request will be continued if no higher priority arises. - `disabled` - Request interception is not currently enabled (see `page.setRequestInterception`). - `none` - `abort/continue/respond` have not been called yet. -- `already-handled` - The interception has already been handled in Legacy Mode by a call to `abort/continue/respond` with +- `alreay-handled` - The interception has already been handled in Legacy Mode by a call to `abort/continue/respond` with a `priority` of `undefined`. Subsequent calls to `abort/continue/respond` will throw an exception. This example will `continue` a request at a slightly higher priority than the current action if the interception has not @@ -5021,7 +5023,7 @@ already handled and is not already being continued. ```js page.on('request', (interceptedRequest) => { const { action, priority } = interceptedRequest.interceptResolutionState(); - if (action === 'already-handled') return; + if (action === 'alreay-handled') return; if (action === 'continue') return; // Change the action to `continue` and bump the priority so `continue` becomes the new winner diff --git a/src/common/HTTPRequest.ts b/src/common/HTTPRequest.ts index 0551a72eb50d3..6fa8928506623 100644 --- a/src/common/HTTPRequest.ts +++ b/src/common/HTTPRequest.ts @@ -231,11 +231,11 @@ export class HTTPRequest { * priority?: number * * InterceptResolutionAction is one of: `abort`, `respond`, `continue`, - * `disabled`, `none`, or `already-handled`. + * `disabled`, `none`, or `alreay-handled`. */ interceptResolutionState(): InterceptResolutionState { if (!this._allowInterception) return { action: 'disabled' }; - if (this._interceptionHandled) return { action: 'already-handled' }; + if (this._interceptionHandled) return { action: 'alreay-handled' }; return { ...this._interceptResolutionState }; } @@ -632,7 +632,7 @@ export type InterceptResolutionAction = | 'continue' | 'disabled' | 'none' - | 'already-handled'; + | 'alreay-handled'; /** * @public diff --git a/test/requestinterception-experimental.spec.ts b/test/requestinterception-experimental.spec.ts index b47cc13b14e3f..9f0203ee301d9 100644 --- a/test/requestinterception-experimental.spec.ts +++ b/test/requestinterception-experimental.spec.ts @@ -845,7 +845,7 @@ describe('request interception', function () { 'Yo, page!' ); }); - it('should indicate already-handled if an intercept has been handled', async () => { + it('should indicate alreay-handled if an intercept has been handled', async () => { const { page, server } = getTestState(); await page.setRequestInterception(true); @@ -857,7 +857,7 @@ describe('request interception', function () { }); page.on('request', (request) => { const { action } = request.interceptResolutionState(); - expect(action).toBe('already-handled'); + expect(action).toBe('alreay-handled'); }); await page.goto(server.EMPTY_PAGE); }); From 70969e97461a3f5f19a090525706bee62228befe Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 29 Nov 2021 15:11:34 -0800 Subject: [PATCH 19/21] fix: already-handled spelling error --- docs/api.md | 18 ++++---- src/common/HTTPRequest.ts | 45 ++++++++++++------- test/requestinterception-experimental.spec.ts | 9 ++-- 3 files changed, 43 insertions(+), 29 deletions(-) diff --git a/docs/api.md b/docs/api.md index 73ecf01998270..599646dfe7b58 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2457,13 +2457,11 @@ Here is the example above rewritten using `request.interceptResolutionState` ```js /* This first handler will succeed in calling request.continue because the request interception has never been resolved. - -Note: `alreay-handled` is misspelled but likely won't be fixed until v13. https://github.com/puppeteer/puppeteer/pull/7780 */ page.on('request', (interceptedRequest) => { // The interception has not been handled yet. Control will pass through this guard. const { action } = interceptedRequest.interceptResolutionState(); - if (action === 'alreay-handled') return; + if (action === 'already-handled') return; // It is not strictly necessary to return a promise, but doing so will allow Puppeteer to await this handler. return new Promise(resolve => { @@ -2472,7 +2470,7 @@ page.on('request', (interceptedRequest) => { // Inside, check synchronously to verify that the intercept wasn't handled already. // It might have been handled during the 500ms while the other handler awaited an async op of its own. const { action } = interceptedRequest.interceptResolutionState(); - if (action === 'alreay-handled') { + if (action === 'already-handled') { resolve(); return; }; @@ -2483,12 +2481,12 @@ page.on('request', (interceptedRequest) => { }); page.on('request', async (interceptedRequest) => { // The interception has not been handled yet. Control will pass through this guard. - if (interceptedRequest.interceptResolutionState().action === 'alreay-handled') return; + if (interceptedRequest.interceptResolutionState().action === 'already-handled') return; await someLongAsyncOperation() // The interception *MIGHT* have been handled by the first handler, we can't be sure. // Therefore, we must check again before calling continue() or we risk Puppeteer raising an exception. - if (interceptedRequest.interceptResolutionState().action === 'alreay-handled') return; + if (interceptedRequest.interceptResolutionState().action === 'already-handled') return; interceptedRequest.continue(); }); ``` @@ -2553,7 +2551,7 @@ page.on('request', (request) => { request.continue({}); }); page.on('request', (request) => { - // { action: 'alreay-handled' }, because continue in Legacy Mode was called + // { action: 'already-handled' }, because continue in Legacy Mode was called console.log(request.interceptResolutionState()); }); @@ -5005,7 +5003,7 @@ When in Cooperative Intercept Mode, awaits pending interception handlers and the - returns: <[InterceptResolutionState]> - `action` <[InterceptResolutionAction]> Current resolution action. Possible values: `abort`, `respond`, `continue`, - `disabled`, `none`, and `alreay-handled` + `disabled`, `none`, and `already-handled` - `priority` The current priority of the winning action. `InterceptResolutionAction` is one of: @@ -5015,7 +5013,7 @@ When in Cooperative Intercept Mode, awaits pending interception handlers and the - `continue` - The request will be continued if no higher priority arises. - `disabled` - Request interception is not currently enabled (see `page.setRequestInterception`). - `none` - `abort/continue/respond` have not been called yet. -- `alreay-handled` - The interception has already been handled in Legacy Mode by a call to `abort/continue/respond` with +- `already-handled` - The interception has already been handled in Legacy Mode by a call to `abort/continue/respond` with a `priority` of `undefined`. Subsequent calls to `abort/continue/respond` will throw an exception. This example will `continue` a request at a slightly higher priority than the current action if the interception has not @@ -5024,7 +5022,7 @@ already handled and is not already being continued. ```js page.on('request', (interceptedRequest) => { const { action, priority } = interceptedRequest.interceptResolutionState(); - if (action === 'alreay-handled') return; + if (action === 'already-handled') return; if (action === 'continue') return; // Change the action to `continue` and bump the priority so `continue` becomes the new winner diff --git a/src/common/HTTPRequest.ts b/src/common/HTTPRequest.ts index 6fa8928506623..58617edc7cdd9 100644 --- a/src/common/HTTPRequest.ts +++ b/src/common/HTTPRequest.ts @@ -180,7 +180,7 @@ export class HTTPRequest { this._frame = frame; this._redirectChain = redirectChain; this._continueRequestOverrides = {}; - this._interceptResolutionState = { action: 'none' }; + this._interceptResolutionState = { action: InterceptResolutionAction.None }; this._interceptHandlers = []; this._initiator = event.initiator; @@ -231,11 +231,13 @@ export class HTTPRequest { * priority?: number * * InterceptResolutionAction is one of: `abort`, `respond`, `continue`, - * `disabled`, `none`, or `alreay-handled`. + * `disabled`, `none`, or `already-handled`. */ interceptResolutionState(): InterceptResolutionState { - if (!this._allowInterception) return { action: 'disabled' }; - if (this._interceptionHandled) return { action: 'alreay-handled' }; + if (!this._allowInterception) + return { action: InterceptResolutionAction.Disabled }; + if (this._interceptionHandled) + return { action: InterceptResolutionAction.AlreadyHnadled }; return { ...this._interceptResolutionState }; } @@ -441,7 +443,10 @@ export class HTTPRequest { priority > this._interceptResolutionState.priority || this._interceptResolutionState.priority === undefined ) { - this._interceptResolutionState = { action: 'continue', priority }; + this._interceptResolutionState = { + action: InterceptResolutionAction.Continue, + priority, + }; return; } if (priority === this._interceptResolutionState.priority) { @@ -451,7 +456,8 @@ export class HTTPRequest { ) { return; } - this._interceptResolutionState.action = 'continue'; + this._interceptResolutionState.action = + InterceptResolutionAction.Continue; } return; } @@ -527,14 +533,17 @@ export class HTTPRequest { priority > this._interceptResolutionState.priority || this._interceptResolutionState.priority === undefined ) { - this._interceptResolutionState = { action: 'respond', priority }; + this._interceptResolutionState = { + action: InterceptResolutionAction.Respond, + priority, + }; return; } if (priority === this._interceptResolutionState.priority) { if (this._interceptResolutionState.action === 'abort') { return; } - this._interceptResolutionState.action = 'respond'; + this._interceptResolutionState.action = InterceptResolutionAction.Respond; } } @@ -605,7 +614,10 @@ export class HTTPRequest { priority >= this._interceptResolutionState.priority || this._interceptResolutionState.priority === undefined ) { - this._interceptResolutionState = { action: 'abort', priority }; + this._interceptResolutionState = { + action: InterceptResolutionAction.Abort, + priority, + }; return; } } @@ -626,13 +638,14 @@ export class HTTPRequest { /** * @public */ -export type InterceptResolutionAction = - | 'abort' - | 'respond' - | 'continue' - | 'disabled' - | 'none' - | 'alreay-handled'; +export enum InterceptResolutionAction { + Abort = 'abort', + Respond = 'respond', + Continue = 'continue', + Disabled = 'disabled', + None = 'none', + AlreadyHnadled = 'already-handled', +} /** * @public diff --git a/test/requestinterception-experimental.spec.ts b/test/requestinterception-experimental.spec.ts index 9f0203ee301d9..d439d98070bf5 100644 --- a/test/requestinterception-experimental.spec.ts +++ b/test/requestinterception-experimental.spec.ts @@ -24,7 +24,10 @@ import { setupTestPageAndContextHooks, describeFailsFirefox, } from './mocha-utils'; // eslint-disable-line import/extensions -import { ActionResult } from '../lib/cjs/puppeteer/api-docs-entry.js'; +import { + ActionResult, + InterceptResolutionAction, +} from '../lib/cjs/puppeteer/api-docs-entry.js'; describe('request interception', function () { setupTestBrowserHooks(); @@ -845,7 +848,7 @@ describe('request interception', function () { 'Yo, page!' ); }); - it('should indicate alreay-handled if an intercept has been handled', async () => { + it('should indicate already-handled if an intercept has been handled', async () => { const { page, server } = getTestState(); await page.setRequestInterception(true); @@ -857,7 +860,7 @@ describe('request interception', function () { }); page.on('request', (request) => { const { action } = request.interceptResolutionState(); - expect(action).toBe('alreay-handled'); + expect(action).toBe(InterceptResolutionAction.AlreadyHnadled); }); await page.goto(server.EMPTY_PAGE); }); From e472bf99351557740bf38cbbdf6c8694ff57454a Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Tue, 7 Dec 2021 12:24:10 -0800 Subject: [PATCH 20/21] chore: fix d.ts import --- test/requestinterception-experimental.spec.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/requestinterception-experimental.spec.ts b/test/requestinterception-experimental.spec.ts index d439d98070bf5..6b496ff52599b 100644 --- a/test/requestinterception-experimental.spec.ts +++ b/test/requestinterception-experimental.spec.ts @@ -24,10 +24,8 @@ import { setupTestPageAndContextHooks, describeFailsFirefox, } from './mocha-utils'; // eslint-disable-line import/extensions -import { - ActionResult, - InterceptResolutionAction, -} from '../lib/cjs/puppeteer/api-docs-entry.js'; +import { ActionResult } from '../lib/cjs/puppeteer/api-docs-entry.js'; +import { InterceptResolutionAction } from '../lib/cjs/puppeteer/common/HTTPRequest.js'; describe('request interception', function () { setupTestBrowserHooks(); From d06d5dafe3949174d0af1c04f1ca404d36410b5f Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 9 Dec 2021 05:44:32 -0800 Subject: [PATCH 21/21] chore: typo --- src/common/HTTPRequest.ts | 4 ++-- test/requestinterception-experimental.spec.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/HTTPRequest.ts b/src/common/HTTPRequest.ts index d6b06d35c149c..a9e0f2c419e62 100644 --- a/src/common/HTTPRequest.ts +++ b/src/common/HTTPRequest.ts @@ -237,7 +237,7 @@ export class HTTPRequest { if (!this._allowInterception) return { action: InterceptResolutionAction.Disabled }; if (this._interceptionHandled) - return { action: InterceptResolutionAction.AlreadyHnadled }; + return { action: InterceptResolutionAction.AlreadyHandled }; return { ...this._interceptResolutionState }; } @@ -644,7 +644,7 @@ export enum InterceptResolutionAction { Continue = 'continue', Disabled = 'disabled', None = 'none', - AlreadyHnadled = 'already-handled', + AlreadyHandled = 'already-handled', } /** diff --git a/test/requestinterception-experimental.spec.ts b/test/requestinterception-experimental.spec.ts index 6b496ff52599b..e16a715c0ea52 100644 --- a/test/requestinterception-experimental.spec.ts +++ b/test/requestinterception-experimental.spec.ts @@ -858,7 +858,7 @@ describe('request interception', function () { }); page.on('request', (request) => { const { action } = request.interceptResolutionState(); - expect(action).toBe(InterceptResolutionAction.AlreadyHnadled); + expect(action).toBe(InterceptResolutionAction.AlreadyHandled); }); await page.goto(server.EMPTY_PAGE); });