复写(Rewrite)
Rewrite 用于按条件修改 HTTP 请求或响应,也可以替换 URL、返回重定向、拒绝请求或生成 Mock 数据。
本文介绍 Loon 3.5.1 (978) 起支持的新语法。
Rewrite 仅对 HTTP 和经过 MitM 解密的 HTTPS 请求生效,并在规则匹配前执行。
可以使用 Rewrite 配置生成器 组合条件和 Action,并直接复制生成结果。
快速开始
每条 Rewrite 使用一行配置,基本格式为:
<phase> if <condition> then <action> [| <action> ...]
为请求设置 Header:
request if ${url} ~= /^https:\/\/api\.example\.com/ then request.header.set("X-Loon", "true")
修改 JSON 响应:
response if ${url} ~= /^https:\/\/api\.example\.com\/profile$/ && ${response.status} == 200 then response.json.replace("data.vip", true)
多个 Action 使用 | 连接,并按照从左到右的顺序执行:
request if ${url} ~= /^https:\/\/api\.example\.com/ then request.header.set("X-Loon", "true") | request.header.del("Cookie")
执行阶段
| 阶段 | 执行时机 | 可用数据 |
|---|---|---|
request | 请求发出前 | URL、请求方法、请求 Header |
response | 收到响应 Header 后 | 请求数据、响应状态码、响应 Header |
请求和响应 Action 通常需要分开配置:
request if ${url} ~= /^https:\/\/example\.com/ then request.header.set("X-Test", "request")
response if ${url} ~= /^https:\/\/example\.com/ then response.header.set("X-Test", "response")
一条普通 Rewrite 不能同时包含请求 Action 和响应 Action。
response.body.mock(...) 是特殊情况:配置阶段仍写作 response,但 Loon 会在请求发往上游前提前生成响应。具体限制参见 Mock 响应 Body。
条件表达式
比较操作符
| 操作符 | 说明 |
|---|---|
== | 精确比较完整值 |
~= | 使用正则查找匹配 |
精确匹配请求方法:
request if ${request.method} == "POST" then request.header.set("X-Method", "POST")
匹配响应 Header:
response if ${response.header['Content-Type']} ~= /^application\/json(?:;|$)/i then response.header.set("X-JSON", "true")
~= 默认查找能够匹配的部分。需要匹配完整值时,请在正则中显式使用 ^ 和 $。
逻辑操作符
| 操作符 | 说明 |
|---|---|
&& | 并且 |
|| | 或者 |
() | 调整或保留条件分组 |
request if ${request.method} == "POST" && (${request.header['X-Region']} == "CN" || ${request.header['X-Region']} == "HK") then request.header.set("X-Matched", "true")
优先级为:
比较操作符 > && > ||
同时使用 && 和 || 时,建议使用括号明确分组。包含两个及以上直接条件的显式分组会保留,只有一个条件的冗余括号会自动省略。
变量
Rewrite 中的通用动态值统一使用 ${...}:
| 来源 | 示例 |
|---|---|
| Loon 内置变量 | ${url} |
| 插件参数 | ${region} |
| 条件正则捕获 | ${item.1} |
内置变量
| 变量 | 类型 | request | response |
|---|---|---|---|
${url} | String | ✓ | ✓ |
${request.method} | String | ✓ | ✓ |
${request.header['name']} | String 或 null | ✓ | ✓ |
${response.status} | Number | — | ✓ |
${response.header['name']} | String 或 null | — | ✓ |
Header 名称查找不区分大小写:
${request.header['content-type']}
${request.header['Content-Type']}
以上表达式引用同一个 Header。request 阶段不能引用尚未生成的响应变量。
Header 值类型
Request Header 和 Response Header 存在时是 String,不存在时是 null。存在但值为空的 Header 是空字符串 "",不等于 null。
# Header 不存在
request if ${request.header['X-Optional']} == null then reject
# Header 存在,但值为空
request if ${request.header['X-Optional']} == "" then reject
Header 使用 == 时,可以根据比较值的来源选择以下类型:
| 值类型 | 写法 | 使用场景 |
|---|---|---|
| String | "CN" | 与固定的 Header 值精确比较 |
| Null | null | 判断 Header 是否不存在 |
| Variable | ${region} | 整个比较值来自 String 类型的变量 |
| Template | "Bearer ${token}" | 将固定文本与一个或多个变量组合后比较 |
| Raw String | `literal ${region}` | 按字面量比较,不处理转义和变量替换 |
使用 ~= 时,右值必须是 Regex,适合匹配 Content-Type、User-Agent 等具有固定格式或包含附加参数的 Header:
response if ${response.header['Content-Type']} ~= /^application\/json(?:;|$)/i then response.header.set("X-JSON", "true")
Number 和 Boolean 不能直接与 Header 比较。插件变量用于 Header 比较时也必须是 String 类型。
Raw Syntax 用于直接填写完整右值语法,例如 ${region}、"CN" 或 `CN`。它是编辑器提供的高级输入方式,不是独立的配置值类 型;生成内容仍需符合上述语法。
当前版本不支持在 if 条件中读取请求或响应 Body,例如 ${request.body}、${response.body} 或 JSON Key Path。
插件参数
插件参数继续在 [Argument] 中声明:
[Argument]
enabled = switch,true,tag=启用
price = input,9.99,type=number,tag=价格
region = select,"CN","US","JP",tag=地区
level = select,1,2,3,type=number,tag=等级
在 Rewrite 中直接引用参数名:
[Rewrite]
response if ${enabled} == true && ${level} == 2 && ${request.header['X-Region']} == ${region} then response.json.replace("data.price", ${price})
| 控件 | 支持类型 | 默认类型 |
|---|---|---|
input | String、Number | String |
select | String、Number | String |
switch | Boolean | Boolean |
input 和 select 需要返回数字时,使用 type=number:
price = input,9.99,type=number
level = select,1,2,3,type=number
未声明 type 的旧插件保持原有行为:input、select 按 String 解析,switch 按 Boolean 解析。参数只作为有类型的数据使用,不会被重新解析为条件或 Action,也不会进行二次变量展开。
本地 Rewrite 没有 [Argument] 参数来源,因此本地编辑页面只能使用内置变量和当前 Rewrite 的正则捕获变量。
条件正则捕获
在正则条件后使用 as <name> 保存匹配结果:
request if ${url} ~= /^https:\/\/api\.shop\.com\/item\/(\d+)/ as item then request.header.set("X-Item-ID", "${item.1}")
| 变量 | 内容 |
|---|---|
${item.0} | 完整匹配内容 |
${item.1} | 第一个捕获组 |
${item.2} | 第二个捕获组 |
使用限制:
as只能用于~=正则条件。- 捕获名称在同一条 Rewrite 中必须唯一。
- 捕获名称不能与插件参数重名。
- 捕获下标不能超过正则中的捕获组数量。
- 被 Action 引用的捕获条件必须经过表达式的所有成功路径,不能位于
||的可选分支中。
有效:
request if (${request.method} == "GET" || ${request.method} == "POST") && ${url} ~= /item\/(\d+)/ as item then request.header.set("X-Item", "${item.1}")
无效:
request if ${url} ~= /item\/(\d+)/ as item || ${request.header['X-Debug']} == "true" then request.header.set("X-Item", "${item.1}")
如果正则整体匹配成功,但被引用的可选捕获组没有值,当前 Action 会运行失败并跳过,后续 Action 继续执行。
条件捕获与 Action 捕获
条件正则和 Action 自带正则使用两套捕获语法:
| 来源 | 声明方式 | 引用方式 | 使用范围 |
|---|---|---|---|
if 条件正则 | ~= /.../ as item | ${item.0}、${item.1} | 当前 Rewrite 的 Action |
| Header/Body Replace 正则 | Action 的 Regex 参数 | $0、$1 | 当前 Action 的替换参数 |
例如:
request if ${url} ~= /^https:\/\/old\.example\.com(\/.*)$/ as item then url.replace("https://new.example.com${item.1}")
request if ${url} ~= /^https:\/\/example\.com/ then request.body.replace(/price=(\d+)/, "amount=$1")
第一条中的 ${item.1} 来自 if 条件;第二条中的 $1 来自 request.body.replace 自带的正则。$n 不是通用变量,不能跨 Action 使用,url.replace("$1") 也是无效配置。
值与字符串
字面量
| 类型 | 示例 |
|---|---|
| String | "hello world" |
| Number | 200、9.99 |
| Boolean | true、false |
| Null | null |
| Regex | /^https:\/\/example\.com/i |
固定字符串必须使用双引号。以下两个值的类型不同:
9.99 # Number
"9.99" # String
正则
正则格式:
/pattern/flags
支持的 Flag:
| Flag | 说明 |
|---|---|
i | 忽略大小写 |
m | 多行模式 |
s | . 匹配换行 |
正则字面量中不会展开 ${...}。需要由插件参数提供完整正则时,将变量直接放在 ~= 右侧:
request if ${url} ~= ${urlPattern} then request.header.set("X-Matched", "true")
双引号字符串
双引号字符串支持 ${...} 变量模板:
request.header.set("X-Info", "price=${price}, region=${region}")
支持以下转义:
| 写法 | 结果 |
|---|---|
\" | 双引号 |
\\ | 反斜杠 |
\n | 换行 |
\r | 回车 |
\t | Tab |
\${ | 字面量 ${ |
Header 名称在变量表达式中使用单 引号:
request.header.set("X-Origin", "UA=${request.header['User-Agent']}")
原始字符串
固定 JSON、HTML 或其他包含大量引号的内容可以使用反引号:
response if ${url} ~= /^https:\/\/api\.example\.com/ then response.body.mock("json", `{"code":0,"message":"ok"}`, 200)
原始字符串具有以下特点:
- 不处理反斜杠转义。
- 不展开
${...}。 - 逗号、等号、括号和双引号均为普通内容。
- 两个连续反引号表示一个字面量反引号。
需要变量时,请使用普通双引号字符串:
response if ${url} ~= /^https:\/\/api\.example\.com\/item\/(\d+)/ as item then response.body.mock("json", "{\"item\":\"${item.1}\"}", 200)
新版语法不会按空格拆分整行,因此不再需要使用 \x20 表示空格。
Action
所有 Action 统一使用位置参数:
action(value, value)
参数必须按照方法声明中的顺序填写,不允许填写参数名称:
# 有效
redirect(302, "https://example.com")
# 无效
redirect(status=302, location="https://example.com")
可选参数只能从最右侧开始省略。以下方法声明中的 [...] 表示尾部可选参数,不是配置中需要填写的字符。
Action 方法速查
url.replace(String)
redirect(Number, String)
reject(Number[, String])
reject_img(Number)
reject_dict(Number)
reject_array(Number)
reject_video(Number)
request.header.add(String, String)
request.header.set(String, String)
request.header.del(String)
request.header.replace(String, Regex, RegexReplacement)
response.header.add(String, String)
response.header.set(String, String)
response.header.del(String)
response.header.replace(String, Regex, RegexReplacement)
request.body.replace(Regex, RegexReplacement)
response.body.replace(Regex, RegexReplacement)
request.json.add(String, Any)
request.json.delete(String)
request.json.replace(String, Any)
request.json.jq(String)
request.json.jq_file(String)
response.json.add(String, Any)
response.json.delete(String)
response.json.replace(String, Any)
response.json.jq(String)
response.json.jq_file(String)
request.body.mock(String, String[, Boolean])
request.body.mock_file(String, String[, Boolean])
response.body.mock(String, String[, Number[, Boolean]])
response.body.mock_file(String, String[, Number[, Boolean]])
RegexReplacement 在配置中仍使用字符串形式,其中的 $0 至 $n 引用同一个 Action 的正则匹配结果。
批量数组参数
Header 修改、Body 正则替换和 JSON 修改支持在一个 Action 中配置多组参数:
request if ${url} ~= /api/ then request.header.set(["X-A", "X-B"], ["1", "2"])
response if ${url} ~= /api/ then response.body.replace([/false/, /disabled/], ["true", "enabled"])
response if ${url} ~= /api/ then response.json.add(["data.a", "data.b"], [1, true])
支持批量参数的 Action:
request.header.add/set/del/replaceresponse.header.add/set/del/replacerequest.body.replace、response.body.replacerequest.json.add/delete/replaceresponse.json.add/delete/replace
单值写法继续有效。使用数组时需遵守以下规则:
- 同一个 Action 的所有参数都必须使用数组,不能混用单值和数组。
- 各参数数组长度必须一致,参数按照相同下标配对并依次执行。
- 数组不能为空,也不能嵌套数组。
- 每个元素仍需符合该位置要求的 String、Regex、RegexReplacement 或 Any 类型。
- 每个 JSON Key Path 都会单独校验。
例如:
request.header.del(["Cookie", "Referer"])
request.header.replace(["X-A", "X-B"], [/old-a/, /old-b/i], ["new-a", "new-b"])
response.json.delete(["data.ads", "data.tracking"])
批量写法在执行效果上等价于按相同顺序填写多个同类 Action,但配置会保留为一条批量指令。
URL 修改
url.replace
url.replace 使用 if 中唯一的必选 URL 正则作为替换范围,Action 中不再重复填写正则:
request if ${url} ~= /^https:\/\/old\.example\.com(\/.*)$/ as urlMatch then url.replace("https://new.example.com${urlMatch.1}")
只替换正则实际命中的范围,未命中的 URL 内容会保留。
使用限制:
if中必须有且只有一个所有成功路径都会经过的${url} ~= /.../条件。- 该 URL 正则不能位于
||的可选分支中。 - 需要捕获内容时,使用
as和${名称.n}。 url.replace的参数中不能使用$n。
redirect
request if ${url} ~= /^http:\/\/example\.com/ then redirect(302, "https://api.example.com")
参数:
| 位置 | 类型 | 说明 |
|---|---|---|
| 1 | Number | 状态码,当前支持 302、307 |
| 2 | String | URL 正则命中范围的替换内容 |
redirect 与 url.replace 使用相同的必选 URL 正则规则,并保留正则未命中的 URL 内容。
例如输入:
http://example.com/item/123?region=CN
上面的配置将生成:
https://api.example.com/item/123?region=CN
Reject
| Action | 响应内容 |
|---|---|
reject(status) | 指定状态码、空 Body |
reject(status, body) | 指定状态码和 UTF-8 文本 |
reject_img(status) | 1×1 GIF |
reject_dict(status) | JSON 对象 {} |
reject_array(status) | JSON 数组 [] |
reject_video(status) | 空白视频 |
状态码必须是 100...599 范围内的整数。
request if ${url} ~= /^https:\/\/example\.com\/ads/ then reject_dict(200)
request if ${url} ~= /^https:\/\/example\.com\/blocked/ then reject(451, "Unavailable for legal reasons")
reject(200, "{}") 返回普通文本。需要 JSON Content-Type 时,应使用 reject_dict(200) 或 reject_array(200)。
Header
请求 Header:
request if ${url} ~= /^https:\/\/example\.com/ then request.header.add("X-Loon", "true")
request if ${url} ~= /^https:\/\/example\.com/ then request.header.set("User-Agent", "Loon")
request if ${url} ~= /^https:\/\/example\.com/ then request.header.del("Cookie")
request if ${url} ~= /^https:\/\/example\.com/ then request.header.replace("User-Agent", /iPhone OS (\d+)/, "iPhone OS $1")
响应 Header:
response if ${url} ~= /^https:\/\/example\.com/ then response.header.add("X-Loon", "true")
response if ${url} ~= /^https:\/\/example\.com/ then response.header.set("Cache-Control", "no-cache")
response if ${url} ~= /^https:\/\/example\.com/ then response.header.del("Set-Cookie")
response if ${url} ~= /^https:\/\/example\.com/ then response.header.replace("Content-Type", /^(.+); charset=.+$/i, "$1")
| Action | 参数顺序 |
|---|---|
*.header.add | Header 名称、Header 值 |
*.header.set | Header 名称、Header 值 |
*.header.del | Header 名称 |
*.header.replace | Header 名称、正则、替换内容 |
header.replace 的替换内容支持当前 Action 正则的 $0、$1 至 $n,也支持 ${...} 通用变量。
Body 正则替换
request if ${url} ~= /^https:\/\/example\.com/ then request.body.replace(/"price":\s*([0-9.]+)/, "\"originalPrice\":$1")
response if ${url} ~= /^https:\/\/example\.com/ then response.body.replace(/"enabled":\s*(false)/, "\"enabled\":$1")
参数顺序:
Regex, RegexReplacement