Puppeteer HTTPRequest.failure() 深度解析:捕获请求失败并读取 errorText
导读
HTTPRequest.failure() 是 Puppeteer 中判断网络请求是否失败的入口方法。页面发出的每个网络请求,在生命周期结束时会触发 requestfinished 或 requestfailed 事件,而 failure() 的作用就是在一个失败请求的 requestfailed 回调中,取出可读的失败原因(如 net::ERR_FAILED)。本文将结合该方法的官方文档声明与 puppeteer-core 的 CDP、WebDriver BiDi 双协议实现,讲清它的签名语义、触发场景、与 requestfinished 的边界划分,并给出可直接落地的日志监控、资源诊断与故障排查代码。
一、方法签名与返回值语义
failure() 是抽象基类 HTTPRequest 上的抽象方法,在 docs/api/puppeteer.httprequest.md 中登记的完整声明如下(对应 API 声明文档):
class HTTPRequest {
abstract failure(): {
errorText: string;
} | null;
}
返回类型: { errorText: string } | null
核心语义可以拆成三点:
- 请求成功时永远返回
null——只要请求最终被服务端正常响应,failure()就是null。 - 请求失败时返回对象——形如
{ errorText: string },其中errorText是人类可读的失败原因文本,例如 Chrome 上的net::ERR_FAILED。 - 失败也不保证一定有文本——即使请求真的失败了,
errorText也"不一定存在"。这正是指南标题文档中那句It is not guaranteed that there will be failure text if the request fails的含义。因此在业务代码中,应把errorText视为可选字段而非必然存在,这也是官方示例使用解构取值时值得注意的边界。
方法在抽象基类中的位置可参见 packages/puppeteer-core/src/api/HTTPRequest.ts 的 JSDoc 注释与抽象签名,它的注释内容与该 API 文档完全一致,属于核心请求对象的稳定接口之一。
二、生命周期背景:何时会触发失败
failure() 单独使用没有意义,它必须配合页面请求事件体系来理解。根据 docs/api/puppeteer.httprequest.md 与 docs/api/index.md 中的说明,每当页面发出一个网络请求,Puppeteer 的 page 对象会按以下规则派发事件:
request:请求被页面发出时触发;requestfinished:响应体下载完成、请求正常结束时触发;requestfailed:请求在某个环节失败时触发,它替代requestfinished,二者在同一请求上互斥;- 上述事件回调中拿到的都是代表该请求的
HTTPRequest实例。
因此,failure() 的正确使用场景几乎总是在 requestfailed 回调中。事件与 failure() 的对应关系可以参考 docs/api/puppeteer.pageevent.md 中关于 requestfailed(事件名 "requestfailed")的说明。
关键边界:HTTP 错误码 ≠ 请求失败
一个非常容易混淆的点是:HTTP 错误响应(如 404、503)在 HTTP 协议层面属于成功响应,请求会照常以 requestfinished 结束,而不会触发 requestfailed。也就是说,用 page.goto() 打开一个 404 页面、加载一个 503 的资源,在请求生命周期上并不算"失败"。
同理,重定向(redirect)也不构成失败:请求收到 3xx 重定向响应后会以 requestfinished 正常结束,随后浏览器向重定向目标发出一个新请求,新请求会继续走独立的生命周期。请求失败通常意味着连接层、DNS、TLS、超时、被浏览器取消或被人为中断等更底层的问题。
三、源码级原理:CDP 与 WebDriver BiDi 的两套实现
Puppeteer 支持基于 CDP(Chrome DevTools Protocol)和基于 WebDriver BiDi 两种协议后端。failure() 作为抽象方法在两类请求对象上各有落地实现,从源码结构看两者共享同一套对外语义,但数据来源不同。
3.1 CDP 实现:由 Network.loadingFailed 事件驱动
在 CDP 通道中,请求失败信息由网络管理器统一收集。关键链路位于 packages/puppeteer-core/src/cdp/NetworkManager.ts 的 #emitLoadingFailed:
#emitLoadingFailed(client, event) {
const request = this.#networkEventManager.getRequest(event.requestId);
// 部分 requestId 永远不会收到 requestWillBeSent 事件(见 crbug.com/750469)
if (!request) return;
this.#adoptCdpSessionIfNeeded(client, request);
request._failureText = event.errorText; // 写入浏览器下发的失败文本
const response = request.response();
if (response) {
response._resolveBody(); // 让挂起取 body 的调用尽快结束
}
this.#forgetRequest(request, true);
this.emit(NetworkManagerEvent.RequestFailed, request); // 最终派发 requestfailed
}
可以清晰看到:浏览器通过 CDP 的 Network.loadingFailed 事件上报 errorText,Puppeteer 把它暂存到请求对象内部字段,随后派发 RequestFailed 事件。这一事件最终经由 packages/puppeteer-core/src/cdp/Page.ts 转发为面向使用者的 page 级 requestfailed 事件。
CdpHTTPRequest.failure() 的实现位于 packages/puppeteer-core/src/cdp/HTTPRequest.ts:
override failure(): {errorText: string} | null {
if (!this._failureText) {
return null; // 没有失败文本,按"未失败"处理
}
return {
errorText: this._failureText,
};
}
注意这里的细节:即便底层确实发生了失败,如果 CDP 上报的 errorText 为空字符串或未定义,该方法也会返回 null。这正是文档中"失败也不保证有 failure text"警告的实现体现——用 failure() 判空不能 100% 等价于判断请求是否失败,判失败应优先以是否收到 requestfailed 事件为准。
3.2 WebDriver BiDi 实现:读取请求对象的 error 字段
在 BiDi 协议通道中,BidiHTTPRequest 直接使用协议层请求对象的 error 字段,见 packages/puppeteer-core/src/bidi/HTTPRequest.ts:
override failure(): {errorText: string} | null {
if (this.#request.error === undefined) {
return null;
}
return {errorText: this.#request.error};
}
从源码结构看,BiDi 后端与 CDP 后端对使用者的可见行为保持一致:error 未定义(即未失败)时返回 null,否则返回包装后的 { errorText }。
四、官方示例与实战模式
4.1 官方示例:记录所有失败请求
failure() API 文档 给出的官方示例是监听 requestfailed 并打印失败请求的 URL 与错误文本:
page.on('requestfailed', request => {
console.log(request.url() + ' ' + request.failure().errorText);
});
4.2 更稳健的写法
考虑到"失败时不保证有 errorText"以及 failure() 理论上可能返回 null 两种边界,生产代码建议写成防御式版本,把失败信息结构化收集而不是直接拼字符串:
const failedRequests: Array<{url: string; errorText: string | null}> = [];
page.on('requestfailed', request => {
failedRequests.push({
url: request.url(),
// failure() 可能为 null;即便非 null,errorText 也不保证存在
errorText: request.failure()?.errorText ?? null,
});
});
requestfailed 事件回调里还能组合使用 HTTPRequest 的其他方法做精细诊断,比如 frame()(发出请求的 frame)、resourceType()(资源类型,如 stylesheet、image、fetch)、response()(失败请求的 response 恒为 null)。例如按资源类型归类失败:
page.on('requestfailed', request => {
console.log({
url: request.url(),
resourceType: request.resourceType(),
errorText: request.failure()?.errorText ?? '(no error text)',
});
});
五、errorText 的实际取值与协议差异
errorText 的内容来自浏览器协议层的原始文本,因此在不同浏览器后端上会呈现不同的字符串风格。仓库测试 test/src/network.test.ts 中的 Page.Events.RequestFailed 用例直接验证了这一行为:
- 测试先开启请求拦截
page.setRequestInterception(true),对 URL 以css结尾的请求调用request.abort(),其余请求request.continue(); - 随后断言恰好收到 1 个
requestfailed; - 对失败请求断言:
response()为null、frame()有值; - 失败文本按浏览器区分断言:Chrome 期望
net::ERR_FAILED,Firefox 期望NS_ERROR_ABORT。
if (isChrome) {
expect(failedRequest.failure()!.errorText).toBe('net::ERR_FAILED');
} else {
expect(failedRequest.failure()!.errorText).toBe('NS_ERROR_ABORT');
}
这说明两个要点:
errorText不是 Puppeteer 生成的统一错误码,而是直接透传浏览器的网络错误文本——Chrome 使用net::前缀(Chromium 网络栈错误),Firefox 使用NS_ERROR_*风格(Gecko 的 nsresult 错误)。- 通过请求拦截主动
abort()一个请求,同样会走requestfailed路径并产生可读取的失败文本,因此failure()也是判断"资源是否被拦截/中止"的可靠手段。
在 test/src/navigation.test.ts 的导航测试中同样能看到:请求事件按 request → requestfailed 的顺序触发,失败请求会替代性地出现在失败事件序列里,进一步佐证了"失败请求不会再触发 requestfinished"的生命周期约定。
常见失败场景归类
综合文档与实现,可用 failure() 判别的失败一般包括:
- 连接类失败:DNS 解析失败、TCP 连接被拒绝/超时、TLS 握手失败,如
net::ERR_NAME_NOT_RESOLVED、net::ERR_CONNECTION_REFUSED; - 协议层中断:响应中途连接断开、下载被中止,如
net::ERR_FAILED、net::ERR_ABORTED; - 人为中止:通过请求拦截调用
request.abort()(配合 setRequestInterception 使用)触发的失败; - 资源被浏览器策略阻止等底层原因。
不属于失败的有:任何得到 HTTP 状态码的响应(含 404/503)、被 continue() 放行并成功完成的请求、以及重定向链上的中间请求。
六、与 requestfinished 配套:监控完整资源生命周期
把 failure() 放进完整的生命周期中才能写出有实用价值的监控代码。官方文档明确过:同一请求要么 requestfinished、要么 requestfailed。可以这样统计页面资源成功率:
const stats = {total: 0, failed: 0, byType: new Map<string, number>()};
page.on('requestfinished', request => {
stats.total++;
const t = request.resourceType();
stats.byType.set(t, (stats.byType.get(t) ?? 0) + 1);
});
page.on('requestfailed', request => {
stats.total++;
stats.failed++;
console.error(`[资源加载失败] ${request.url()}`);
console.error(` 错误: ${request.failure()?.errorText ?? '(无错误文本)'}`);
});
await page.goto('https://example.com/', {waitUntil: 'networkidle0'});
console.log(`资源总数=${stats.total}, 失败=${stats.failed}`);
几点工程建议:
- 不要在
requestfinished/requestfailed之外盲调failure():对于仍在途或已成功的请求它是null,用途有限;把它与对应事件绑定才是正确姿势。 - 对
errorText做归一化:如果监控系统需要跨 Chrome/Firefox 比对,建议把net::ERR_*与NS_ERROR_*映射成内部统一错误类别,而不是直接存储原始串。 - 失败请求没有响应体:
failure()非null时,request.response()返回null(测试 test/src/network.test.ts 已断言),所以不要在失败分支尝试读取 body。 - 与拦截配合做降级:当检测到第三方资源(埋点、广告 SDK、字体)失败且可容忍时,可以用 request interception 决定重试或放行;测试 test/src/requestinterception-experimental.test.ts 也验证了正常完成的请求其
failure()为null。
总结
failure()返回null或{ errorText: string },仅用于查询"请求是否失败及失败原因",通常在page的requestfailed事件回调内调用;errorText透传浏览器底层错误文本(Chrome 为net::ERR_*,Firefox 为NS_ERROR_*),且失败也不保证存在文本,代码必须做空值兜底;- HTTP 4xx/5xx、重定向都不算请求失败,不会触发
requestfailed; - CDP 通道的数据链路是
Network.loadingFailed→NetworkManager.#emitLoadingFailed写入_failureText→ 派发事件,见 packages/puppeteer-core/src/cdp/NetworkManager.ts;BiDi 通道则直接读取协议请求的error字段,见 packages/puppeteer-core/src/bidi/HTTPRequest.ts; - 相关行为均有测试佐证,可对照 test/src/network.test.ts 深入验证。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00