Puppeteer 如何拦截 beacon 请求和 service-worker.js 下载?
可以扩大拦截范围,但现有的 Network.setBypassServiceWorker 设置不能保证覆盖这两类请求。对于支持在 browser target 上使用 Fetch domain 的 Chrome/Chromium,可以尝试浏览器级 CDP 拦截,同时核对 beacon 的触发时机,以及 Service Worker 首次安装和更新时的不同下载路径。原问题没有提供 Puppeteer 和 Chrome 版本,因此无法保证某段代码能覆盖当前环境中的全部请求。
为什么 bypass 没有解决问题?
Network.setBypassServiceWorker({ bypass: true }) 让相关请求绕过 Service Worker 的处理,直接从网络加载。它不会禁止 Service Worker 注册,也不会把页面 CDP session 变成浏览器全局拦截器。Puppeteer 对应的接口是 page.setBypassServiceWorker(true)。参见 Puppeteer 接口说明。
页面请求被 Service Worker 接管、Service Worker 自己发起请求,以及浏览器下载或检查 Service Worker 脚本,是三种不同的行为。不能仅凭文件名或页面中的一次设置,就把它们视为同一条拦截路径。
代理日志中缺少新增请求头,也只能证明该请求头没有到达代理。还需要记录拦截事件,才能判断请求是否进入过处理器。
尝试在 browser target 上启用 Fetch 拦截
Chromium 当前源码中,browser target 注册了 FetchHandler,网络拦截链也会处理 browser 级拦截器。因此,如果所用的 Chrome/Chromium 支持这一实现,可以从 browser target 创建 CDP session,不必只从 page.target() 创建。这个方案依据 Chromium 的实现,具体支持情况需要在所用浏览器版本上确认。参见 browser target 实现和网络拦截实现。
下面的示例在打开业务页面前启用拦截,只向指定主机添加示例请求头:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const client = await browser.target().createCDPSession();
// 替换为需要添加请求头的实际主机。
const allowedHosts = new Set(['example.com']);
client.on('Fetch.requestPaused', event => {
void (async () => {
const { requestId, request, resourceType } = event;
console.log('[paused]', resourceType, request.method, request.url);
const url = new URL(request.url);
const shouldAddHeader =
(url.protocol === 'http:' || url.protocol === 'https:') &&
allowedHosts.has(url.hostname);
if (!shouldAddHeader) {
await client.send('Fetch.continueRequest', { requestId });
return;
}
const headers = Object.entries(request.headers)
.filter(([name]) => name.toLowerCase() !== 'x-proxy-tag')
.map(([name, value]) => ({ name, value: String(value) }));
headers.push({ name: 'x-proxy-tag', value: 'example-value' });
await client.send('Fetch.continueRequest', {
requestId,
headers,
});
})().catch(error => {
console.error('Fetch interception failed:', error);
});
});
await client.send('Fetch.enable', {
patterns: [{ urlPattern: '*', requestStage: 'Request' }],
});
const page = await browser.newPage();
await page.setBypassServiceWorker(true);
await page.goto('https://example.com');
// 在这里执行会触发目标请求的页面操作。
// 确认请求完成后,再关闭 browser。
示例省略了现有代理配置。实际使用时,应在 puppeteer.launch() 中保留你的代理设置。x-proxy-tag 和 example-value 都是占位值。
启用 Fetch 后,匹配的请求会暂停,必须继续、终止或提供响应。处理器发生错误时要及时处理,否则请求可能一直挂起。headers 参数用于覆盖请求头,因此示例先保留原有字段,再追加自定义字段。相关行为见 CDP Fetch 文档。
排查时建议只保留这一套修改请求头的拦截逻辑,避免页面拦截器、插件和 browser 级拦截器同时修改同一个请求。
分别验证 beacon 和脚本下载
对于 beacon,先在页面仍打开时主动触发一次请求,再测试页面隐藏、跳转或关闭时的请求。navigator.sendBeacon() 使用适合页面退出场景的异步发送机制,请求可以在页面卸载后继续执行。因此,只观察页面仍存活时的 session,可能不足以解释退出阶段的流量。参见 Beacon 规范。
不要只按 xhr 或 fetch 类型筛选。先记录所有暂停事件的 URL、方法和资源类型,再与代理日志对应。如果主动触发时能够捕获,退出阶段却有遗漏,应重点检查页面关闭和 session 生命周期。
对于 service-worker.js,分别测试首次注册和已有注册后的更新检查。Chromium 为 Service Worker 主脚本准备了独立的网络处理路径,不能假定它一定经过页面拦截器。如果请求已经发出,之后再监听 worker target,也无法追溯修改那次请求。上述网络实现展示了主脚本加载的专门处理。
验证时应对照暂停事件和代理实际收到的请求头。仅看到 Network 事件,不能证明请求已经被暂停并修改。
必须覆盖全部代理流量时怎么办?
如果所用版本不支持 browser target 上的 Fetch.enable,或者实际测试仍有脚本更新请求遗漏,可以在浏览器前面的本地代理统一添加请求头,再转发给上游代理。这个位置更贴近“所有到达上游代理的请求都必须带标记”的需求。
对于 HTTPS,普通 CONNECT 隧道无法修改加密隧道内的 HTTP 请求头。若需要添加这类请求头,代理必须具备相应的 HTTPS 解密与转发配置;给 CONNECT 请求加头,也不等于给其中的 service-worker.js 请求加头。
备注:内容仅供参考。
