模拟与故障注入
看流量回答的是 App 做了什么。这一页说的是另一半:让网络回答一些它本来不会回答的东西,好看看 App 会怎么处理。一个还没写好的后端、一个返回 500 的端点、一个要八秒才回来的响应、一个干脆消失了的 API。
这里的一切在 Chute 能跑的所有平台上都成立;一切涉及 HTTPS 的,都要先对那个主机打开解密——一条 Chute 读不懂的加密请求,它也没法替它作答。
Chute 能模拟什么,不能模拟什么
Chute 介入的是连接和 HTTP 报文,它没有流量整形器,所以:
| 能 | 返回固定正文、返回指定状态码、在请求或响应前加固定延迟、直接拒绝连接、把 HTTP/3 客户端逼回 TCP、把请求发给它没要的另一台主机 |
| 不能 | 限制带宽、丢包或乱序、加抖动、让一条连接中途劣化,或者在传输层模拟某个指定的 RTT |
配置里没有 [Throttle] 段,任何地方也没有限速。如果你要的是慢的链路而不是慢的响应,那属于网络条件模拟器(Apple 的 Network Link Conditioner,或者路由器),不属于 Chute。
挑一种机制
| 要模拟 | 用 | 在哪 |
|---|---|---|
| 一段还不存在的响应正文 | [Map Local] |
Mock 响应 |
| 正好是 503 | [URL Rewrite] … reject |
URL 重写 |
| 空的 200、空白图片、空 JSON 对象 | reject-200、reject-img、reject-dict |
URL 重写 |
| 其他任意状态码——401、429、500 | 一个 http-request 脚本 |
JS 脚本 |
| 延迟 | 一个 http-request 或 http-response 脚本 |
JS 脚本 |
| 一个压根连不上的端点 | 一条 REJECT 规则 |
内置策略 |
| 一个不肯回落到 TCP 的客户端 | block-quic |
杂项选项 |
| 同一个 URL 背后换一个后端 | [Host],或 [URL Rewrite] 的 header 模式 |
本地 DNS 映射 |
固定的响应正文
[Map Local] 用一个文件或者内联的 base64 来应答匹配的请求,不去问真实服务器:
[Map Local]
^https://api\.example\.com/v1/profile.* data="/Users/me/mocks/profile.json"
^https://api\.example\.com/v1/flags.* base64="eyJiZXRhIjogdHJ1ZX0="
它能不能生效由三件事决定:
- 正则必须匹配整个 URL,不是其中一段。除非你要匹配的是一个完全没有查询串的 URL,否则模式要以
.*结尾。 data=由运行 Chute 的那台设备读取。 在 Mac 上这很方便——改文件,下一个请求就看到新内容。在手机或 Apple TV 上,一个来自你 Mac 的路径毫无意义;那里请改用base64=,或者把文件用 HTTP 提供出来再用 URL 重写。- 状态码永远是
200 OK。[Map Local]没有办法设置状态码,而且应答之后连接就关掉了。要别的状态码,用脚本——见下一节。
正文支持 {{ "{{url}}" }}、{{ "{{host}}" }}、{{ "{{path}}" }}、{{ "{{method}}" }} 和 {{ "{{ua}}" }} 这几个模板变量,足够做一个把请求内容回显出来的 Mock。
一个错误状态码
要 503 不需要脚本——一条 reject 模式的 URL 重写就返回 HTTP/1.1 503:
[URL Rewrite]
^https://api\.example\.com/v1/orders.* _ reject
同族的另外几种覆盖了「没有有用内容」的其他形态:reject-200(200 加空正文)、reject-img(一张 1×1 的 GIF)、reject-dict(JSON 的 {},200)。它们对 HTTPS 都只在该主机正在被解密时才生效。
要其他任意状态码,用一个 http-request 脚本把请求短路掉:
[Script]
Fail429 = type=http-request, script-path=/Users/me/mocks/fail429.js, pattern=^https://api\.example\.com/v1/orders
// fail429.js —— 不联系服务器就作答
$done({
response: {
status: 429,
headers: {
"Content-Type": "application/json",
"Retry-After": "30"
},
body: JSON.stringify({ error: "rate_limited" })
}
})
脚本的 pattern 是在 URL 里任意位置匹配的,和各改写族不同——像 ^https://api\.example\.com/v1/orders 这样的前缀就够了,不用补 .*。
在 HTTP/1.1 这条路径上,状态行里的原因短语无论状态码是多少都写作
OK(HTTP/1.1 429 OK)。客户端读的是数字不是短语,所以这只是观感问题——但你在原始抓包里看到的就是它。
延迟
脚本会一直挡着报文直到它调用 $done(),所以一个定时器就是一段延迟:
[Script]
SlowAPI = type=http-response, script-path=/Users/me/mocks/slow.js, pattern=^https://api\.example\.com/v1/, timeout=15
// slow.js —— 晚 8 秒再把真实响应交回去
setTimeout(function () {
$done({})
}, 8)
预算是脚本自己的 timeout:默认 5 秒,大于 30 的会被夹到 30。超时时还没调用 $done() 的脚本会被当作放行——报文原样继续——所以延迟设得比 timeout 长不会响亮地失败,它只是不再延迟了。把 timeout 设得比你想要的延迟大,就像上面的例子。
用 type=http-request 可以在联系服务器之前就延迟(App 看到的是一次慢的往返),用 type=http-response 则在之后延迟(服务器很快,App 照样等)。
一个干脆没了的端点
Mock 替换的是响应;REJECT 规则拒的是连接。它作用在连接层面,所以覆盖的是任意协议而不只是 HTTP,而且不需要解密:
[Rule]
DOMAIN-SUFFIX,api.example.com,REJECT
REJECT-DROP、REJECT-TINYGIF 和 REJECT-NO-DROP 出于兼容也被接受,行为都等同于普通 REJECT。对 HTTP 请求来说,show-error-page-for-reject = true 会把干巴巴的拒绝换成一个能读的错误页,在浏览器里一眼就知道是你拦的。
这也是检查「到底有没有回落路径」的办法——把主用主机拒掉,看 App 是去够备用的那个,还是就在那儿转圈。
把客户端从 HTTP/3 上赶下来
QUIC 跑在 UDP 上,Chute 解不了它的密,所以一个走 HTTP/3 的 App 对本页所有机制都是隐形的。把它的 QUIC 流拒掉,兼容的客户端就会改用 TCP 重试,那里这一切都有效:
[General]
block-quic = on
auto 只在流要走代理时拒 QUIC;on 到处都拒,包括 DIRECT。对于经 TUN 进来的流量,Chute 会用 ICMP 端口不可达来应答被拒的 QUIC 流,让客户端立刻回落,而不用干等一次超时。
把请求送到别处
两种办法,在两个层面:
[Host]
api.example.com = 10.0.0.5
[Host] 映射用你指定的地址来应答 DNS 查询——可以是一台预发布机器,也可以是一个哪儿都不通的地址,如果你要的是「连接超时」而不是「连接被拒」。它对所有协议都生效,也不需要解密。改完记得清一下 DNS 缓存。
[URL Rewrite]
^https://api\.example\.com/v1/(.*) https://staging.example.com/v1/$1 header
header 模式就地改写请求并把 Host 头一并修好,客户端根本不知道自己被转走了。这一种在 HTTP 层,对 HTTPS 需要解密。目标没法就地改写时,Chute 会回落成用一个 307 指向新 URL。
确认它真的生效了
一条从来没匹配上的规则,看起来和一条匹配了但什么也没做的规则一模一样——这正是整页内容的失败方式。
- 改写与 Mock:Web 控制台的规则页列出本次运行中生效过的每一条 URL 重写、Header 重写、Body 重写和 Map Local 规则,带次数。不在列表里就意味着它从来没匹配过。同样的数据在
GET /api/rules的rewrite_hits里。 - 逐条连接:在控制台或 Dashboard 里打开这条连接,读它的已应用改写行,那里会用规则自己的话把规则报出来。
- 脚本不出现在那张表里。 脚本的证据是它自己的输出:
console.log的内容会进日志,在控制台的日志页或者GET /api/logs里读。
收拾干净
从控制台、从 Dashboard 或者通过 POST /api/rewrites/:family 加的规则活在运行中的内核里,下次重启就没了——这让它们非常适合做实验,也非常不适合放你要长期依赖的东西。写在配置文件里的规则能扛住重启,这让它很适合放一个 Mock,同时也是一件极其不该忘掉的事:一条留在配置里的 [Map Local] 几周后还在应答请求,而它看起来和一个坏掉的服务器一模一样。
本页为英文版的翻译。如内容有出入,以英文版为准。