Playwright Route 对象全解:网络请求拦截、改写与 Mock 的源码级实战指南
本篇基于 Playwright 官方 API 文档 class-route.md 展开,系统讲解 Route 对象的五个核心方法(abort、continue、fallback、fetch、fulfill)及全部配置参数,并结合 客户端 Route 实现、服务端 Route 实现 与 拦截测试用例 印证其底层执行链路。读完后你将能够熟练地对任意网络请求做阻断、头改写、方法改写、响应 Mock 与“请求转发后修改响应”等高级 Mock 操作,并理解多路由链式处理(fallback 链)的执行顺序原理。
1. Route 是什么:拦截的入口与处理对象
每当通过 Page.route 或 BrowserContext.route 建立网络路由后,匹配到的每个请求都会生成一个 Route 对象,由你在路由回调中对其进行处理。Route 自 v1.8 引入,是 Playwright 网络 Mock 能力(详见 networking 文档)的核心处理对象。
Route 提供两类能力:
- 获取被拦截的请求:
route.request()返回 Request 对象,可读取 URL、method、headers、postData、resourceType、frame 等信息; - 决定请求的命运:阻断(
abort)、放行并改写(continue)、交给下一个处理器(fallback)、代发请求拿结果(fetch)、本地伪造响应(fulfill)。
从源码结构看,客户端 Route 类 继承自 ChannelOwner,所有终结性操作(abort/fulfill/continue)都会经过统一的 _handleRoute 包装:
// packages/playwright-core/src/client/network.ts (L362-L371)
private async _handleRoute(callback: () => Promise<void>) {
this._checkNotHandled();
try {
await callback();
this._reportHandled(true);
} catch (e) {
this._didThrow = true;
throw e;
}
}
这意味着一个 Route 实例只能被“终结性处理”一次,重复调用 abort/continue/fulfill 会抛出 Route is already handled!(见 _checkNotHandled)。而 fallback 是例外——它只做标记、不终结路由,这正是链式路由的基础。
2. 多路由的执行顺序:后注册者先执行
理解 Route 前必须先理解路由匹配顺序。当多个路由匹配同一请求时,它们按注册的相反顺序执行,即最后注册的路由永远有最高优先级。这一设计的目的是让“后写的路由”总能覆盖“先写的路由”。
源码证据在 browserContext.ts:
async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}) {
this._routes.unshift(new network.RouteHandler(this._baseURL, url, handler, options.times));
}
unshift 将新处理器插到数组头部,因此匹配时从头遍历得到的就是“倒序注册表”。官方测试 should unroute 精确验证了这一点:按顺序注册 4 个路由后,拦截到的执行序列是 [4, 3, 2, 1];再 unroute 移除后变为 [3, 2, 1]。
链式执行的底层实现在服务端 Route.handle 与 continue:服务端为每个 Route 维护一个 _futureHandlers 队列,continue({ isFallback: true }) 会不断 shift 出下一个处理器继续调用;只有当队列耗尽时才真正把请求发往网络(this._delegate.continue(overrides))。
3. Route.abort:阻断请求
route.abort(errorCode?) 自 v1.8 提供,用于中断路由对应的请求。errorCode 为可选字符串,默认 'failed',可选取值共 15 种:
| errorCode | 含义 |
|---|---|
'aborted' |
操作被(用户行为)中断 |
'accessdenied' |
访问非网络类资源被拒绝 |
'addressunreachable' |
IP 地址不可达,通常表示到指定主机/网络没有路由 |
'blockedbyclient' |
客户端主动阻止了该请求 |
'blockedbyresponse' |
响应携带了不满足的条件(如 X-Frame-Options、CSP 祖先检查失败) |
'connectionaborted' |
因收不到 ACK 导致连接超时 |
'connectionclosed' |
连接被关闭(对应 TCP FIN) |
'connectionfailed' |
连接尝试失败 |
'connectionrefused' |
连接尝试被拒绝 |
'connectionreset' |
连接被重置(对应 TCP RST) |
'internetdisconnected' |
网络已断开 |
'namenotresolved' |
主机名无法解析 |
'timedout' |
操作超时 |
'failed' |
通用失败(默认值) |
最典型的使用场景是屏蔽静态资源以加快测试:
// 阻断所有图片请求
await page.route('**/*.{png,jpg,jpeg}', route => route.abort());
// 或按资源类型精细阻断
await page.route('**/*', route => {
return route.request().resourceType() === 'image' ? route.abort() : route.continue();
});
服务端 abort 实现 会先发出 RequestAborted 事件再委托给具体浏览器的 RouteDelegate 执行,这正是 requestaborted 事件的来源。
4. Route.continue:放行请求并附带动词/头部/URL 改写
route.continue(options?) 将请求(可带覆盖项)发往网络。它是最常用的“透明改写”手段,例如统一追加认证头:
await page.route('**/*', async (route, request) => {
const headers = {
...request.headers(),
foo: 'foo-value', // 设置 "foo" 头
bar: undefined, // 移除 "bar" 头(置为 undefined)
};
await route.continue({ headers });
});
各语言的方法名存在差异(文档中通过 langs 别名声明):Java 为 route.resume(options),Python 为 route.continue_(options),.NET 为 route.ContinueAsync(options),JS 的 continue 因语言保留字而直接可用。
4.1 四个改写选项
| 选项 | 类型 | 说明 |
|---|---|---|
url |
string | 改写请求 URL,新 URL 必须与原 URL 协议相同 |
method |
string | 改写请求方法(如 GET 变 POST) |
postData |
string / Buffer / Serializable | 改写请求体;JS、Python 支持对象(自动序列化为 JSON),Java 支持 string/Buffer,.NET 仅支持 Buffer |
headers |
Object<string, string> | 改写请求头,值会被转为字符串 |
4.2 关键行为细节(文档原文强调)
- headers 会随重定向生效:
headers选项对原始请求及其发起的所有重定向都生效;而url、method、postData只作用于原始请求,不会带到重定向后的请求。 - continue 是终结操作:调用后立即发往网络,后续匹配到的路由处理器不会再被调用。若希望链上后续处理器继续参与,应使用
Route.fallback。 - 禁止性请求头不可覆盖:
Cookie、Host、Content-Length等 forbidden request headers 无法被改写;若提供了这些头的覆盖值,会被忽略并使用原始请求头。要注入自定义 Cookie 应使用BrowserContext.addCookies。
源码印证了第 3 点:服务端 continue 方法 会调用 applyHeadersOverrides 过滤出原请求中的 forbidden 头并优先保留:
// packages/playwright-core/src/server/network.ts
export function applyHeadersOverrides(original: HeadersArray, overrides: HeadersArray): HeadersArray {
const forbiddenHeaders = original.filter(header => isForbiddenHeader(header.name, header.value));
const allowedHeaders = overrides.filter(header => !isForbiddenHeader(header.name, header.value));
return mergeHeaders([allowedHeaders, forbiddenHeaders]);
}
客户端侧还有一个容易忽视的实现细节:continue/fallback 的覆盖项并不是当场生效,而是通过 Request._applyFallbackOverrides 累积到 Request 的 _fallbackOverrides 上。因此在 fallback 链中,后一个处理器看到的 request.headers() / request.url() 已经是前序处理器改写后的值——这就是“中间处理器可以逐层修改请求”的机制。
5. Route.fallback:把请求交给链上的下一个处理器
route.fallback(options?) 自 v1.23 提供,语义与 continue 相同,区别在于:请求先交给下一个匹配的路由处理器处理,直到所有处理器都 fallback 后才真正发往网络。
5.1 经典示例:三段式 fallback 链
官方文档示例完整展示了执行顺序——请求先由最底部注册的处理器处理,然后逐层回退,最终被最先注册的路由 abort:
await page.route('**/*', async route => {
// Runs last.
await route.abort();
});
await page.route('**/*', async route => {
// Runs second.
await route.fallback();
});
await page.route('**/*', async route => {
// Runs first.
await route.fallback();
});
5.2 实战模式:按请求类型拆分处理器
注册多个路由非常适合把不同种类的请求交给独立处理器(如 API 调用 vs 页面资源、GET vs POST):
// 只处理 GET 请求
await page.route('**/*', async route => {
if (route.request().method() !== 'GET') {
await route.fallback(); // 非 GET 请求回退给下一个处理器
return;
}
// Handling GET only.
// ...
});
// 只处理 POST 请求
await page.route('**/*', async route => {
if (route.request().method() !== 'POST') {
await route.fallback();
return;
}
// Handling POST only.
// ...
});
5.3 边回退边改写:中间处理器可以修改请求
fallback 同样接受 url / method / postData / headers 四个选项,中间处理器可以在回退的同时改写请求,后续处理器将看到改写后的值:
await page.route('**/*', async (route, request) => {
const headers = {
...request.headers(),
foo: 'foo-value', // 设置 "foo" 头
bar: undefined, // 移除 "bar" 头
};
await route.fallback({ headers });
});
5.4 fallback 选项与 continue 选项的一个重要区别
| 选项 | fallback 特有说明 |
|---|---|
url |
改写 URL 不影响路由匹配——所有路由始终使用原始请求 URL 进行匹配(continue 没有此条额外说明,因为 continue 之后不再匹配) |
method / postData / headers |
语义与 continue 完全一致 |
相关测试可参考 tests/library/browsercontext-route.spec.ts 中的 should chain fallback 与 should chain fallback w/ dynamic URL,后者验证了动态改写 URL 后链仍按原始 URL 匹配的行为。
6. Route.fetch:代发请求并拿回 APIResponse 以便二次加工
route.fetch(options?) 自 v1.29 提供,返回 APIResponse。它执行真实请求但不直接 fulfill,让你可以在拿到响应后修改其内容(状态码、响应体、头)再用 fulfill 返回给页面——这是“改写真实 API 响应”的标准姿势:
await page.route('https://dog.ceo/api/breeds/list/all', async route => {
const response = await route.fetch();
const json = await response.json();
json.message['big_red_dog'] = []; // 修改真实响应中的字段
await route.fulfill({ response, json });
});
Java 对应 route.fetch() + route.fulfill(new Route.FulfillOptions().setResponse(response)...),Python 为 await route.fetch(),.NET 为 await route.FetchAsync()。
6.1 fetch 选项全表
| 选项 | 版本 | 类型 | 说明 |
|---|---|---|---|
url |
v1.29 | string | 改写请求 URL,协议必须与原请求一致 |
maxRedirects |
v1.31 | int | 自动跟随的重定向上限,默认 20,传 0 不跟随;超限抛错 |
maxRetries |
v1.46 | int | 网络错误重试上限,默认 0(不重试)。当前仅重试 ECONNRESET 错误,不按 HTTP 状态码重试 |
timeout |
v1.33 | float | 请求超时(毫秒),默认 30000,传 0 禁用超时 |
signal |
— | AbortSignal | 取消信号 |
method |
v1.29 | string | 改写请求方法 |
postData |
v1.29 | string/Buffer/Serializable | 设置请求体;JS、Python 支持对象,对象会被序列化为 JSON 字符串,且若未显式设置 content-type 则设为 application/json,否则为 application/octet-stream |
headers |
v1.29 | Object<string, string> | 改写请求头 |
文档特别提示:fetch 的 headers 选项同时作用于所发起的请求及其重定向。如果只想作用于原始请求而不作用于重定向,应改用 Route.continue。
源码印证:客户端 Route.fetch 直接委托给 APIRequestContext._innerFetch,与 page.request 复用同一套底层 fetch 栈;maxRetries 的重试循环实现在 server/fetch.ts 的 _sendRequestWithRetries。
7. Route.fulfill:本地伪造响应
route.fulfill(options?) 用给定响应满足该路由请求,不发真实网络请求。两个最常用示例:
(1)把所有请求都伪造为 404:
await page.route('**/*', async route => {
await route.fulfill({
status: 404,
contentType: 'text/plain',
body: 'Not Found!'
});
});
(2)用静态文件响应:
await page.route('**/xhr_endpoint', route => route.fulfill({ path: 'mock_data.json' }));
(Java 用 new Route.FulfillOptions().setPath(Paths.get("mock_data.json")),Python 用 route.fulfill(path="mock_data.json"),.NET 用 route.FulfillAsync(new() { Path = "mock_data.json" })。)
7.1 fulfill 选项全表
| 选项 | 版本 | 类型 | 说明 |
|---|---|---|---|
status |
v1.8 | int | 响应状态码,默认 200 |
headers |
v1.8 | Object<string, string> | 响应头,值会被转为字符串 |
contentType |
v1.8 | string | 等价于设置 Content-Type 响应头 |
body |
v1.8 | string/Buffer(JS、Python);string(Java、.NET) | 响应体;Java/.NET 的二进制体请用 bodyBytes |
bodyBytes |
v1.9 | Buffer(Java、.NET) | 以原始字节作为响应体 |
json |
v1.29 | Serializable(JS、Python、.NET) | JSON 响应,未显式设置时自动把 content type 设为 application/json |
path |
v1.8 | path | 以文件内容响应,content type 按扩展名推断;相对路径按当前工作目录解析 |
response |
v1.15 | APIResponse | 用已有的 APIResponse(典型来自 route.fetch())满足请求,其单个字段仍可用 fulfill 选项覆盖 |
7.2 客户端 fulfill 的底层细节
客户端 _innerFulfill 揭示了几个实用行为:
json与body互斥,同时指定会断言失败(Can specify either body or json parameters);- 传
path时文件在 Node 侧读取并 base64 编码传输,content-type 由getMimeTypeForPath按扩展名推断,兜底为application/octet-stream; - 传
response时优先复用响应体缓存(fetchResponseUid),跨连接时回退为读取完整 body; - 若提供了 body 且未显式指定
content-length,会自动按 body 长度补上该头。
服务端还有两个“文档未显式提及但源码确认”的行为值得了解(来自 server/network.ts):
- 跨域请求自动补 CORS 头:
_maybeAddCorsHeaders会在请求带Origin且与资源源不同、且响应头未显式设置access-control-allow-origin时,自动追加access-control-allow-origin、access-control-allow-credentials: true与vary: Origin三个头。这解释了为什么 fulfill 一个跨域 XHR mock 后浏览器不会报 CORS 错误。 - fulfill 触发
RequestFulfilled事件,abort 触发RequestAborted,continue 触发RequestContinued——这些事件可用于requestfailed/response之外的监听场景。
8. Route.request:拿回被路由的请求
route.request()(v1.8)返回 Request 对象,即当前被路由处理的请求本身。它是路由回调中所有判断的起点——request.url()、request.method()、request.resourceType()、request.headers()、request.postData()、request.frame()、request.isNavigationRequest() 等。注意若路由是挂在 BrowserContext 上,frame() 对导航请求的可用性受请求发生时机限制,文档与 Request 类说明 中均有对应提示。
9. 常见组合套路速查
结合以上方法,以下是文档与测试中反复出现的组合模式:
| 目标 | 推荐写法 |
|---|---|
| 屏蔽图片等静态资源 | route.abort()(可选 errorCode) |
| 注入统一请求头 / 改写 method | route.continue({ headers }) 等 |
| 多处理器分层处理 | 每个处理器不匹配时 route.fallback(),最后一个终结 |
| 伪造固定响应 | route.fulfill({ status, body / json / path }) |
| 改写真实 API 响应 | const r = await route.fetch(); ...; route.fulfill({ response: r, ... }) |
| 转发到另一后端(同协议) | route.continue({ url: '...' }) 或 route.fallback({ url }) |
10. 延伸阅读(仓库内路径)
- 网络拦截总览与更多场景:docs/src/network.md(Route 概念入口、abort 示例、CORS 头注入说明均在此汇总)
- 路由注册方 API:BrowserContext 类文档(
route/unroute/unrouteAll)、Page 类文档(page.route) - 相关对象文档:Request、APIResponse、APIRequestContext、WebSocketRoute
- 核心源码:客户端 Route / RouteHandler、服务端 Route 与 fallback 链、浏览器端拦截实现示例(Chromium)
- 测试用例:context 级路由与 fallback 链测试、WebSocket 路由测试
适用前提:以上行为基于当前仓库代码与文档,Route 自 v1.8 可用,fallback 需 v1.23+,fetch 需 v1.29+,maxRedirects 需 v1.31+,timeout 需 v1.33+,maxRetries 需 v1.46+。跨语言使用时注意方法名差异(Java 的 resume/fallback、Python 的 continue_)。
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 StartedRust0624
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