你给 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-vergev2rayNShadowrocket 等已知客户端 UA,其他一律挡掉。
  • 黑名单模式:拦截 curlwgetpython-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 但内容为 HTMLUser-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 刷新后时间更新但节点为 0URL 编码错误(+/%/& 问题)手工编码 token,重新添加远程配置5 分钟
Safari 能看到文本,App 更新时间不变User-Agent 被拦截,请求未到达服务端加 &flag=shadowrocket 或用 SubConverter 中转3 分钟
Safari 打开是 Cloudflare 验证页面Cloudflare 按 UA 弹出 ChallengeSubConverter 中转或服务商加白 Shadowrocket UA10 分钟
iPhone 上空白,电脑上同一链接正常剪贴板或输入法改动了半角符号逐字符对比两个平台上的链接3 分钟
链接里有中文或空格URL 不符合 RFC 3986百分号编码整条链接2 分钟
Safari 也是 403 / 空白 / 网页token 失效或链接本身有问题联系服务商确认账号和订阅状态不定
节点列表有数字但全是红色超时这是连接问题,不是导入问题回到网络层排查,不要继续改 URL

如果 iPhone、iPad 和桌面端希望共用一份订阅来源,格式差异(Clash YAML / SIP008 / Shadowrocket URI 列表)本身也会造成某一端空白。准备一份兼容 Clash / Singbox / V2Ray 的订阅可以减少多端之间的格式转换步骤,但如果有某一端单独空白,仍然优先按上表的顺序排查编码和 UA。

怎么确认修复已经生效?

修复后验证这五条,缺一条就说明还有变量没排除:

  1. Safari 能下载到文本:内容以 ss://vmess://trojan:// 开头,或以 eyJ 等 Base64 特征开头。
  2. Shadowrocket 远程配置更新时间刷新:删掉旧配置重新添加后,时间戳变成当前时间。
  3. 节点数量不再为 0:立即出现具体数字(10、20、50 等)。
  4. 节点延迟测试能跑出数值:点测速后有 ms 数值或明确的超时提示,不是卡住不动。
  5. 关闭 App 重新打开后列表还在:不会每次启动都需要重新导入。

只看到”刷新完成”或”更新成功”不算验证通过——这两个提示只表示 HTTP 请求完成了,不表示返回的内容能被解析成节点。

如果按本文顺序走到这里节点还是空白,把三个信息记录下来交给服务商:订阅 URL(脱敏)、curl 的 HTTP 状态码、Shadowrocket 刷新后界面截图。这三个信息比”节点空白”四个字更有用。

相关文章