Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cooperative request intercepts #6735

Merged
merged 67 commits into from Jul 2, 2021
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
284f541
feat: cooperative request intercepts
benallfree Jan 19, 2021
8b6827f
Merge branch 'main' of github.com:puppeteer/puppeteer into main
benallfree Feb 12, 2021
16b512d
feat: cooperative_request
benallfree Feb 12, 2021
8dfbfc6
Revert "feat: cooperative_request"
benallfree Feb 12, 2021
8b7af59
Revert "Revert "feat: cooperative_request""
benallfree Feb 12, 2021
f82f72c
Revert "Revert "Revert "feat: cooperative_request"""
benallfree Feb 12, 2021
ff6d08d
Revert "Revert "Revert "Revert "feat: cooperative_request""""
benallfree Feb 12, 2021
25e0f4f
chore: clarification
benallfree Feb 12, 2021
41ef2d3
chore: spelling fix
benallfree Feb 12, 2021
4005364
chore: testing
benallfree Feb 12, 2021
a815a1c
chore: sequentially execute promised handlers
benallfree Feb 12, 2021
dfb1ba1
Merge branch 'main' of github.com:puppeteer/puppeteer into main
benallfree Apr 26, 2021
5f6371d
chore: merge fixes
benallfree Apr 26, 2021
ab1875c
chore: api wip
benallfree Apr 28, 2021
3e5655a
chore: respondXXX and tests
benallfree Apr 29, 2021
736bd0a
chore: formatting
benallfree Apr 29, 2021
ffd6e41
chore: naming
benallfree Apr 29, 2021
930bd4c
chore: remove promise return
benallfree Apr 29, 2021
b3524d8
chore: priority fix
benallfree Apr 29, 2021
6ab90c5
chore: experimental api
benallfree Apr 29, 2021
b31bac1
chore: formatting
benallfree Apr 29, 2021
512109b
chore: formatting
benallfree Apr 29, 2021
6e130ce
Merge branch 'upstream-main'
benallfree Apr 29, 2021
f181875
chore: cleanup
benallfree Apr 29, 2021
57fcf49
Merge branch 'main' into main
mathiasbynens May 4, 2021
3ac5c73
Merge branch 'upstream-main'
benallfree May 23, 2021
2b8088e
feat: api simplification
benallfree May 23, 2021
b35d54a
Merge branch 'main' into main
benallfree May 24, 2021
edfbbb3
chore: merge updates
benallfree May 24, 2021
9823e8c
chore: wip
benallfree May 24, 2021
20bb756
chore: lint fixes
benallfree May 24, 2021
b56dd04
Merge branch 'main' into main
jschfflr Jun 1, 2021
e9d5440
Merge branch 'main' into main
jschfflr Jun 8, 2021
682f665
Merge remote-tracking branch 'upstream/main'
benallfree Jun 28, 2021
5b11091
Merge branch 'main' of github.com:benallfree/puppeteer
benallfree Jun 28, 2021
48c1e1b
chore: wip
benallfree Jun 28, 2021
45250d4
chore: wip
benallfree Jun 28, 2021
356b506
chore: docs
benallfree Jun 28, 2021
a6f3951
chore: spelling
benallfree Jun 28, 2021
7be48b8
chore: remove default priority constant
benallfree Jun 28, 2021
69357ec
chore: wip
benallfree Jun 28, 2021
c4f4751
chore: unit tests
benallfree Jun 28, 2021
ca2c44b
chore: docs update
benallfree Jun 28, 2021
0da0132
chore: api.md update
benallfree Jun 28, 2021
d949785
chore: api.md
benallfree Jun 28, 2021
3206f5d
chore: api.md
benallfree Jun 28, 2021
88067ea
chore: api.md
benallfree Jun 28, 2021
2172437
chore: update api docs
jschfflr Jun 28, 2021
4de57aa
chore: update test
jschfflr Jun 29, 2021
9a2eaa7
chore: fix linter errors
jschfflr Jun 29, 2021
90608ed
Update docs/api.md
benallfree Jul 1, 2021
8656884
Update docs/api.md
benallfree Jul 1, 2021
039802b
Update docs/api.md
benallfree Jul 1, 2021
fec57b8
Update docs/api.md
benallfree Jul 1, 2021
4a53d27
Update docs/api.md
benallfree Jul 1, 2021
5ace813
Update src/common/HTTPRequest.ts
benallfree Jul 1, 2021
e483b9e
Update src/common/HTTPRequest.ts
benallfree Jul 1, 2021
23f8ee6
Update src/common/HTTPRequest.ts
benallfree Jul 1, 2021
14ec5b4
Update src/common/HTTPRequest.ts
benallfree Jul 1, 2021
679cb1a
Update src/common/NetworkManager.ts
benallfree Jul 1, 2021
f15dba4
Merge branch 'main' into main
benallfree Jul 1, 2021
8a4a2fb
chore: docs
benallfree Jul 1, 2021
b3fd6a7
Update src/common/HTTPRequest.ts
benallfree Jul 1, 2021
99ce3ce
Update src/common/NetworkManager.ts
benallfree Jul 1, 2021
ff19a6a
chore: apply suggestions from code review
jschfflr Jul 2, 2021
fec8819
Merge branch 'main' into main
jschfflr Jul 2, 2021
684887b
chore: update api.md
jschfflr Jul 2, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
227 changes: 221 additions & 6 deletions docs/api.md
Expand Up @@ -180,6 +180,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)
- [Upgrading to Cooperative Mode for Package Maintainers](#upgrading-to-cooperative-mode-for-package-maintainers)
* [page.setUserAgent(userAgent)](#pagesetuseragentuseragent)
* [page.setViewport(viewport)](#pagesetviewportviewport)
* [page.tap(selector)](#pagetapselector)
Expand Down Expand Up @@ -328,18 +330,23 @@
* [elementHandle.type(text[, options])](#elementhandletypetext-options)
* [elementHandle.uploadFile(...filePaths)](#elementhandleuploadfilefilepaths)
- [class: HTTPRequest](#class-httprequest)
* [httpRequest.abort([errorCode])](#httprequestaborterrorcode)
* [httpRequest.continue([overrides])](#httprequestcontinueoverrides)
* [httpRequest.abort([errorCode], [priority])](#httprequestaborterrorcode-priority)
* [httpRequest.abortErrorReason()](#httprequestaborterrorreason)
* [httpRequest.continue([overrides], [priority])](#httprequestcontinueoverrides-priority)
* [httpRequest.continueRequestOverrides()](#httprequestcontinuerequestoverrides)
* [httpRequest.enqueueInterceptAction(pendingHandler)](#httprequestenqueueinterceptactionpendinghandler)
* [httpRequest.failure()](#httprequestfailure)
* [httpRequest.finalizeInterceptions()](#httprequestfinalizeinterceptions)
* [httpRequest.frame()](#httprequestframe)
* [httpRequest.headers()](#httprequestheaders)
* [httpRequest.isNavigationRequest()](#httprequestisnavigationrequest)
* [httpRequest.method()](#httprequestmethod)
* [httpRequest.postData()](#httprequestpostdata)
* [httpRequest.redirectChain()](#httprequestredirectchain)
* [httpRequest.resourceType()](#httprequestresourcetype)
* [httpRequest.respond(response)](#httprequestrespondresponse)
* [httpRequest.respond(response, [priority])](#httprequestrespondresponse-priority)
* [httpRequest.response()](#httprequestresponse)
* [httpRequest.responseForRequest()](#httprequestresponseforrequest)
* [httpRequest.url()](#httprequesturl)
- [class: HTTPResponse](#class-httpresponse)
* [httpResponse.buffer()](#httpresponsebuffer)
Expand Down Expand Up @@ -1491,6 +1498,7 @@ If URLs are specified, only cookies for those URLs are returned.
- returns: <[Coverage]>

#### page.createPDFStream([options])

- `options` <[Object]> Options object which might have the following properties:
- `path` <[string]> The file path to save the PDF to. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). If no path is provided, the PDF won't be saved to the disk.
- `scale` <[number]> Scale of the webpage rendering. Defaults to `1`. Scale amount must be between 0.1 and 2.
Expand Down Expand Up @@ -2045,6 +2053,7 @@ Page is guaranteed to have a main frame which persists during navigations.
- returns: <[Mouse]>

#### page.pdf([options])

- `options` <[Object]> Options object which might have the following properties:
- `path` <[string]> The file path to save the PDF to. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). If no path is provided, the PDF won't be saved to the disk.
- `scale` <[number]> Scale of the webpage rendering. Defaults to `1`. Scale amount must be between 0.1 and 2.
Expand Down Expand Up @@ -2344,6 +2353,175 @@ const puppeteer = require('puppeteer');
})();
```

##### Cooperative Intercept Mode and Legacy Intercept Mode

`request.respond`, `request.abort`, and `request.continue` can accept an optional `priority` to activate Cooperative Intercept Mode. In Cooperative Mode, all intercept handlers are guaraneed to run and the interception will be resolved to the highest priority resolution. Here are the rules of Cooperative Mode:
benallfree marked this conversation as resolved.
Show resolved Hide resolved

- Async handlers will finish before intercept resolution is finalized
- The highest priority interception resolution "wins", ie, the interception will ultimately be 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 will gracefully prefer `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) will cause immediate resolution. For Cooperative Mode to work, all resolutions must use a `priority`.
benallfree marked this conversation as resolved.
Show resolved Hide resolved

In this example, Legacy Mode prevails and the request is aborted immediately because at least one handler omits `priority` when resolving the intercept:

```ts
// Final outcome: immediate abort()
page.setRequestInterception(true);
page.on('request', (request) => {
request.abort('failed'); // Legacy behavior: would execute immediately
});
page.on('request', (request) => {
console.log(request.interceptResolution()); // ['already-handled'], meaning a legacy resolution has taken place
request.continue({}, 0); // Legacy behavior: throws an exception becasue abort() was already called
});
```

In this example, Legacy Mode prevails and the request is continued because at least one handler does not specify a `priority`:

```ts
// Final outcome: immediate continue()
page.setRequestInterception(true);
page.on('request', (request) => {
request.abort('failed', 0); // Cooperative behavior: would execute at priority 0, but instead will throw an exception because legacy continue() gets called
});
page.on('request', (request) => {
console.log(request.interceptResolution()); // ['abort', 0], meaning an abort @ 0 is the current winning resolution
request.continue({}); // Legacy behavior: executes immediately, causing the previous cooperative abort() to throw an exception
});
```

In this example, Cooperative Mode is active because all handlers specify a `priority`. `continue()` wins because it has a higher priority than `abort()`.

```ts
// Final outcome: cooperative continue()
page.setRequestInterception(true);
page.on('request', (request) => {
request.abort('failed', 0); // Cooperative behavior: aborts at priority 10
});
page.on('request', (request) => {
request.continue(request.continueRequestOverrides(), 5); // Cooperative behavior: continues at priority 5
});
page.on('request', (request) => {
console.log(request.interceptResolution()); // ['continue', 5], because continue @ 5 > abort @ 0
});
```

In this example, Cooperative 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()
page.setRequestInterception(true);
page.on('request', (request) => {
request.abort('failed', 10); // Cooperative behavior: aborts at priority 10
});
page.on('request', (request) => {
request.continue(request.continueRequestOverrides(), 15); // Cooperative behavior: continues at priority 15
});
page.on('request', (request) => {
request.respond(request.responseForRequest(), 15); // Cooperative behavior: responds at priority 15
});
page.on('request', (request) => {
console.log(request.interceptResolution()); // ['respond', 15], because respond @ 15 > continue @ 15 > abort @ 10
});
```

##### Upgrading to Cooperative Mode for Package Maintainers
benallfree marked this conversation as resolved.
Show resolved Hide resolved

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:

```ts
page.on('request', (interceptedRequest) => {
if (
interceptedRequest.url().endsWith('.png') ||
interceptedRequest.url().endsWith('.jpg')
)
interceptedRequest.abort();
else interceptedRequest.continue();
});
```

To use Cooperative Mode, upgrade `continue()` and `abort()`:

```ts
page.on('request', (interceptedRequest) => {
if (
interceptedRequest.url().endsWith('.png') ||
interceptedRequest.url().endsWith('.jpg')
)
interceptedRequest.abort('failed', 0);
else
interceptedRequest.continue(
interceptedRequest.continueRequestOverrides(),
0
);
});
```

With those simple upgrades, your handler will use Cooperative Mode instead.
benallfree marked this conversation as resolved.
Show resolved Hide resolved

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.
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:

```ts
let _priority = undefined; // Defaults to undefined which preserves legacy mode behavior
export const setInterceptResolutionStrategy = (defaultPriority = 0) =>
(_priority = defaultPriority);

page.on('request', (interceptedRequest) => {
if (
interceptedRequest.url().endsWith('.png') ||
interceptedRequest.url().endsWith('.jpg')
)
interceptedRequest.abort('failed', _priority);
else
interceptedRequest.continue(
interceptedRequest.continueRequestOverrides(),
_priority
);
});
```

If your package calls for more fine-grained control resolution priorities, use a config pattern like this:

```ts
interface ResolutionStrategy {
abortPriority: number;
continuePriority: number;
}
const DEFAULT_STRATEGY: ResolutionStrategy = {
abortPriority: 0,
continuePriority: 0,
};

// Defaults to undefined which preserves legacy mode behavior
let _strategy: Partial<ResolutionStrategy> = {};

export const setInterceptResolutionStrategy = (strategy: ResolutionStrategy) =>
(_strategy = { ...DEFAULT_STRATEGY, ...strategy });

page.on('request', (interceptedRequest) => {
if (
interceptedRequest.url().endsWith('.png') ||
interceptedRequest.url().endsWith('.jpg')
)
interceptedRequest.abort('failed', _strategy.abortPriority);
else
interceptedRequest.continue(
interceptedRequest.continueRequestOverrides(),
_strategy.continuePriority
);
});
```

The above solution will ensure that your package continues to work as expected with other legacy handlers while also allowing the user to adjust the importance of your package in the resolution chain when Cooperative Mode is being used.
benallfree marked this conversation as resolved.
Show resolved Hide resolved

#### page.setUserAgent(userAgent)

- `userAgent` <[string]> Specific user agent to use in this page
Expand Down Expand Up @@ -4413,7 +4591,7 @@ If request fails at some point, then instead of `'requestfinished'` event (and p

If request gets a 'redirect' response, the request is successfully finished with the 'requestfinished' event, and a new request is issued to a redirected URL.

#### httpRequest.abort([errorCode])
#### httpRequest.abort([errorCode], [priority])

- `errorCode` <[string]> Optional error code. Defaults to `failed`, could be
one of the following:
Expand All @@ -4432,18 +4610,26 @@ If request gets a 'redirect' response, the request is successfully finished with
- `namenotresolved` - The host name could not be resolved.
- `timedout` - An operation timed out.
- `failed` - A generic failure occurred.
- `priority` <[number]> - Optional intercept abort priority. If provided, intercept will be resolved using coopeative handling rules. Otherwise, intercept will be resovled immediately.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- `priority` <[number]> - Optional intercept abort priority. If provided, intercept will be resolved using coopeative handling rules. Otherwise, intercept will be resovled immediately.
- `priority` <[number]> - Optional intercept abort priority. If provided, intercept is resolved using cooperative handling rules. Otherwise, intercept is resolved immediately.

- returns: <[Promise]>

Aborts request. To use this, request interception should be enabled with `page.setRequestInterception`.
Exception is immediately thrown if the request interception is not enabled.

#### httpRequest.continue([overrides])
#### httpRequest.abortErrorReason()

- 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.

#### httpRequest.continue([overrides], [priority])

- `overrides` <[Object]> Optional request overwrites, which can be one of the following:
- `url` <[string]> If set changes the request URL. This is not a redirect. The request will be silently forwarded to the new URL. For example, the address bar will show the original URL.
- `method` <[string]> If set changes the request method (e.g. `GET` or `POST`).
- `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.
- `priority` <[number]> - Optional intercept abort priority. If provided, intercept will be resolved using coopeative handling rules. Otherwise, intercept will be resovled immediately.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- `priority` <[number]> - Optional intercept abort priority. If provided, intercept will be resolved using coopeative handling rules. Otherwise, intercept will be resovled immediately.
- `priority` <[number]> - Optional intercept abort priority. If provided, intercept is resolved using cooperative handling rules. Otherwise, intercept is resolved immediately.

- returns: <[Promise]>

Continues request with optional request overrides. To use this, request interception should be enabled with `page.setRequestInterception`.
Expand All @@ -4461,6 +4647,22 @@ page.on('request', (request) => {
});
```

#### httpRequest.continueRequestOverrides()

- returns: <[Object]> Optional request overwrites, which can be one of the following:
- `url` <[string]> If set changes the request URL. This is not a redirect. The request will be silently forwarded to the new URL. For example, the address bar will show the original URL.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- `url` <[string]> If set changes the request URL. This is not a redirect. The request will be silently forwarded to the new URL. For example, the address bar will show the original URL.
- `url` <[string]> Changes the request URL if set. This is not a redirect — the request is silently forwarded to the new URL. For example, the address bar shows the original URL.

- `method` <[string]> If set changes the request method (e.g. `GET` or `POST`).
- `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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- `headers` <[Object]> If set changes the request HTTP headers. Header values will be converted to a string.
- `headers` <[Object]> If set changes the request HTTP headers. Header values are converted to a string.


Returns the most recent set of request overrides set with a previous call to continue() in Cooperative Mode.

#### httpRequest.enqueueInterceptAction(pendingHandler)

- `pendingHandler` <[function]> The request interception handler to enqueue

Enqueues a request handler for processing. This facilitates proper execution of async handlers.

#### httpRequest.failure()

- returns: <?[Object]> Object describing request failure, if any
Expand All @@ -4477,6 +4679,12 @@ page.on('requestfailed', (request) => {
});
```

#### httpRequest.finalizeInterceptions()

- returns: <[Promise<unknown>]>

When in Cooperative Mode, awaits pending interception handlers and then decides how to fulfill the request interception.

#### httpRequest.frame()

- returns: <?[Frame]> A [Frame] that initiated this request, or `null` if navigating to error pages.
Expand Down Expand Up @@ -4536,13 +4744,14 @@ console.log(chain.length); // 0
Contains the request's resource type as it was perceived by the rendering engine.
ResourceType will be one of the following: `document`, `stylesheet`, `image`, `media`, `font`, `script`, `texttrack`, `xhr`, `fetch`, `eventsource`, `websocket`, `manifest`, `other`.

#### httpRequest.respond(response)
#### httpRequest.respond(response, [priority])

- `response` <[Object]> Response that will fulfill this request
- `status` <[number]> Response status code, defaults to `200`.
- `headers` <[Object]> Optional response headers. Header values will be converted to a string.
- `contentType` <[string]> If set, equals to setting `Content-Type` response header
- `body` <[string]|[Buffer]> Optional response body
- `priority` <[number]> - Optional intercept abort priority. If provided, intercept will be resolved using coopeative handling rules. Otherwise, intercept will be resovled immediately.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- `priority` <[number]> - Optional intercept abort priority. If provided, intercept will be resolved using coopeative handling rules. Otherwise, intercept will be resovled immediately.
- `priority` <[number]> - Optional intercept abort priority. If provided, intercept is resolved using cooperative handling rules. Otherwise, intercept is resolved immediately.

- returns: <[Promise]>

Fulfills request with given response. To use this, request interception should
Expand All @@ -4569,6 +4778,12 @@ page.on('request', (request) => {

- returns: <?[HTTPResponse]> A matching [HTTPResponse] object, or `null` if the response has not been received yet.

#### httpRequest.responseForRequest()

- 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.

#### httpRequest.url()

- returns: <[string]> URL of the request.
Expand Down