首页
/ Playwright Route 对象全解:网络请求拦截、改写与 Mock 的源码级实战指南

Playwright Route 对象全解:网络请求拦截、改写与 Mock 的源码级实战指南

2026-09-06 12:49:38作者:农烁颖Land

本篇基于 Playwright 官方 API 文档 class-route.md 展开,系统讲解 Route 对象的五个核心方法(abortcontinuefallbackfetchfulfill)及全部配置参数,并结合 客户端 Route 实现服务端 Route 实现拦截测试用例 印证其底层执行链路。读完后你将能够熟练地对任意网络请求做阻断、头改写、方法改写、响应 Mock 与“请求转发后修改响应”等高级 Mock 操作,并理解多路由链式处理(fallback 链)的执行顺序原理。

1. Route 是什么:拦截的入口与处理对象

每当通过 Page.routeBrowserContext.route 建立网络路由后,匹配到的每个请求都会生成一个 Route 对象,由你在路由回调中对其进行处理。Route 自 v1.8 引入,是 Playwright 网络 Mock 能力(详见 networking 文档)的核心处理对象。

Route 提供两类能力:

  1. 获取被拦截的请求route.request() 返回 Request 对象,可读取 URL、method、headers、postData、resourceType、frame 等信息;
  2. 决定请求的命运:阻断(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 关键行为细节(文档原文强调)

  1. headers 会随重定向生效headers 选项对原始请求及其发起的所有重定向都生效;而 urlmethodpostData 只作用于原始请求,不会带到重定向后的请求。
  2. continue 是终结操作:调用后立即发往网络,后续匹配到的路由处理器不会再被调用。若希望链上后续处理器继续参与,应使用 Route.fallback
  3. 禁止性请求头不可覆盖CookieHostContent-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 fallbackshould 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> 改写请求头

文档特别提示:fetchheaders 选项同时作用于所发起的请求及其重定向。如果只想作用于原始请求而不作用于重定向,应改用 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 揭示了几个实用行为:

  • jsonbody 互斥,同时指定会断言失败(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):

  1. 跨域请求自动补 CORS 头_maybeAddCorsHeaders 会在请求带 Origin 且与资源源不同、且响应头未显式设置 access-control-allow-origin 时,自动追加 access-control-allow-originaccess-control-allow-credentials: truevary: Origin 三个头。这解释了为什么 fulfill 一个跨域 XHR mock 后浏览器不会报 CORS 错误。
  2. 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. 延伸阅读(仓库内路径)

适用前提:以上行为基于当前仓库代码与文档,Route 自 v1.8 可用,fallback 需 v1.23+,fetch 需 v1.29+,maxRedirects 需 v1.31+,timeout 需 v1.33+,maxRetries 需 v1.46+。跨语言使用时注意方法名差异(Java 的 resume/fallback、Python 的 continue_)。

登录后查看全文
热门项目推荐
相关项目推荐