请求拦截
🌐 Request Interception
一旦启用请求拦截,除非请求被继续、响应或中止,否则每个请求都会暂停。
🌐 Once request interception is enabled, every request will stall unless it's continued, responded or aborted.
中止所有图片请求的简单请求拦截器的示例:
🌐 An example of a naïve request interceptor that aborts all image requests:
import puppeteer from 'puppeteer';
(async () => {
const browser = await puppeteer.launch();
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')
)
interceptedRequest.abort();
else interceptedRequest.continue();
});
await page.goto('https://example.com');
await browser.close();
})();
多个拦截处理程序和异步解析
🌐 Multiple Intercept Handlers and Asynchronous Resolutions
默认情况下,如果在已调用其中任何一个之后调用 request.abort、request.continue 或 request.respond,Puppeteer 将引发 Request is already handled! 异常。
🌐 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.
始终假设一个未知的处理程序可能已经调用过 abort/continue/respond。即使你的处理程序是你注册的唯一一个,第三方包也可能注册他们自己的处理程序。因此,在调用 abort/continue/respond 之前,始终使用 request.isInterceptResolutionHandled 检查解析状态非常重要。
🌐 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
before calling abort/continue/respond.
重要的是,当你的处理程序正在等待异步操作时,拦截解析可能会由另一个监听器处理。因此,request.isInterceptResolutionHandled 的返回值仅在同步代码块中是安全的。始终同步地一起执行 request.isInterceptResolutionHandled 和 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:
/*
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:
/*
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();
});
对于更细粒度的自我检查(见下文的协作拦截模式),你也可以在使用 abort/continue/respond 之前同步调用 request.interceptResolutionState。
🌐 For finer-grained introspection (see Cooperative Intercept Mode below), you may
also call
request.interceptResolutionState
synchronously before using abort/continue/respond.
这是使用 request.interceptResolutionState 重写的上述示例
🌐 Here is the example above rewritten using request.interceptResolutionState
/*
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 {action} = interceptedRequest.interceptResolutionState();
if (action === InterceptResolutionAction.AlreadyHandled) 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 === InterceptResolutionAction.AlreadyHandled) {
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 ===
InterceptResolutionAction.AlreadyHandled
)
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 ===
InterceptResolutionAction.AlreadyHandled
)
return;
interceptedRequest.continue();
});
合作拦截模式
🌐 Cooperative Intercept Mode
request.abort、request.continue 和 request.respond 可以接受可选的 priority 以在协作拦截模式下工作。当所有处理程序都使用协作拦截模式时,Puppeteer 保证所有拦截处理程序将按注册顺序运行并等待完成。拦截将解析为最高优先级的结果。以下是协作拦截模式的规则:
- 所有分辨率必须向
abort/continue/respond提供一个数字priority参数。 - 如果任何分辨率未提供数字
priority,则遗留模式处于激活状态,而协作拦截模式处于非激活状态。 - 异步处理程序在拦截解析完成之前完成。
- 优先级最高的拦截决议“获胜”,即拦截最终会根据获得最高优先级的决议被中止/响应/继续。
- 在平局的情况下,
abort>respond>continue。
为了标准化,在指定合作拦截模式优先级时,使用 0 或 DEFAULT_INTERCEPT_RESOLUTION_PRIORITY(从 HTTPRequest 导出),除非你有明确的理由使用更高的优先级。这会优雅地优先选择 respond 而不是 continue,以及 abort 而不是 respond,并允许其他处理程序协同工作。如果你确实想使用不同的优先级,更高的优先级会胜过较低的优先级。允许使用负优先级。例如,continue({}, 4) 将胜过 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).
为了保持向后兼容性,任何在未指定 priority(旧版模式)的情况下解决拦截的处理程序都会导致立即解决。为了使合作拦截模式生效,所有解决方案必须使用 priority。在实际操作中,这意味着你仍然必须测试 request.isInterceptResolutionHandled,因为你无法控制的处理程序可能已经在没有优先级的情况下调用了 abort/continue/respond(旧版模式)。
🌐 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).
在此示例中,传统模式占主导,并且请求会立即中止,因为至少有一个处理程序在解析拦截时省略了 priority:
🌐 In this example, Legacy Mode prevails and the request is aborted immediately
because at least one handler omits priority when resolving the intercept:
// 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 => {
if (request.isInterceptResolutionHandled()) return;
// Control will never reach this point because the request was already aborted in Legacy Mode
// Cooperative Intercept Mode: votes for continue at priority 0.
request.continue({}, 0);
});
在此示例中,遗留模式占主导,并且请求会继续,因为至少有一个处理程序未指定 priority:
🌐 In this example, Legacy Mode prevails and the request is continued because at
least one handler does not specify a priority:
// Final outcome: immediate continue()
page.setRequestInterception(true);
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;
// Cooperative Intercept Mode: votes to abort at priority 0.
request.abort('failed', 0);
});
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;
// Control reaches this point because the request was cooperatively aborted which postpones resolution.
// { action: InterceptResolutionAction.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: InterceptResolutionAction.AlreadyHandled }, because continue in Legacy Mode was called
console.log(request.interceptResolutionState());
});
在此示例中,合作拦截模式处于激活状态,因为所有处理程序都指定了 priority。continue() 获胜,因为它的优先级高于 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().
// Final outcome: cooperative continue() @ 5
page.setRequestInterception(true);
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;
// Cooperative Intercept Mode: votes to abort at priority 10
request.abort('failed', 0);
});
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;
// Cooperative Intercept Mode: votes to continue at priority 5
request.continue(request.continueRequestOverrides(), 5);
});
page.on('request', request => {
// { action: InterceptResolutionAction.Continue, priority: 5 }, because continue @ 5 > abort @ 0
console.log(request.interceptResolutionState());
});
在此示例中,协作拦截模式处于激活状态,因为所有处理程序都指定了 priority。respond() 获胜,因为它的优先级与 continue() 相同,但 respond() 打败了 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().
// Final outcome: cooperative respond() @ 15
page.setRequestInterception(true);
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;
// Cooperative Intercept Mode: votes to abort at priority 10
request.abort('failed', 10);
});
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;
// Cooperative Intercept Mode: votes to continue at priority 15
request.continue(request.continueRequestOverrides(), 15);
});
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;
// Cooperative Intercept Mode: votes to respond at priority 15
request.respond(request.responseForRequest(), 15);
});
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;
// Cooperative Intercept Mode: votes to respond at priority 12
request.respond(request.responseForRequest(), 12);
});
page.on('request', request => {
// { action: InterceptResolutionAction.Respond, priority: 15 }, because respond @ 15 > continue @ 15 > respond @ 12 > abort @ 10
console.log(request.interceptResolutionState());
});
合作请求继续
🌐 Cooperative Request Continuation
Puppeteer 需要显式调用 request.continue(),否则请求将会挂起。即使你的处理程序打算不采取特殊操作,或者‘选择退出’,仍然必须调用 request.continue()。
🌐 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.
第一种情况(常见)是你的处理程序打算选择不对请求做任何特殊处理。它对进一步的操作没有意见,只是打算默认继续进行和/或将决定权交给可能有意见的其他处理程序。但如果没有其他处理程序,我们必须调用 request.continue() 来确保请求不会挂起。
🌐 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.
我们称之为无偏见的延续,因为其目的是在没有更好主意的情况下继续请求。对于这种类型的延续,请使用request.continue({...}, DEFAULT_INTERCEPT_RESOLUTION_PRIORITY)(或0)。
🌐 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.
第二种情况(少见)是你的处理程序实际上确实有自己的意见,并打算通过覆盖其他地方发出的较低优先级的 abort() 或 respond() 来强制继续。我们称之为有主见的继续。在这些少见的情况下,如果你打算指定一个覆盖的继续优先级,请使用自定义优先级。
🌐 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.
总而言之,要推断判断你使用 request.continue 是否只是为了默认/绕过行为,还是属于你的处理程序的预期使用场景。考虑在适用场景中使用自定义优先级,而在其他情况下使用默认优先级。请注意,你的处理程序可能同时有有主见(Opinionated)和无主见(Unopinionated)的情况。
🌐 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:
page.on('request', interceptedRequest => {
if (request.isInterceptResolutionHandled()) return;
if (
interceptedRequest.url().endsWith('.png') ||
interceptedRequest.url().endsWith('.jpg')
)
interceptedRequest.abort();
else interceptedRequest.continue();
});
要使用协作拦截模式,请升级 continue() 和 abort():
🌐 To use Cooperative Intercept Mode, upgrade continue() and abort():
page.on('request', interceptedRequest => {
if (request.isInterceptResolutionHandled()) return;
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 now uses Cooperative Intercept Mode instead.
然而,我们建议采用稍微更稳健的解决方案,因为上述方法会引入几个微妙的问题:
🌐 However, we recommend a slightly more robust solution because the above introduces several subtle issues:
- 向后兼容性。 如果任何处理程序仍然使用旧模式解析(即未指定优先级),即使你的处理程序先运行,该处理程序也会立即解析拦截。这可能会导致用户体验出现令人不安的行为,因为用户可能会发现你的处理程序没有解析拦截,而是其他处理程序优先处理,而用户所做的只是升级你的软件包。
- 硬编码优先级。 你的软件包用户无法指定处理程序的默认解析优先级。当用户希望根据使用场景调整优先级时,这一点可能会变得很重要。例如,一个用户可能希望你的软件包具有较高的优先级,而另一个用户可能希望它具有较低的优先级。
为了解决这两个问题,我们推荐的方法是从你的包中导出一个 setInterceptResolutionConfig()。然后用户可以调用 setInterceptResolutionConfig() 来显式激活你包中的合作拦截模式,以免他们对拦截方式的变化感到意外。他们还可以可选地使用 setInterceptResolutionConfig(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 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:
// Defaults to undefined which preserves Legacy Mode behavior
let _priority = undefined;
// Export a module configuration function
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')
)
interceptedRequest.abort('failed', _priority);
else
interceptedRequest.continue(
interceptedRequest.continueRequestOverrides(),
DEFAULT_INTERCEPT_RESOLUTION_PRIORITY, // Unopinionated continuation
);
});
如果你的包需要对解析优先级进行更细粒度的控制,请使用如下配置模式:
🌐 If your package calls for more fine-grained control over resolution priorities, use a config pattern like this:
interface InterceptResolutionConfig {
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: undefined, // Default to Legacy Mode
continuePriority: undefined, // Default to Legacy Mode
};
// Defaults to undefined which preserves Legacy Mode behavior
let _config: Partial<InterceptResolutionConfig> = {};
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', _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 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.