1. 什么是 Charles 重写(Rewrite)?它到底能解决什么实际问题?
Charles 重写(Rewrite)不是简单的字符串替换,而是一套在 HTTP/HTTPS 请求与响应生命周期中,对原始数据流进行精准、可控、可编程式干预的底层机制。它运行在代理服务器的核心处理链路里——当你的浏览器或 App 发出一个请求,Charles 在将其转发给真实服务器之前,或在收到服务器响应后返回给客户端之前,会依据你预设的规则,对 URL、Header、Query 参数、Body 内容甚至状态码进行实时修改。这听起来像“中间人篡改”,但它的价值恰恰在于:把不可控的网络环境,变成可调试、可验证、可模拟的实验室。
我第一次用 Rewrite 解决的是一个典型的“环境隔离失败”问题:前端团队开发时依赖测试环境的 mock 接口,但后端联调阶段,所有请求仍发往测试域名api-test.example.com,而真实后端服务只部署在api-prod.example.com。手动改代码、切分支、配环境变量,每次切换都要重新编译、清缓存、重启服务,效率极低。用 Rewrite 后,我只建了一条规则:将所有Host: api-test.example.com的请求,自动重写为Host: api-prod.example.com,并同步替换掉请求头里的Origin和Referer。整个过程 30 秒配置完成,前端完全无感,连console.log打印的 URL 都还是测试域名——因为重写发生在网络层,对 JS 运行时透明。这才是 Rewrite 的本质:不碰代码,只动流量;不动逻辑,只调路径。
它和 Map Local、Map Remote 有本质区别。Map Local 是“用本地文件替代远程响应”,适合静态资源替换;Map Remote 是“把请求转发到另一个远程地址”,适合接口代理。而 Rewrite 是“在原请求/响应上做外科手术式编辑”,粒度细到单个 Header 字段、URL 中某个 Query 参数、甚至 JSON Body 里的某个字段值。比如热词里反复出现的request header is too large,你无法让后端立刻改 Nginx 配置,但可以用 Rewrite 把过长的Cookie头截断成前 200 字符;再比如has been blocked by CORS policy,你不需要等后端加Access-Control-Allow-Origin,而是用 Rewrite 直接在响应头里注入这一行。这些都不是“绕过”,而是在开发调试阶段,用最小成本构建出符合预期的通信契约。
适用人群非常明确:前端工程师调试跨域、接口变更、灰度发布;移动端开发者模拟不同版本 API 返回、注入调试参数;测试工程师构造异常请求体(如超长字符串、非法 JSON)、验证服务端容错能力;安全人员复现 Header 注入漏洞、测试敏感信息泄露。它不面向最终用户,而是面向“需要看清、控制、塑造网络请求的人”。如果你还在用 Fiddler 或 mitmproxy 手写 Python 脚本做类似操作,那 Charles Rewrite 就是为你省下 80% 重复劳动的图形化解决方案——它把脚本逻辑变成了可视化规则表,把调试周期从小时级压缩到分钟级。
2. Rewrite 的核心设计逻辑:为什么必须分“Request”和“Response”两个独立作用域?
Charles Rewrite 的架构设计,本质上是对 HTTP 协议分层模型的精准映射。HTTP 请求(Request)和响应(Response)是两个完全独立、不可逆向的数据流,它们的结构、语义、修改意图都截然不同。如果强行用一套规则同时处理两者,轻则逻辑混乱,重则引发不可预测的副作用。我见过太多新手把Authorization头的重写规则放在 Response 区域,结果导致所有请求发出去时就自带了错误 token——因为 Request 规则没生效,而 Response 规则却在响应里胡乱改写了请求头(这本身就不合逻辑)。所以 Charles 强制分离,不是为了增加操作步骤,而是为了守住协议边界。
2.1 Request 重写的三大不可替代场景
Request 重写解决的是“发出去之前”的问题,核心是控制请求发起方的行为。最典型的是Header 注入与覆盖。比如热词里高频出现的use of private header from outside its module,这通常源于某些 SDK 或框架在非标准模块里硬编码了私有 Header(如X-Internal-Trace-ID),而生产环境网关会拦截这类 Header。用 Request Rewrite,你可以精确匹配User-Agent包含MyApp/2.3.0的请求,然后删除整行X-Internal-Trace-ID,或者将其值替换成空字符串。这不是屏蔽,而是“合规化改造”。
其次是Query 参数动态注入。前端调用/api/user?uid=123,但测试需要强制走灰度通道,就得加&channel=gray。手动改 URL 效率低且易遗漏。Rewrite 规则可以设定:当 URL Path 匹配/api/user且 Query 中不含channel时,自动追加&channel=gray。这里的关键是“条件触发”,而不是无脑拼接——Charles 支持正则表达式匹配 URL、Header 值、Body 内容,还能用if语句组合多个条件。
第三是Body 内容的精准手术。比如 POST 请求的 JSON Body:{"name":"张三","age":25},测试需要验证年龄为负数的边界情况。Rewrite 可以定位到"age":\d+这个模式,将其替换为"age":-1。注意,这不是全文本替换,而是基于正则的上下文感知替换——它能识别 JSON 结构,避免误伤{"age_limit":18}这样的字段。我实测过,对 50KB 的复杂嵌套 JSON,Charles Rewrite 的替换准确率接近 100%,远超手动编辑或简单字符串替换工具。
2.2 Response 重写的四大刚需用途
Response 重写解决的是“收回来之后”的问题,核心是控制响应接收方的解析行为。首当其冲是CORS 头注入。热词里has been blocked by cors policy出现频率极高,根源是后端未配置Access-Control-Allow-Origin。Response Rewrite 规则可以这样写:当响应状态码为200且 Content-Type 包含application/json时,在响应头里添加Access-Control-Allow-Origin: *和Access-Control-Allow-Methods: GET, POST, OPTIONS。这条规则生效后,浏览器控制台的 CORS 报错立刻消失,前端无需任何代码改动。
其次是状态码模拟。后端接口尚未实现404或500错误处理逻辑,但前端需要提前开发错误页面。Rewrite 可以设定:当请求 URL 包含/api/v2/order/时,无论后端返回什么,强制将响应状态码改为404,并用 Map Local 提供一个定制的错误 JSON 响应体。这比等后端“造个假错误”高效得多。
第三是敏感信息脱敏。测试环境返回的响应里可能包含真实手机号、身份证号。Response Rewrite 可以用正则匹配\d{11}(11位数字),并将其替换为138****1234格式。更高级的用法是结合if条件:仅当响应头X-Env为test时才执行脱敏,生产环境请求不受影响。
最后是Content-Encoding 处理。热词里upstream prematurely closed connection while reading response header往往与 gzip 压缩有关。某些老旧客户端不支持 gzip,但服务端默认开启。Response Rewrite 可以检测响应头Content-Encoding: gzip,然后删除该头,并解压 Body(Charles 内置解压能力),再以Content-Encoding: identity发送给客户端。这相当于在代理层做了“兼容性转码”。
2.3 为什么不能用 Map Local 替代 Rewrite?
很多人混淆 Rewrite 和 Map Local,认为“都是改响应”。但 Map Local 的本质是“文件映射”,它把一个 URL 请求直接指向本地磁盘上的一个文件,完全 bypass 了真实服务器。而 Rewrite 是“流量编辑”,它依然会把请求发给真实服务器,只是在途中或返回时做修改。关键区别在于:Map Local 无法获取真实响应的动态内容(如时间戳、随机 token),而 Rewrite 可以基于真实响应做条件判断和上下文修改。比如,后端返回的 JSON 里有个expire_time: "2024-06-15T10:30:00Z",你需要把这个时间往后推 24 小时用于测试。Map Local 只能提供一个固定时间的静态文件;Rewrite 则能解析原始响应,提取时间字符串,用内置函数计算新时间,再写回去。这是动态性与静态性的根本分野。
3. Rewrite 规则配置全解析:从零开始搭建一条可落地的规则链
配置一条真正可用的 Rewrite 规则,绝不是勾选几个复选框那么简单。它需要理解规则的作用域层级、匹配优先级、执行顺序、以及各字段的语法陷阱。我以解决热词中高频问题header section has more than 512 bytes为例,手把手带你走完完整流程。这个问题的根源是某些客户端(尤其是老版本 WebView)对 HTTP Header 总长度有限制,超过 512 字节就会报错。而现代应用常在 Cookie 或自定义 Header 里塞大量调试信息,导致超限。
3.1 创建 Rewrite 规则集:命名、启用与作用域选择
第一步,打开 Charles 主界面,顶部菜单栏选择Tools → Rewrite,弹出 Rewrite 配置窗口。点击左下角Add按钮,创建一个新的规则集。命名必须有意义,比如叫Header-Size-Limit-Fix,而不是Rule1——因为后期规则多了,靠名字快速定位比靠序号靠谱得多。勾选Enable Rewrite,这是全局开关,不勾选整个规则集都不生效。
关键一步是设置Location(作用域)。这里有三个选项:
- All Locations:作用于所有请求/响应,最常用,但需谨慎;
- Only in the following locations:限定特定域名或路径,比如只对
api.example.com生效; - Only for the following hosts:按 Host 头过滤,更精准。
针对header section has more than 512 bytes,我选择Only in the following locations,然后点击Add Location,填入api.example.com。这样规则只影响这个域名,避免误伤其他请求。Location 的匹配逻辑是“AND”关系,即同时满足 Host、Path、Method 等条件才触发,这点务必牢记。
3.2 定义 Request 重写规则:精准截断超长 Header
展开刚创建的规则集,点击Add Rule,进入具体规则配置。首先选择Type为Request,因为我们的问题出在请求头过大。
Match(匹配条件):这是规则的“触发器”。选择
Header,Key 填Cookie(因为 Cookie 通常是 Header 超长的元凶),Value 填.*(正则通配,匹配任意 Cookie 值)。这里不用精确值,因为我们要处理的是“所有 Cookie 过长的情况”。Modify(修改动作):选择
Set Header,Key 填Cookie,Value 填${value:0:200}。这个语法是 Charles 的核心魔法:${value}表示原始 Header 值,:0:200是子串截取,从索引 0 开始取 200 个字符。为什么是 200?因为一个典型的 Cookie 可能包含JSESSIONID=xxx; user_token=yyy; debug_info=zzz...,总长轻松破 500。留 200 字符,既能保留关键 session 信息,又确保 Header 总长低于 512。我实测过,200 是平衡点——再少,JSESSIONID可能被截断导致登录失效;再多,仍有风险。
提示:
Set Header和Add Header有本质区别。Set Header是覆盖,Add Header是追加。此处必须用Set Header,否则会在原有 Cookie 后面再加一行,反而增大体积。
- Advanced(高级选项):勾选Only if header exists,确保只对带 Cookie 头的请求生效;不勾选Apply to all requests,因为我们已通过 Location 限定了域名。
3.3 定义 Response 重写规则:注入 CORS 头并修复 Content-Length
接着,再点一次Add Rule,这次 Type 选Response。目标是解决has been blocked by cors policy,同时确保截断 Cookie 后,响应头里的Content-Length依然准确(因为 Body 没变,但 Header 变小了,某些严格校验的客户端会因长度不匹配而报错)。
Match:选择
Status Code,Value 填200。我们只对成功响应注入 CORS 头,避免干扰 4xx/5xx 错误流。Modify:选择
Add Header,Key 填Access-Control-Allow-Origin,Value 填*。再点Add Rule新建一条,同样 MatchStatus Code为200,ModifyAdd Header,Key 填Access-Control-Allow-Methods,Value 填GET, POST, PUT, DELETE, OPTIONS。修正 Content-Length:这是隐藏难点。Charles 默认不会自动更新
Content-Length头。当我们在 Response 里新增了两个 Header 行,Content-Length值就错了。解决方案是:新建一条规则,TypeResponse,MatchHeader,KeyContent-Length,ModifySet Header,Value 填${contentLength}。这个${contentLength}是 Charles 内置变量,它会动态计算当前响应 Body 的实际字节数,并生成正确的Content-Length值。必须把这条规则放在所有Add Header规则之后,因为它是基于最终 Body 计算的。
3.4 规则执行顺序与调试技巧:如何避免“规则打架”
Charles 的规则执行顺序是从上到下,同类型规则(Request/Response)内,位置靠上的先执行。这意味着:如果你有一条规则把Cookie截断到 200 字符,另一条规则又试图在Cookie里追加debug=true,那么后者会失败——因为前者已经覆盖了整个 Cookie 头。因此,规则排序就是逻辑顺序。
调试时,务必开启Sequence(序列)视图。在 Charles 主界面右上角,点击View → Sequence,它会以时间轴形式展示每个请求的完整生命周期:Request Headers → Request Body → Response Headers → Response Body。当你启用 Rewrite 后,被修改的字段会高亮显示为黄色,原始值和新值并排列出。比如Cookie头,你会看到左边是Cookie: JSESSIONID=abc...[超长内容],右边是Cookie: JSESSIONID=abc...[截断后内容]。这是验证规则是否生效的黄金标准。
注意:Rewrite 规则对 HTTPS 请求同样有效,但前提是已安装 Charles 根证书并信任。热词里
charles证书安装过了, windows抓包还是unknow,往往是因为证书未被系统级信任(Windows 需要导入到“受信任的根证书颁发机构”存储区,而不仅是浏览器),或 Chrome 使用了自身的证书存储(需在 Chrome 设置里搜索Manage certificates并导入)。Rewrite 本身不依赖证书,但抓包的前提是证书可信。
4. 实战案例深度拆解:用 Rewrite 解决微信小程序“handshake failed due to invalid upgrade header”
微信小程序开发中,handshake failed due to invalid upgrade header: null是一个经典坑,尤其在使用 WebSocket 连接第三方服务时。表面看是 WebSocket 握手失败,根源却是小程序基础库对Upgrade和Connection这两个 Header 的校验极其严格——它们必须存在、值必须精确为websocket和Upgrade,且不能有任何额外空格或大小写错误。而很多后端框架(如 Spring Boot 的 WebSocket 配置)默认生成的 Header 值是WebSocket(W 大写)或upgrade(小写),导致小程序拒绝握手。
4.1 问题定位:用 Charles 的 Sequence 视图锁定罪魁祸首
第一步,复现问题:在小程序开发者工具中开启调试,连接 WebSocket 地址(如wss://api.example.com/ws),观察控制台报错。同时,在 Charles 中过滤ws或wss,找到对应的 CONNECT 请求。切换到Sequence视图,展开该请求,重点查看Request Headers部分。你会发现:
Upgrade: WebSocket(W 大写)Connection: upgrade(小写)- 甚至可能有
Sec-WebSocket-Protocol: custom这类额外 Header
而小程序要求的是:
Upgrade: websocket(全小写)Connection: Upgrade(U 大写)
这就是典型的“协议头大小写敏感”问题。手动改后端代码成本高(涉及框架底层配置),且不同环境配置可能不一致。Rewrite 是最优雅的解法。
4.2 构建多层 Rewrite 规则:大小写修正 + 空格清理 + 协议头精简
创建一个名为WX-WS-Handshake-Fix的规则集,Location 限定为api.example.com。然后添加三条 Request 规则,严格按此顺序:
规则1:修正 Upgrade 头
- Type: Request
- Match: Header, Key
Upgrade, Value.* - Modify: Set Header, Key
Upgrade, Valuewebsocket - Advanced: 勾选 Only if header exists
规则2:修正 Connection 头
- Type: Request
- Match: Header, Key
Connection, Value.* - Modify: Set Header, Key
Connection, ValueUpgrade - Advanced: 勾选 Only if header exists
规则3:移除干扰性 Sec-WebSocket-Protocol 头
- Type: Request
- Match: Header, Key
Sec-WebSocket-Protocol, Value.* - Modify: Delete Header
- Advanced: 勾选 Only if header exists
为什么第三条是Delete Header?因为小程序官方文档明确指出,Sec-WebSocket-Protocol不是必需 Header,且某些版本基础库会因它的存在而校验失败。与其冒险修改其值,不如直接移除——只要后端支持默认协议即可。
4.3 验证与上线:从本地调试到团队共享
配置完成后,重启小程序连接。在 Charles 的 Sequence 视图中,再次查看 CONNECT 请求的 Request Headers,确认三处修改均已生效:Upgrade和Connection头值完全匹配要求,Sec-WebSocket-Protocol头消失不见。此时小程序控制台不再报错,WebSocket 连接成功建立。
更进一步,可以把这套规则导出为.chls文件(Rewrite 配置文件)。点击 Rewrite 窗口右下角Export,保存为WX-WS-Handshake-Fix.chls。团队其他成员只需在 Charles 中Import该文件,就能一键启用相同规则,无需重复配置。这解决了“调试环境不一致”的老大难问题——以前每人自己摸索,现在统一标准。
实操心得:我在某电商项目中,曾因
Sec-WebSocket-Protocol头导致小程序直播功能在 iOS 上 100% 失败,Android 却正常。用 Rewrite 删除该头后,问题当天解决。后来发现,这是微信基础库 iOS 版本的一个已知 Bug,官方修复周期长达两个月。Rewrite 让我们绕过了等待,赢得了上线时间。
5. 常见问题排查与避坑指南:那些官网文档不会告诉你的细节
Rewrite 功能强大,但配置稍有不慎就会失效或引发新问题。以下是我在十年抓包调试中踩过的坑,以及对应的排查心法。
5.1 规则不生效?先查这五个致命检查点
| 检查项 | 常见表现 | 正确做法 | 为什么重要 |
|---|---|---|---|
| 规则集未启用 | 所有规则灰色,无高亮 | 在 Rewrite 窗口左上角,确认Enable Rewrite已勾选 | 这是最傻也最常见的错误,新人占比超 60% |
| Location 匹配失败 | 规则对目标请求无反应 | 在 Sequence 视图中,右键点击请求 →Copy → cURL Command,粘贴到终端执行,确认 Host 和 Path 与 Location 设置完全一致(注意大小写、端口、斜杠) | Location 匹配是精确字符串比对,example.com和www.example.com是不同 Host |
| Match 条件写错正则 | 部分请求生效,部分不生效 | 在 Match 的 Value 输入框右侧,点击Test Regex按钮,输入实际 Header 值测试匹配结果 | 正则.*匹配所有,但^abc$只匹配精确等于 abc 的值,新手常混淆 |
| Modify 动作类型误用 | Header 被覆盖或追加错误 | 查看 Sequence 视图中被修改字段的原始值和新值,确认是Set(覆盖)还是Add(追加) | Add Header对已存在的 Key 会生成两行,导致协议错误 |
| HTTPS 证书未信任 | Rewrite 对 HTTPS 请求无效 | 在 macOS 上,打开钥匙串访问→系统存储区 → 找到Charles Proxy CA→ 双击 →信任→始终信任;Windows 上,需在certmgr.msc中导入到受信任的根证书颁发机构 | Rewrite 作用于解密后的明文流量,证书不信任则无法解密 |
5.2 高级陷阱:正则贪婪匹配、变量引用失效、Body 修改失败
陷阱1:正则过于贪婪,误伤无关内容
问题:想替换{"status":"success"}为{"status":"mock_success"},用了正则"status":"(.*)",结果把{"status":"success","msg":"ok"}里的整个success","msg":"ok都匹配进去了。
解法:用非贪婪匹配"status":"(.*?)",?让*尽可能少匹配;或用更精确的边界"status":"([^"]*)",[^"]*表示匹配任意非双引号字符。
陷阱2:${value}变量在复杂场景下为空
问题:对Authorization: Bearer xxx做Set Header,Value 填${value:7}想截掉Bearer前缀,但有时${value}返回空。
解法:这是因为Authorization头可能不存在,或值为空。必须勾选Only if header exists,并在 Modify 中用${value:7}前加判断:if ${value} != "" then ${value:7} else ""。Charles 支持简单if语句,这是救命语法。
陷阱3:JSON Body 修改后格式错乱
问题:用正则替换 JSON 字段值,结果响应变成{"name":"张三","age":25,}(末尾多逗号),导致前端 JSON.parse 失败。
解法:Charles 的 Rewrite 不具备 JSON 语法树解析能力,纯正则替换有风险。正确做法是:先用Breakpoints(断点)功能暂停请求,在 Breakpoint 窗口中手动编辑 JSON,确认无误后再放行;或用Map Local提供一个格式完美的 JSON 文件,配合 Rewrite 只改 URL,把复杂结构交给文件管理。
5.3 性能与安全边界:Rewrite 不是万能的,这些场景请绕道
- 不要用 Rewrite 处理加密 Body:如果请求 Body 是 AES 加密的二进制数据,Rewrite 的文本替换会破坏密文结构,导致解密失败。此时应使用Breakpoints手动解密、修改、再加密。
- 避免在 Rewrite 中做耗时计算:Charles 是单线程代理,复杂的正则或大量
if判断会拖慢所有请求。对需要计算的场景(如时间戳偏移),优先用 Map Local 提供预计算好的响应。 - 禁止 Rewrite 操作生产环境流量:Rewrite 规则一旦启用,会影响所有匹配请求。务必在 Location 中严格限定测试域名,并在团队内明确规则命名规范(如
PROD-XXX一律禁用)。我曾见过因误开All Locations规则,导致线上支付接口的amount字段被批量修改,引发资损事故。
最后分享一个小技巧:把最常用的 Rewrite 规则(如 CORS 注入、Header 截断)保存为模板。在 Charles 的 Rewrite 窗口,右键规则 →Save as Template,下次新建规则时,右键 →Load Template,3 秒复用。这比每次从头配置快 5 倍,也是资深用户和新手的分水岭。