你给 Shadowrocket 贴了一条订阅链接,刷新后节点数还是 0。Safari 打开同一条链接能看到一堆 ss:// 或 vmess:// 开头的文本,但回到 App 里怎么刷新都是空的。
这种情况最常见的两个原因,都不是链接”过期了”:一是 URL 里的特殊字符在传输过程中被错误编码了,二是服务端看了 Shadowrocket 发送的 User-Agent 请求头之后,返回的不是节点数据而是网页。
先别扫码、别卸载、别换订阅。把编码和请求头修对,比重新折腾一轮省时间。
订阅 URL 里的 + / % / & 是怎么让节点变空白的?
Shadowrocket 拿到的订阅内容是一段文本,但在这个文本到达 App 之前,URL 本身要先经历至少两次”转义”:你复制链接时的一次,客户端发 HTTP 请求时的一次。特殊字符在两轮转义里只要出一次错,App 拿到的就是乱码。
最常见的四种编码故障:
| 字符 | 正常含义 | 编码出错后 | 对订阅的影响 |
|---|---|---|---|
+ | Base64 里的合法字符 | 被当成空格(URL 规范中 + = 空格) | Base64 解码失败,内容变成乱码 |
% | 百分号编码的起始符 | %25 被二次编码成 %2525 | 解码后缺少原始字符,URI 不完整 |
& | URL 参数分隔符 | 被未编码的 & 意外截断 | 订阅 token 只剩前半段,后半段丢失 |
= | URL 键值分隔符 | 订阅内容中包含 = 但被当成参数 | Base64 填充符被错误拆分 |
具体地说:很多订阅链接长这样:
https://sub.example.com/link/abc123?token=xyz&flag=shadowrocket
如果 token 的值本身包含 +、% 或 =,这些字符在粘贴到浏览器或 App 时没有被正确编码,Shadowrocket 发出去的 HTTP 请求就和你想的不一样了。服务端收到的是一段残缺参数,可能返回空响应、404,或者更隐蔽的——返回 200 但 Body 里只有 {"error":"invalid token"} 的 JSON。
双重编码是最隐蔽的一种。链接已经被编码过一次(% 变成了 %25),你把它贴进 Shadowrocket 后,App 又做了一次编码,%25 变成了 %2525。服务端解码时只解一层,拿到的仍然是 %25 而不是原始字符,最终 Base64 解码失败。
User-Agent 不对,服务端回的就不是节点
另一个比 URL 编码更隐蔽的原因:服务端会看 HTTP 请求头里的 User-Agent 字段。
Shadowrocket 发起订阅更新时,发出的 User-Agent 大概是 Shadowrocket/2.2.80 CFNetwork/... 这种格式。如果你用的订阅服务前面套了 Cloudflare 或其他反向代理,服务端可能会按 User-Agent 做访问控制:
- 白名单模式:只放行
clash-verge、v2rayN、Shadowrocket等已知客户端 UA,其他一律挡掉。 - 黑名单模式:拦截
curl、wget、python-requests等脚本工具 UA。 - Challenge 模式:对未识别 UA 弹出 Cloudflare JS Challenge 或 Turnstile 验证页面。
第三种最坑——因为 HTTP 状态码仍然是 200,但返回的 Body 是一段 HTML(Cloudflare 的验证页面),Shadowrocket 把它当成节点配置去解析,当然解析不出来。界面上只看到”刷新完成”四个字,节点数还是 0。
Safari 能打开同一链接是因为 Safari 的 User-Agent 是标准的浏览器标识,Cloudflare 会放行。但这不代表链接本身没问题——只是 App 和浏览器的”身份证”不同。
**怎么确认是 UA 的问题?**用电脑终端跑一遍对照:
# 用 Shadowrocket 常见的 UA 去请求
curl -s -o /dev/null -w "%{http_code}" \
-H "User-Agent: Shadowrocket/2.2.80" \
"你的订阅链接"
# 和浏览器默认 UA 对比
curl -s -o /dev/null -w "%{http_code}" \
-H "User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15" \
"你的订阅链接"
如果第一条命令返回 403、503 或者返回的 Body 以 <!DOCTYPE html> 开头,第二条正常返回节点文本,那就确定是 User-Agent 被拦截。
先确认是编码还是 UA:Safari、curl 和 Shadowrocket 日志三对照
走到这一步,先别急着改。把三个信号对齐,避免同时动两个变量。
第一步:Safari 直接打开订阅链接
关闭 Shadowrocket 当前的代理连接,在 iOS Safari 地址栏粘贴完整 URL。如果 Safari 能打开并显示/下载一段文本(不是网页),记录下这段文本的前 200 个字符。如果 Safari 也打不开,问题更可能是 DNS、链接失效或网络层阻断,不是本文覆盖的范围。
第二步:电脑端 curl 测试 UA
用上一节的 curl 命令,分别以 Shadowrocket UA 和浏览器 UA 请求同一链接。对比 HTTP 状态码和返回内容的前几行。
第三步:Shadowrocket 内检查最后更新时间
App 内 → 设置 → 订阅 → 找到对应的远程配置,看”上次更新”时间。如果时间变了但节点数仍是 0,说明 App 拉到了内容但解析不了——偏 URL 编码和内容格式。如果时间没变,说明请求根本没到达服务端或被拦截——偏 User-Agent 和网络层。
这三个信号对齐后,你就能确定先修编码还是先修 UA。
| 信号组合 | 可能性最高的原因 | 先做什么 |
|---|---|---|
| Safari 正常,curl (Shadowrocket UA) 返回 200 但内容为 HTML | User-Agent 被服务端过滤 | 修复 UA 或加 flag 参数 |
| Safari 正常,curl (Shadowrocket UA) 也正常 | URL 在 iPhone 复制过程中被修改 | 逐字符对比 iPhone 和电脑上的链接 |
| Safari 也打不开 | DNS、链接失效、网络阻断 | 先解决基础可达性 |
Safari 看到的是 JSON {"error":"..."} | token 或参数被截断 | 检查 URL 里的 & 和 = 是否完整 |
| 更新时间变了但节点仍为 0 | 内容格式不兼容(如 Clash YAML 含不支持的 proxy-group 类型) | 换 SubConverter 转成 mixed 或 ss 格式 |
修 URL 编码:手工逐段改,别靠 App 自己猜
确定了是编码问题后,修复链路如下:
1. 拿到原始订阅链接的完整副本
如果链接是从微信、QQ、Telegram 或短信里复制的,先在电脑上用纯文本编辑器粘贴出来。不要用手机直接粘贴——输入法可能会自动把半角符号替换成全角(& → &,: → :,/ → /)。
2. 拆开 URL 结构,逐段检查
一条典型的订阅 URL 长这样:
https://sub.example.com/link/abc123?token=eyJh&flag=shadowrocket
逐段看:
- Scheme:
https://— 不要写成http://或缺少斜杠 - Host:
sub.example.com— 确认没有多余空格或编码 - Path:
/link/abc123— 确认没有中文或特殊符号 - Query:
?token=...&flag=...— 这是编码问题的重灾区
3. 把 token 值单独做一次 URL 编码
如果 token 包含 +、/、= 这些 Base64 常见字符,用在线工具或命令行做一次完整的 URL 编码:
python3 -c "import urllib.parse; print(urllib.parse.quote('原始token值', safe=''))"
把编码后的 token 拼回 URL,确保特殊字符全部变成 %XX 形式。
4. 在 Safari 中测试编码后的完整链接
编码后的链接先在 Safari 里打开。如果 Safari 能看到节点文本,再把这条编码后的链接粘贴到 Shadowrocket 的远程配置里。不要走过期订阅的”编辑”入口改 URL——直接删掉旧配置,新建一个远程配置,贴上编码后的链接。
绕过 User-Agent 拦截:三种可行方式
User-Agent 拦截的核心矛盾是:Shadowrocket 不提供直接的 UA 自定义设置,但服务端又按 UA 过滤请求。下面是三种绕过方式,按改造成本从低到高排列:
方式一:订阅链接追加 flag 参数(成本最低,先试这个)
很多订阅服务在链接尾部支持 flag 参数来标识客户端类型:
原链接:https://sub.example.com/link/abc?token=xyz
修改为:https://sub.example.com/link/abc?token=xyz&flag=shadowrocket
服务端看到 flag=shadowrocket 之后,可能关闭 UA 校验、返回 Shadowrocket 兼容格式,或者直接跳过 Cloudflare 的浏览器验证。但这取决于服务商是否实现了这个逻辑——先问你的服务商支持哪些 flag 值。
如果链接已经有其他参数,用 & 连接;如果是第一个参数,用 ?:
https://sub.example.com/sub?flag=shadowrocket&token=xyz
方式二:SubConverter 中转并指定 UA
用 SubConverter 做中转请求,你在 Shadowrocket 里填的是 SubConverter 的地址,SubConverter 用你指定的 UA 去向源站拉取:
https://subconverter.example.com/sub?target=mixed&url=你的订阅链接&ua=shadowrocket
参数说明:
target=mixed:输出 Shadowrocket 兼容的混合格式(SS/SSR/VMess/Trojan URI 列表)url=:你的原始订阅链接(必须先做 URL 编码)ua=shadowrocket:SubConverter 向源站请求时使用的 User-Agent
这个方式的额外好处是顺便解决了格式兼容问题——如果源站只输出 Clash YAML,SubConverter 会转成 Shadowrocket 能识别的 URI 列表。
方式三:Shadowrocket 重写规则修改 Header
Shadowrocket 支持在本地配置里写重写规则,在 HTTP 请求发出前修改 Header。进入 Shadowrocket → 配置 → 编辑配置 → 重写规则,添加:
[Rewrite]
# 覆盖订阅请求的 User-Agent 为浏览器标识
URL-REGEX: ^https?://sub\.example\.com/.* HEADER User-Agent Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15
注意:这条规则需要把 sub\.example\.com 替换成你的订阅服务器域名。正则匹配的范围尽量精确,避免影响其他请求的 Header。
**每次只试一种方式,改了之后删掉旧远程配置重新添加,看节点数有没有变化。**同时改 flag、SubConverter 和重写规则,出了问题都不知道是哪一步生效的。
常见原因速查与修复优先级
写到这里,把最常见的几种空节点原因按出现概率排出来。对号入座比逐项排查快。
| 现象 | 概率最高的原因 | 修复操作 | 预计耗时 |
|---|---|---|---|
| Safari 能看到文本,App 刷新后时间更新但节点为 0 | URL 编码错误(+/%/& 问题) | 手工编码 token,重新添加远程配置 | 5 分钟 |
| Safari 能看到文本,App 更新时间不变 | User-Agent 被拦截,请求未到达服务端 | 加 &flag=shadowrocket 或用 SubConverter 中转 | 3 分钟 |
| Safari 打开是 Cloudflare 验证页面 | Cloudflare 按 UA 弹出 Challenge | SubConverter 中转或服务商加白 Shadowrocket UA | 10 分钟 |
| iPhone 上空白,电脑上同一链接正常 | 剪贴板或输入法改动了半角符号 | 逐字符对比两个平台上的链接 | 3 分钟 |
| 链接里有中文或空格 | URL 不符合 RFC 3986 | 百分号编码整条链接 | 2 分钟 |
| Safari 也是 403 / 空白 / 网页 | token 失效或链接本身有问题 | 联系服务商确认账号和订阅状态 | 不定 |
| 节点列表有数字但全是红色超时 | 这是连接问题,不是导入问题 | 回到网络层排查,不要继续改 URL | — |
如果 iPhone、iPad 和桌面端希望共用一份订阅来源,格式差异(Clash YAML / SIP008 / Shadowrocket URI 列表)本身也会造成某一端空白。准备一份兼容 Clash / Singbox / V2Ray 的订阅可以减少多端之间的格式转换步骤,但如果有某一端单独空白,仍然优先按上表的顺序排查编码和 UA。
怎么确认修复已经生效?
修复后验证这五条,缺一条就说明还有变量没排除:
- Safari 能下载到文本:内容以
ss://、vmess://、trojan://开头,或以eyJ等 Base64 特征开头。 - Shadowrocket 远程配置更新时间刷新:删掉旧配置重新添加后,时间戳变成当前时间。
- 节点数量不再为 0:立即出现具体数字(10、20、50 等)。
- 节点延迟测试能跑出数值:点测速后有 ms 数值或明确的超时提示,不是卡住不动。
- 关闭 App 重新打开后列表还在:不会每次启动都需要重新导入。
只看到”刷新完成”或”更新成功”不算验证通过——这两个提示只表示 HTTP 请求完成了,不表示返回的内容能被解析成节点。
如果按本文顺序走到这里节点还是空白,把三个信息记录下来交给服务商:订阅 URL(脱敏)、curl 的 HTTP 状态码、Shadowrocket 刷新后界面截图。这三个信息比”节点空白”四个字更有用。
相关文章
- Shadowrocket 订阅更新后节点列表空白 — 先用 Safari 检查 URL 响应 — 从 Safari 响应入手判断 URL 是否返回可解析内容
- Shadowrocket 订阅导入后节点为空 — URL、token 和格式排查 — token 失效、格式不匹配、旧远程配置残留
- Shadowrocket 订阅更新超时 — DNS、HTTP 状态码和格式排查 — iOS 端权限、DNS 和内容格式的超时排查链路