feat: expose HTTPRequest intercept resolution state and clarify docs (#7796)

Co-authored-by: Rodrigo Fernández <fdez.romero@gmail.com>
This commit is contained in:
Ben Allfree 2021-12-06 23:48:42 -08:00 committed by GitHub
parent 636b0863a1
commit dc23b7535c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 368 additions and 101 deletions

View File

@ -182,8 +182,10 @@
* [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)
- [Upgrading to Cooperative Mode for package maintainers](#upgrading-to-cooperative-mode-for-package-maintainers)
- [Multiple Intercept Handlers and Asynchronous Resolutions](#multiple-intercept-handlers-and-asynchronous-resolutions)
- [Cooperative Intercept Mode](#cooperative-intercept-mode)
- [Cooperative Request Continuation](#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)
* [page.tap(selector)](#pagetapselector)
@ -345,6 +347,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)
@ -2364,6 +2368,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')
@ -2376,17 +2381,134 @@ 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 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:
```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 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.
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.
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;
// 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 { action } = interceptedRequest.interceptResolutionState();
if (action === 'alreay-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().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 === 'alreay-handled') return;
interceptedRequest.continue();
});
```
##### Cooperative Intercept 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 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`.
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).
In this example, Legacy Mode prevails and the request is aborted immediately because at least one handler omits `priority` when resolving the intercept:
@ -2394,17 +2516,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) => {
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.
// Cooperative Intercept Mode: votes for continue at priority 0.
request.continue({}, 0);
});
```
@ -2415,73 +2536,110 @@ 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) => {
// 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.
if (request.isInterceptResolutionHandled()) return
// Cooperative Intercept Mode: votes to abort at priority 0.
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.
// { 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) => {
// { action: 'alreay-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()`.
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
page.setRequestInterception(true);
page.on('request', (request) => {
// Cooperative Mode: votes to abort at priority 10
if (request.isInterceptResolutionHandled()) return
// Cooperative Intercept Mode: votes to abort at priority 10
request.abort('failed', 0);
});
page.on('request', (request) => {
// Cooperative Mode: votes to continue at priority 5
if (request.isInterceptResolutionHandled()) return
// Cooperative Intercept 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());
// { action: 'continue', priority: 5 }, because continue @ 5 > abort @ 0
console.log(request.interceptResolutionState());
});
```
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
page.setRequestInterception(true);
page.on('request', (request) => {
// Cooperative Mode: votes to abort at priority 10
if (request.isInterceptResolutionHandled()) return
// Cooperative Intercept Mode: votes to abort at priority 10
request.abort('failed', 10);
});
page.on('request', (request) => {
// Cooperative Mode: votes to continue at priority 15
if (request.isInterceptResolutionHandled()) return
// Cooperative Intercept Mode: votes to continue at priority 15
request.continue(request.continueRequestOverrides(), 15);
});
page.on('request', (request) => {
// Cooperative Mode: votes to respond at priority 15
if (request.isInterceptResolutionHandled()) return
// Cooperative Intercept Mode: votes to respond at priority 15
request.respond(request.responseForRequest(), 15);
});
page.on('request', (request) => {
// Cooperative Mode: votes to respond at priority 12
if (request.isInterceptResolutionHandled()) return
// Cooperative Intercept 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());
// { action: 'respond', priority: 15 }, because respond @ 15 > continue @ 15 > respond @ 12 > abort @ 10
console.log(request.interceptResolutionState());
});
```
##### Upgrading to Cooperative Mode for package maintainers
##### Cooperative Request 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, 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.
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) => {
if (request.isInterceptResolutionHandled()) return
if (
interceptedRequest.url().endsWith('.png') ||
interceptedRequest.url().endsWith('.jpg')
@ -2491,10 +2649,11 @@ 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) => {
if (request.isInterceptResolutionHandled()) return
if (
interceptedRequest.url().endsWith('.png') ||
interceptedRequest.url().endsWith('.jpg')
@ -2508,24 +2667,29 @@ 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.** 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 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
let _priority = undefined;
// Export a module configuration function
export const setInterceptResolutionStrategy = (defaultPriority = 0) =>
(_priority = defaultPriority);
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 (
interceptedRequest.url().endsWith('.png') ||
interceptedRequest.url().endsWith('.jpg')
@ -2534,48 +2698,56 @@ 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 ResolutionStrategy {
abortPriority: number;
continuePriority: number;
interface InterceptResolutionConfig {
abortPriority?: number;
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_STRATEGY: ResolutionStrategy = {
abortPriority: 0,
continuePriority: 0,
const DEFAULT_CONFIG: InterceptResolutionConfig = {
abortPriority: undefined, // Default to Legacy Mode
continuePriority: undefined, // Default to Legacy Mode
};
// Defaults to undefined which preserves Legacy Mode behavior
let _strategy: Partial<ResolutionStrategy> = {};
let _config: Partial<InterceptResolutionConfig> = {};
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
{
// 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(),
_strategy.continuePriority
_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 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])
@ -4743,7 +4915,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])
@ -4758,9 +4930,14 @@ 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) => {
if (request.isInterceptResolutionHandled()) return;
// Override headers
const headers = Object.assign({}, request.headers(), {
foo: 'bar', // set "foo" header
@ -4778,7 +4955,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)
@ -4806,7 +4983,7 @@ page.on('requestfailed', (request) => {
- returns: <[Promise<unknown>]>
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()
@ -4824,6 +5001,47 @@ When in Cooperative Mode, awaits pending interception handlers and then decides
- `url` <?[string]> Initiator URL, set for `parser`, `script` and `SignedExchange` type.
- `lineNumber` <?[number]> 0 based initiator line number, set for `parser` and `script`.
#### httpRequest.interceptResolutionState()
- returns: <[InterceptResolutionState]>
- `action` <[InterceptResolutionAction]> Current resolution action. Possible values: `abort`, `respond`, `continue`,
`disabled`, `none`, and `alreay-handled`
- `priority` <?[number]> The current priority of the winning action.
`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.
- `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
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
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 === 'continue') return;
// Change the action 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]>
@ -4894,6 +5112,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',
@ -4913,7 +5133,7 @@ page.on('request', (request) => {
- returns: <?[HTTPResponse]> 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()

View File

@ -36,6 +36,14 @@ export interface ContinueRequestOverrides {
headers?: Record<string, string>;
}
/**
* @public
*/
export interface InterceptResolutionState {
action: InterceptResolutionAction;
priority?: number;
}
/**
* Required response data to fulfill a request with.
*
@ -58,6 +66,13 @@ export interface ResponseForRequest {
*/
export type ResourceType = Lowercase<Protocol.Network.ResourceType>;
/**
* The default cooperative request interception resolution priority
*
* @public
*/
export const DEFAULT_INTERCEPT_RESOLUTION_PRIORITY = 0;
interface CDPSession extends EventEmitter {
send<T extends keyof ProtocolMapping.Commands>(
method: T,
@ -137,9 +152,8 @@ export class HTTPRequest {
private _continueRequestOverrides: ContinueRequestOverrides;
private _responseForRequest: Partial<ResponseForRequest>;
private _abortErrorReason: Protocol.Network.ErrorReason;
private _currentStrategy: InterceptResolutionStrategy;
private _currentPriority: number | undefined;
private _interceptActions: Array<() => void | PromiseLike<any>>;
private _interceptResolutionState: InterceptResolutionState;
private _interceptHandlers: Array<() => void | PromiseLike<any>>;
private _initiator: Protocol.Network.Initiator;
/**
@ -166,9 +180,8 @@ export class HTTPRequest {
this._frame = frame;
this._redirectChain = redirectChain;
this._continueRequestOverrides = {};
this._currentStrategy = 'none';
this._currentPriority = undefined;
this._interceptActions = [];
this._interceptResolutionState = { action: 'none' };
this._interceptHandlers = [];
this._initiator = event.initiator;
for (const key of Object.keys(event.request.headers))
@ -210,14 +223,28 @@ export class HTTPRequest {
}
/**
* @returns An array of the current intercept resolution strategy and priority
* `[strategy,priority]`. Strategy is one of: `abort`, `respond`, `continue`,
* `disabled`, `none`, or `already-handled`.
* @returns An InterceptResolutionState object describing the current resolution
* action and priority.
*
* InterceptResolutionState contains:
* action: InterceptResolutionAction
* priority?: number
*
* InterceptResolutionAction is one of: `abort`, `respond`, `continue`,
* `disabled`, `none`, or `alreay-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 { action: 'disabled' };
if (this._interceptionHandled) return { action: 'alreay-handled' };
return { ...this._interceptResolutionState };
}
/**
* @returns `true` if the intercept resolution has already been handled,
* `false` otherwise.
*/
isInterceptResolutionHandled(): boolean {
return this._interceptionHandled;
}
/**
@ -229,7 +256,7 @@ export class HTTPRequest {
enqueueInterceptAction(
pendingHandler: () => void | PromiseLike<unknown>
): void {
this._interceptActions.push(pendingHandler);
this._interceptHandlers.push(pendingHandler);
}
/**
@ -237,12 +264,12 @@ export class HTTPRequest {
* the request interception.
*/
async finalizeInterceptions(): Promise<void> {
await this._interceptActions.reduce(
await this._interceptHandlers.reduce(
(promiseChain, interceptAction) => promiseChain.then(interceptAction),
Promise.resolve()
);
const [resolution] = this.interceptResolution();
switch (resolution) {
const { action } = this.interceptResolutionState();
switch (action) {
case 'abort':
return this._abort(this._abortErrorReason);
case 'respond':
@ -411,21 +438,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 = { action: 'continue', priority };
return;
}
if (priority === this._currentPriority) {
if (priority === this._interceptResolutionState.priority) {
if (
this._currentStrategy === 'abort' ||
this._currentStrategy === 'respond'
this._interceptResolutionState.action === 'abort' ||
this._interceptResolutionState.action === 'respond'
) {
return;
}
this._currentStrategy = 'continue';
this._interceptResolutionState.action = 'continue';
}
return;
}
@ -498,18 +524,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 = { action: 'respond', priority };
return;
}
if (priority === this._currentPriority) {
if (this._currentStrategy === 'abort') {
if (priority === this._interceptResolutionState.priority) {
if (this._interceptResolutionState.action === 'abort') {
return;
}
this._currentStrategy = 'respond';
this._interceptResolutionState.action = 'respond';
}
}
@ -577,11 +602,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 = { action: 'abort', priority };
return;
}
}
@ -602,7 +626,7 @@ export class HTTPRequest {
/**
* @public
*/
export type InterceptResolutionStrategy =
export type InterceptResolutionAction =
| 'abort'
| 'respond'
| 'continue'
@ -610,6 +634,13 @@ export type InterceptResolutionStrategy =
| 'none'
| 'alreay-handled';
/**
* @public
*
* Deprecate ASAP
*/
export type InterceptResolutionStrategy = InterceptResolutionAction;
/**
* @public
*/

View File

@ -845,6 +845,22 @@ describe('request interception', function () {
'Yo, page!'
);
});
it('should indicate alreay-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 { action } = request.interceptResolutionState();
expect(action).toBe('alreay-handled');
});
await page.goto(server.EMPTY_PAGE);
});
});
});