 实战与源码解析:用自定义响应拦截并接管页面网络请求)
Puppeteer HTTPRequest.respond() 实战与源码解析用自定义响应拦截并接管页面网络请求【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteerHTTPRequest.respond() 是 Puppeteer 请求拦截体系中用于直接伪造并返回一个响应的核心方法它允许你在页面真实发出网络请求之前用一段内存中的状态码、响应头与响应体把它喂饱从而实现对 API 数据、静态资源乃至错误页的完全可控模拟。本文将基于 Puppeteer 仓库中 docs/api/puppeteer.httprequest.respond.md 的 API 契约结合 packages/puppeteer-core/src/api/HTTPRequest.ts 与 packages/puppeteer-core/src/cdp/HTTPRequest.ts 的源码实现讲透它的参数语义、前置条件、合作式优先级拦截原理与常见陷阱读完即可在自己的测试与爬虫项目中直接落地。HTTPRequest.respond() 在请求拦截体系中的定位在 Puppeteer 中开启请求拦截后页面发出的每一个网络请求都会先经过 JavaScript 层由开发者决定它的最终命运。与其配套的是三个互斥动作它们的 API 文档互相引用、构成完整的拦截决策面HTTPRequest.abort()以某个网络错误码如failed、aborted、internetdisconnected中止请求HTTPRequest.continue()放行请求并可选地改写 URL、方法、请求头与 bodyHTTPRequest.respond()本文主角不真正发往网络而是返回一个调用方伪造的响应。respond的典型适用场景包括单元测试 / E2E 测试中 mock 后端接口返回、屏蔽并替换第三方统计脚本、模拟 404/500 等异常响应以验证前端错误处理逻辑、离线环境加载图片等静态资源等。仓库中的拦截测试大量使用该能力例如 test/src/requestinterception.test.ts 中就用request.respond返回 201、422、302 乃至image/png图片二进制等各类伪造响应来验证页面行为。方法签名与返回值class HTTPRequest { respond( response: PartialResponseForRequest, priority?: number, ): Promisevoid; }返回Promisevoid代表本次伪造响应的发送指令已被提交底层是向浏览器发送 CDP 指令详见下文实现原理章节而非页面真正渲染完毕。该方法既不需要也不应该await在事件回调内部阻塞——事件回调里通常直接调用后返回如官方示例所示。参数详解response 与 priorityresponsePartialresponse的类型是PartialResponseForRequest。完整接口定义见 puppeteer.responseforrequest.mdRequired response data to fulfill a request with.其字段在源码 packages/puppeteer-core/src/api/HTTPRequest.ts#L45-L58 中定义如下字段类型说明默认值statusnumberHTTP 状态码源码中缺省回落到200见 CDP 实现response.status || 200headersRecordstring, string \| string[] \| unknown可选的响应头。值为数组时会被映射为多个同名字段适用于set-cookie等多值头非数组值统一经String()转换不传则不附加额外响应头contentTypestring响应Content-Type会以content-type响应头形式下发不传则由浏览器自行嗅探bodystring \| Uint8Array响应体文本或二进制无 bodycontent-length也不会被自动补写见下文需要特别强调两处Partial 带来的便利接口定义中 4 个字段全部是必填没有标 Optional但respond接收的是Partial因此你可以只给出其中一部分如官方 404 示例只传了status/contentType/body省去headers。status缺省时并非返回undefined而是由实现层兜底为200这一点可从 packages/puppeteer-core/src/cdp/HTTPRequest.ts#L268 的const status response.status || 200;得到印证。headers中值转换的具体规则在 puppeteer.responseforrequest.md 与源码注释中均有陈述数组值逐个String(...)支持同名字段多值非数组值直接String(value)。此外 CDP 实现中所有响应头 key 都会被toLowerCase()归一化后再拼装。priority合作式拦截的决议优先级priority参数可空。它的行为语义在两个 API 文档respond/continue/abort中一致均为If provided, intercept is resolved using cooperative handling rules. Otherwise, intercept is resolved immediately. 提供该参数时本次拦截决议走合作式规则否则立刻决议。含义拆解如下不传 priorityrespond立刻生效直接把伪造响应下发给浏览器源码中表现为直接await this._respond(response)传 priority调用方只表达意图 权重多个监听器如多个page.on(request, ...)回调的决议会先按优先级登记最终由优先级最高者胜出实现更细粒度的协作。仓库在 packages/puppeteer-core/src/api/HTTPRequest.ts#L72 导出了一个默认优先级常量DEFAULT_INTERCEPT_RESOLUTION_PRIORITY 0对应 API 文档 puppeteer.default_intercept_resolution_priority.md决议动作的枚举见 puppeteer.interceptresolutionaction.mdabort | respond | continue | disabled | none | already-handled状态结构见 puppeteer.interceptresolutionstate.md。优先级决议的详细源码逻辑在下一章展开。前置条件必须先开启请求拦截respond与abort、continue一样必须在 Page.setRequestInterception() 开启后才有意义。该方法文档明确一旦开启拦截每个请求都会停住stall除非它被 continue / respond / abort或命中浏览器缓存完成——否则页面将永远等不到该资源。从源码看这个前置条件在 packages/puppeteer-core/src/api/HTTPRequest.ts#L389-L392 的verifyInterception()中被强制执行protected verifyInterception(): void { assert(this.interception.enabled, Request Interception is not enabled!); assert(!this.interception.handled, Request is already handled!); }由此可知两条硬性规则未开启拦截就调用respond会立刻抛出Request Interception is not enabled!即官方文档 Remarks 所述的 Exception is immediately thrown一个请求只能被处置一次——若已被abort/continue/respond处置过再对同一请求调用任一处置方法会抛出Request is already handled!。实战示例以下示例均来自官方文档或仓库测试的等价写法可在 Node.js ESM 环境中直接运行验证。示例 1官方示例——把一切请求都返回 404这是 puppeteer.httprequest.respond.md 自带的经典示例用于演示全量接管await page.setRequestInterception(true); page.on(request, request { request.respond({ status: 404, contentType: text/plain, body: Not Found!, }); });注意这里由于所有请求都会被respond覆盖没有请求会被漏放行因此页面不会悬挂。若只想接管部分请求就必须对其余请求调用request.continue()见示例 2否则它们会永久 stall。示例 2Mock JSON API同时放行其他请求最常用的伪后端模式命中目标 URL 时返回定制 JSON其余请求放行。import puppeteer from puppeteer; const browser await puppeteer.launch(); const page await browser.newPage(); await page.setRequestInterception(true); page.on(request, request { if (request.url().includes(/api/user)) { void request.respond({ status: 200, contentType: application/json, body: JSON.stringify({id: 1, name: Ada Lovelace}), }); return; // 关键处理完务必 return避免继续落到 continue 分支 } void request.continue(); }); await page.goto(https://example.com); // 页面中的 /api/user 请求将拿到上述伪数据 await browser.close();示例 3返回图片等二进制资源body接受Uint8Array因此可以注入本地图片、字体、音视频等二进制资源。仓库测试 test/src/requestinterception.test.ts 中即有先读取test/assets/pptr.png、再以contentType: image/png回放的用法等价代码如下import {readFileSync} from node:fs; import puppeteer from puppeteer; const imageBuffer readFileSync(new URL(../assets/pptr.png, import.meta.url)); await page.setRequestInterception(true); page.on(request, request { if (request.url().endsWith(.png)) { void request.respond({ contentType: image/png, body: imageBuffer, // Uint8Array/Buffer 均可 }); } else { void request.continue(); } });示例 4多个监听器 合作式优先级当同一请求有多个拦截回调时可通过priority让高优先级的一方胜出。仓库测试 test/src/requestinterception-experimental.test.ts 验证了该机制其思路可简化为await page.setRequestInterception(true); // 监听器 A以优先级 1 表态要 respond page.on(request, request { if (request.url().endsWith(.css)) { void request.respond({status: 200, body: a}, 1); } }); // 监听器 B以默认优先级 0 表态要 respond 另一份响应 page.on(request, request { if (request.url().endsWith(.css)) { void request.respond({status: 500, body: b}, 0); } });此时最终下发的是优先级更高1的监听器 A 的响应。若两个监听器都不传priority二者都会立刻决议后执行者会撞上Request is already handled!断言合作式优先级正是为这种多监听器编排场景而设计的。底层原理从 respond() 到 CDP Fetch.fulfillRequest要真正掌握respond需要顺着源码走一遍调用链。第一步通用 API 层做校验与决议登记公共方法定义在 packages/puppeteer-core/src/api/HTTPRequest.ts#L494-L523async respond( response: PartialResponseForRequest, priority?: number, ): Promisevoid { this.verifyInterception(); if (!this.canBeIntercepted()) { return; // 例如 dataURL 请求直接静默返回noop } if (priority undefined) { return await this._respond(response); // 立刻决议 } this.interception.response response; // 暂存伪造响应 // 只有当尚未有决议、或本优先级更高时才写入 Respond 决议 if ( this.interception.resolutionState.priority undefined || priority this.interception.resolutionState.priority ) { this.interception.resolutionState { action: InterceptResolutionAction.Respond, priority, }; return; } // 同级优先级若已有 abort 决议则不覆盖否则置为 Respond if (priority this.interception.resolutionState.priority) { if (this.interception.resolutionState.action abort) { return; } this.interception.resolutionState.action InterceptResolutionAction.Respond; } }结合continueHTTPRequest.ts#L426-L460与abortHTTPRequest.ts#L539-L563的实现可以推断出同级优先级下的抢占次序大致为abort respond continue同级respond遇到已有abort时主动让位同级continue遇到已有abort/respond时同样不覆盖而abort使用priority 的判定同级即可覆盖其他动作。所有挂起的处置动作会进入拦截器队列由 HTTPRequest.ts#L259-L263 的finalizeInterceptions()依序执行完毕后统一结算决议。canBeIntercepted()返回false时respond直接返回——这正是文档最后 NOTE 的源码级体现对data:dataURL请求调用respond是 noop无效操作不支持 mock dataURL 请求。同理abort/continue对这类请求也不会真正下发。第二步body 编码与长度计算抽象方法_respond由具体传输层实现。在调用它之前公共层用静态方法 HTTPRequest.ts#L568-L581 的getResponse()统一处理响应体编码static getResponse(body: string | Uint8Array): { contentLength: number; base64: string; } { const byteBody: Uint8Array isString(body) ? new TextEncoder().encode(body) // 字符串按 UTF-8 编码 : body; // Uint8Array 直接用 return { contentLength: byteBody.byteLength, // 字节长度 base64: typedArrayToBase64(byteBody), // 转 base64 }; }字符串 body 会以 UTF-8 编码后计算真实字节长度byteLength而非字符数这一点对含中文、emoji 等非 ASCII 内容的响应很重要。第三步CDP 层组包并发送 Fetch.fulfillRequest以 ChromeCDP 协议为例_respond的完整实现在 packages/puppeteer-core/src/cdp/HTTPRequest.ts#L236-L286。核心逻辑如下置interception.handled true标记该请求已被处置有body时调用getResponse得到字节长度与 base64 内容遍历response.headerskey 一律toLowerCase()数组值映射为string[]保持多值头非数组值String()若给了contentType则写入content-type头可覆盖 headers 中同名字段之外再附加自动补齐content-length当 body 非空且响应头中未显式提供content-length时用第 2 步算出的字节长度补上源码parsedBody?.contentLength !(content-length in responseHeaders)因此 body 为空时不会写入状态码兜底为200response.status || 200通过 CDP 会话发送Fetch.fulfillRequest指令把requestId、responseCode、由STATUS_TEXTS查表得到的responsePhrase如 404 → Not Found、responseHeaders以及 base64 的body一并下发给浏览器若发送失败catch 分支会重置interception.handled false以便重试并记录错误日志。值得一提的实现事实在 packages/puppeteer-core/src 范围内搜索_respond(response: PartialResponseForRequest)的具体实现目前只出现在 packages/puppeteer-core/src/cdp/HTTPRequest.ts 一条传输通路上即伪造响应的下发最终是由 Chrome DevTools Protocol 的Fetch.fulfillRequest完成的抽象层的_respond声明位于 packages/puppeteer-core/src/api/HTTPRequest.ts#L248。这也解释了为何respond能够绕过真实网络栈直接注入响应——浏览器拿到的是 CDP 层完全虚构的完整 HTTP 消息。常见陷阱与边界情况结合文档 Remarks/NOTE 与源码使用respond时需要特别注意以下边界dataURL 请求不可 mock对data:URL 调用respond是 noop不会有任何效果、也不会报错canBeIntercepted()提前返回。被拦截的请求必须全部给出处置开启拦截后若只想拦截一部分务必对剩余请求调用request.continue()注意要在respond分支后return避免同一个请求被二次处置抛出Request is already handled!。一个请求只能处置一次respond/continue/abort三者互斥重复处置会立刻抛异常。body 为空时不自动补content-length且空 body 也不会有 base64 内容下发。多值响应头请用数组如同时下发多个 cookie应写作headers: {set-cookie: [a1; Path/, b2; Path/]}非数组值一律String()化且 CDP 层会把 key 转为小写因此不应依赖响应头大小写。content-type单独成参contentType是独立参数会以content-type头形式下发若同时在headers里写了content-type二者会被合并为同名字段实践中应避免重复指定。默认 200 是兜底行为不想写status时接口不会报错但请记得缺省值在实现层是 200若需要非 200 语义如 404 示例务必显式传入。测试佐证仓库中的拦截用例若想观察respond在生产级场景下的完整用法可直接阅读仓库测试test/src/requestinterception.test.ts包含以respond返回 201、422、302 状态码、以及返回image/png二进制流的完整用例覆盖伪造响应对页面 JS 感知、图片加载等行为的影响test/src/requestinterception-experimental.test.ts验证了带priority的合作式拦截respond 与 continue 以 1/0 优先级竞争时的胜出方是理解priority语义最直接的运行示例。关联阅读伪造响应的数据契约puppeteer.responseforrequest.md开启拦截的前置 APIpuppeteer.page.setrequestinterception.md拦截三兄弟的另外两位puppeteer.httprequest.abort.md 与 puppeteer.httprequest.continue.md优先级相关类型puppeteer.interceptresolutionaction.md、puppeteer.interceptresolutionstate.md、puppeteer.default_intercept_resolution_priority.md核心实现源码packages/puppeteer-core/src/api/HTTPRequest.ts公共 API 与决议逻辑、packages/puppeteer-core/src/cdp/HTTPRequest.tsFetch.fulfillRequest下发综上respond表面上只是给请求回一个假响应的一行调用但支撑它的是校验、合作式优先级决议、base64/长度编码、响应头归一化与 CDPFetch.fulfillRequest下发这样一条完整链路。理解了这条链路你就能在接口 mock、资源替换、异常注入等场景中准确预测每一次拦截的最终结果。【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考