Puppeteer 请求拦截与改写核心 API:HTTPRequest.continue() 深度实战指南
本文以 docs/api/puppeteer.httprequest.continue.md 为骨架,系统讲解 Puppeteer(JavaScript API for Chrome and Firefox)中 HTTPRequest.continue() 的使用方式与底层原理。该方法与 Page.setRequestInterception() 配合,是拦截并改写页面发出的任意 HTTP 请求(Header、URL、Method、POST Body)的标准入口,是广告过滤、Mock 数据、请求降级与跨域改造类爬虫/自动化脚本的核心设施。读完本文,你将掌握请求拦截的完整调用链、ContinueRequestOverrides 的四个可改写字段、可选的协作式优先级语义(cooperative interception),以及 CDP 与 WebDriver BiDi 两种协议通道下的真实实现差异。
为什么需要 continue():一次请求、三种归宿
在现代浏览器自动化场景中,"监听请求"只是第一步,真正的难点是"在请求真正发出前改写它"。Puppeteer 为此设计了请求拦截(request interception)机制:当通过 Page.setRequestInterception() 开启拦截后,页面的每个网络请求都会被挂起(stall),直到开发者显式地给出处理结论。结论只有三种:
request.abort()—— 终止该请求(例如拦截图片、屏蔽追踪域名);request.respond()—— 伪造响应,直接返回本地 Mock 数据,请求不会到达服务器;request.continue()—— 放行请求,并且允许在放行前对请求本身做改写(这是本文主角)。
从源码注释看,三者是并列的"处理动作":InterceptResolutionAction 枚举同时收录了 'abort' | 'respond' | 'continue',另有 'disabled' | 'none' | 'already-handled' 三种状态值(见 packages/puppeteer-core/src/api/HTTPRequest.ts)。而拦截的"终结"发生在 finalizeInterceptions():它会先依次执行入队的处理器,再根据最终决议动作分别调用 _abort()、_respond() 或 _continue()(见 packages/puppeteer-core/src/api/HTTPRequest.ts)。continue() 正是触发 _continue() 这一底层放行通道的公共 API。
方法签名与核心语义
官方 API 文档给出的完整签名如下(docs/api/puppeteer.httprequest.continue.md):
class HTTPRequest {
continue(
overrides?: ContinueRequestOverrides,
priority?: number,
): Promise<void>;
}
该方法属于 HTTPRequest 实例(由 page.on('request', req => ...) 事件回调提供),返回 Promise<void>。两个参数都可选。语义要点:
- 无参调用
request.continue()即"原样放行",等价于不改写任何字段; - 传入
overrides则按需改写 URL、Method、PostData 或 Headers; - 传入
priority则走协作式拦截决议(见下文专节),否则立即执行放行。
在 packages/puppeteer-core/src/api/HTTPRequest.ts 中的真实实现证实了这一语义:continue() 首先调用 verifyInterception() 做前置校验,然后判断 canBeIntercepted(),通过后若未提供 priority,直接 await this._continue(overrides) 立即放行。
前置条件:必须先开启请求拦截
文档的 Remarks 部分明确强调了两点(docs/api/puppeteer.httprequest.continue.md):
To use this, request interception should be enabled with
Page.setRequestInterception(). Exception is immediately thrown if the request interception is not enabled.
- 必须先调用
page.setRequestInterception(true); - 若拦截未开启却调用
continue(),会立即抛出异常。
异常的具体文案藏在源码的 verifyInterception() 中(packages/puppeteer-core/src/api/HTTPRequest.ts):
protected verifyInterception(): void {
assert(this.interception.enabled, 'Request Interception is not enabled!');
assert(!this.interception.handled, 'Request is already handled!');
}
即未开启时抛 Request Interception is not enabled!,请求已被处理过(例如已被 abort/respond/continue 命中)再次调用时抛 Request is already handled!。后者是新手最常见的报错来源——一个请求只能被处理一次。
哪些请求可以被 continue?
拦截并非对所有请求生效。canBeIntercepted() 的实现给出了边界(packages/puppeteer-core/src/cdp/HTTPRequest.ts):
protected canBeIntercepted(): boolean {
return !this.url().startsWith('data:') && !this._fromMemoryCache;
}
data:协议的请求(内联图片等 data URL)不可拦截;- 命中浏览器内存缓存(memory cache)的请求不可拦截。
当 canBeIntercepted() 返回 false 时,continue() 会静默直接返回而不报错。另外需注意:HTTP 层面"成功"的 404/503 等错误响应仍属于可被正常放行的请求,只有真正失败(如 net::ERR_FAILED)才会走 requestfailed 事件路径。
改写参数:ContinueRequestOverrides 逐字段拆解
overrides 的类型为 ContinueRequestOverrides,它只包含四个可选字段(定义见 packages/puppeteer-core/src/api/HTTPRequest.ts,属性表见 docs/api/puppeteer.continuerequestoverrides.md):
| 字段 | 类型 | 说明与默认值 |
|---|---|---|
headers |
Record<string, string>(可选) |
覆盖请求头;值为 undefined 表示删除该请求头。不传则保持原请求头。 |
method |
string(可选) |
改写请求方法(如 GET、POST)。不传则保持原方法。 |
postData |
string(可选) |
改写 POST 请求体。仅对带 body 的方法有意义。不传则保持原请求体。 |
url |
string(可选) |
改写目标 URL。注意:这只是改变请求的 URL,并非重定向(This is not a redirect),不会触发 3xx 跳转语义。不传则保持原 URL。 |
要点提示:
headers中的 key 一律小写处理。官方文档示例中request.headers()返回的头部键已全部是小写,因此用Object.assign({}, request.headers(), ...)做增量修改是最安全的做法;postData字段为字符串。注意获取侧 API:request.postData()已标记@deprecated,官方推荐改用request.fetchPostData()获取完整 body(见 docs/api/puppeteer.httprequest.fetchpostdata.md 与 docs/api/puppeteer.httprequest.postdata.md)——当 body 过长或不易解码时postData()可能返回undefined,此时应使用fetchPostData()。
实战一:改写与删除请求头(文档示例)
官方文档给出的典型用法是"读原头 → 改头 → continue"(docs/api/puppeteer.httprequest.continue.md):
await page.setRequestInterception(true);
page.on('request', request => {
// Override headers
const headers = Object.assign({}, request.headers(), {
foo: 'bar', // set "foo" header
origin: undefined, // remove "origin" header
});
request.continue({headers});
});
这段代码演示了 continue() 的两个独特能力:
- 新增请求头:把
foo头置为bar; - 删除请求头:把
origin置为undefined—— 注意这里并不是把值置成字符串"undefined",而是语义化的"移除"。这一约定在类型Record<string, string>之外的取值空间中通过底层序列化逻辑实现。
实战二:改写 URL、方法与请求体
continue() 的四个字段可自由组合。例如把某次表单提交从 GET 改写为 POST 并携带新 body,或把资源请求指向本地镜像:
await page.setRequestInterception(true);
page.on('request', request => {
if (request.url().includes('/api/v2/legacy')) {
// 改写目标地址:不触发重定向,而是直接发出新 URL 的请求
request.continue({url: 'https://example.com/api/v3/legacy'});
} else if (request.url().endsWith('/login')) {
request.continue({
method: 'POST',
postData: 'username=admin&from=interceptor',
headers: {
...request.headers(),
'content-type': 'application/x-www-form-urlencoded',
},
});
} else {
request.continue(); // 其余请求原样放行,这一分支必不可少
}
});
⚠️ 若开启了拦截却没有对某请求调用
continue()/respond()/abort(),该请求会一直挂起直到超时。文档 Page.setRequestInterception() 明确写道:Once request interception is enabled, every request will stall unless it's continued, responded or aborted; or completed using the browser cache. 因此拦截处理器必须保证每个请求都有归宿。
底层实现:CDP 与 WebDriver BiDi 双通道
Puppeteer 当前同时支持 Chrome(CDP 协议)与 Firefox(WebDriver BiDi 协议),_continue() 的协议层实现因此存在两套。这一事实可直接从仓库源码确认。
CDP 通道:Fetch.continueRequest
CDP 实现位于 packages/puppeteer-core/src/cdp/HTTPRequest.ts:
async _continue(overrides: ContinueRequestOverrides = {}): Promise<void> {
const {url, method, postData, headers} = overrides;
this.interception.handled = true;
const postDataBinaryBase64 = postData
? stringToBase64(postData)
: undefined;
if (this._interceptionId === undefined) {
throw new Error(
'HTTPRequest is missing _interceptionId needed for Fetch.continueRequest',
);
}
await this.#client
.send('Fetch.continueRequest', {
requestId: this._interceptionId,
url,
method,
postData: postDataBinaryBase64,
headers: headers ? headersArray(headers) : undefined,
})
.catch(error => {
this.interception.handled = false;
return handleError(error, this.#logger);
});
}
值得注意的实现细节:
- 底层走的是 Chrome DevTools Protocol 的
Fetch.continueRequest域调用,requestId即该请求的拦截 ID(_interceptionId); postData字符串先经stringToBase64()转成 base64 再传给协议层,因为 CDP 的 Fetch 域要求 base64 编码的 body;headers经headersArray()序列化为协议要求的键值数组;值为undefined的头部正是在序列化层被剔除,从而实现"删头"语义;- 错误回滚:协议调用失败时会把
interception.handled复位为false并记录日志,意味着该请求仍可被后续处理器重新处理,而不是"卡死"在已处理状态; - 若
_interceptionId缺失(如该请求本不可被拦截)则抛错说明。
WebDriver BiDi 通道:request.continueRequest
BiDi(Firefox)实现位于 packages/puppeteer-core/src/bidi/HTTPRequest.ts,同样是先标记 handled = true,再调用 this.#request.continueRequest(...),body 以结构化 {type, value} 形式(而非 CDP 的 base64 字符串)传递:
override async _continue(overrides: ContinueRequestOverrides = {}): Promise<void> {
const headers: Bidi.Network.Header[] = getBidiHeaders(overrides.headers);
this.interception.handled = true;
return await this.#request.continueRequest({
url: overrides.url,
method: overrides.method,
body: overrides.postData ? { /* ...结构化 body... */ } : undefined,
headers,
});
}
因此"上层 API 一致、下层协议自适应"是本方法的架构特点:无论浏览器走 CDP 还是 BiDi,开发者写的 request.continue({...}) 代码都无需变化。
协作式拦截:priority 参数与决议规则
continue() 的第二个参数 priority 是 Puppeteer 较新的高级特性。文档原话(docs/api/puppeteer.httprequest.continue.md):
If provided, intercept is resolved using cooperative handling rules. Otherwise, intercept is resolved immediately.
立即决议模式(不传 priority)
不传 priority 时 continue() 直接执行 _continue() 放行请求。与此同时若页面事件流中另有一个处理器调用了 respond(),二者会产生竞态——后执行的调用很可能触发 Request is already handled! 异常。这正是引入协作模式的原因。
协作决议模式(传 priority)
传 priority 时 continue() 不会立即放行,而是先登记意图:把本次 overrides 存入 interception.requestOverrides,再与已登记的其它动作按优先级"协商"(实现见 packages/puppeteer-core/src/api/HTTPRequest.ts)。规则如下:
- 若此前没有更高优先级的动作,把决议动作置为
Continue并记录本次 priority; - 若本次 priority 高于已登记动作,则覆盖为
Continue; - 若 priority 相等:当已登记动作是
abort或respond时,保持原动作(中止/伪响应优先于放行);否则置为Continue; abort()/respond()同样支持 priority,最终谁胜出由数值大小决定。
该机制允许多个独立的拦截处理器(如"广告拦截扩展"与"业务 Mock 中间件")各自声明优先级,而不会互相踩踏。仓库为此提供了默认常量与配套方法:
DEFAULT_INTERCEPT_RESOLUTION_PRIORITY = 0(packages/puppeteer-core/src/api/HTTPRequest.ts),测试中普遍使用request.continue({}, 0)这样的写法;request.continueRequestOverrides()可读取将被用于放行的 overrides(packages/puppeteer-core/src/api/HTTPRequest.ts);request.interceptResolutionState()返回{action, priority}决议快照(packages/puppeteer-core/src/api/HTTPRequest.ts);request.enqueueInterceptAction(handler)可把异步处理器加入队列,保证在拦截被 finalize 前全部执行完毕(packages/puppeteer-core/src/api/HTTPRequest.ts)。
协作式语义并非纸上谈兵,仓库自带的实验性测试 test/src/requestinterception-experimental.test.ts 中大量出现 request.continue({}, 0) 与 request.abort('aborted', 1)、request.respond({...}, 1) 混用的场景,通过数值优先级验证"高优先级 abort/respond 覆盖低优先级 continue"的决议结果,可作为理解该机制的活教材。
最佳实践与常见坑位清单
最后,把容易踩坑的点汇总如下:
- 先开闸再改写:忘记
await page.setRequestInterception(true)就调用continue()会同步抛出Request Interception is not enabled!,异常发生在verifyInterception()阶段(packages/puppeteer-core/src/api/HTTPRequest.ts); - 每请求必有归宿:拦截开启后所有请求默认挂起,
else分支务必补request.continue(); - 一请求一处理:同一请求多次调用任意处理 API 会抛
Request is already handled!;需要"多个处理器共同决定"时应走priority协作模式或enqueueInterceptAction(); - URL 改写不是重定向:
overrides.url只改变本次发出的请求地址,不产生 3xx 跳转链,也不会改变浏览器地址栏; - 删除头部用
undefined:在 headers 对象中将某键设为undefined才能在序列化时移除该头,直接传空字符串可能会被当作合法值发送; - 边界请求放行受限:
data:URL 与命中内存缓存的请求不可被拦截,continue()对它们会静默跳过; - 保持事件监听优先注册:
page.on('request', ...)的处理器应在page.goto()等导航动作之前注册,避免页面首屏请求在监听器就绪前已经发出;需要保持单页长期监听时,通常选择在创建 page 后立即开启拦截。
进一步阅读
- 拦截开关与全流程:查看 Page.setRequestInterception() 文档,其中附有"终止所有图片请求"的完整示例;
- 兄弟 API:对比阅读 HTTPRequest.abort()(终止请求)与 HTTPRequest.respond()(伪响应);
- 覆盖参数类型:见 ContinueRequestOverrides 接口文档;
- 底层调用链实现:见 packages/puppeteer-core/src/api/HTTPRequest.ts、packages/puppeteer-core/src/cdp/HTTPRequest.ts、packages/puppeteer-core/src/bidi/HTTPRequest.ts;
- 浏览器差异化支持说明:见 docs/webdriver-bidi.md 与 docs/supported-browsers.md。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00