微信 H5 授权偶发Load failed?罪魁祸首竟是 Alt-Svc 头
摘要:微信公众号 H5 授权登录在 iOS 旧版本上偶发
TypeError: Load failed,时好时坏、复现率极低。排查发现根因是 nginx 声明了带 HTTP/3 草案标识符的Alt-Svc头,但服务器 UDP 443 并未放行。旧版 WebKit 识别草案标识符后主动尝试 QUIC 握手,握手失败导致请求挂死。本文完整记录 bug 由来、排查思路与修复方案。关键词:HTTP/3 · QUIC · Alt-Svc · WKWebView · 微信 H5 · 协议协商
目录
- 一、背景与现象
- 二、排查过程
- 三、Bug 由来:根因分析
- 四、修复方案
- 五、经验总结
一、背景与现象
项目是一个微信公众号 H5 商城,授权登录链路为:
auth.oiwrus.cn 授权页 │ 微信 OAuth 授权 ▼ 微信客户端回调 → 后端 exchange code 换 openid/unionid │ ▼ 跳回 H5 业务页(a2.h5.oiwrus.cn)某天运营反馈:iOS 用户频繁授权失败,前端捕获到如下错误日志:
{"errName":"TypeError","errMsg":"Load failed","attempts":3,"loginUrl":"https://admin.oiwrus.cn/api/smplive/user/officialAccountLogin","from":"http://a2.h5.oiwrus.cn","ua":"Mozilla/5.0 (iPhone; CPU iPhone OS 26_3_1 ...) MicroMessenger/8.0.75"}诡异点在于:错误"时好时坏",同一台手机这次能过、下次就失败,复现率完全看脸。
涉及拓扑
| 域名 | 角色 | 协议 |
|---|---|---|
auth.oiwrus.cn | 授权页(静态页) | HTTPS |
a2.h5.oiwrus.cn | H5 业务页 | HTTP |
admin.oiwrus.cn | 后端 API | HTTPS(nginx + PHP-FPM) |
二、排查过程
第 1 步:先看服务端日志
检查 nginx error log、PHP 业务日志,发现根本没有请求到达后端——报错发生在网络层,是浏览器侧fetch直接挂了,而不是后端返回 4xx/5xx。
结论:问题不在业务代码,在网络层。
第 2 步:埋点收集客户端证据
由于问题在客户端网络层,我们在授权页加了错误埋点,记录完整的 UA 和失败上下文(时间、重试次数、referrer、在线状态)。
// get_auth.html 中的错误上报window.__wechatAuthErrorLog={time:newDate().toISOString(),errName:e.name,errMsg:e.message,attempts:retryCount,loginUrl:LOGIN_URL,from:location.origin,href:location.href,ua:navigator.userAgent,onLine:navigator.onLine};这一步是整个排查的关键——没有 UA 数据,后面根本无从下手。
第 3 步:对比不同 iOS 版本,发现"反向玄学"
统计埋点后发现一个反直觉的现象:
| iOS 版本 | 微信内授权 |
|---|---|
| 26.3.1(旧) | ❌ 经常失败 |
| 26.5.2(新) | ✅ 正常 |
新旧系统表现相反,且报错统一是TypeError: Load failed——这是典型的网络协议层握手失败特征,而不是 HTTP 业务错误。
第 4 步:怀疑 HTTP/3(QUIC)
Load failed+ 时好时坏 + 版本差异,这三个特征叠加,高度指向HTTP/3 协议协商。回想近期做过的优化:为了给网站提速,我们在 nginx 上声明了 HTTP/3 的Alt-Svc头,但只加了声明、并没有真正打通 UDP 443。
检查 nginx 配置,找到了问题源头:
add_header Alt-Svc 'quic=":443"; h3=":443"; h3-29=":443"; h3-27=":443"; h3-T050=":443"; h3-Q050=":443"; h3-Q049=":443"; h3-Q048=":443"; h3-Q046=":443"; h3-Q043=":443"';第 5 步:A/B 验证,一击命中
移除 Alt-Svc 头,reload nginx。观察两天:iOS 26.3.1 授权恢复稳定。实锤。
三、Bug 由来:根因分析
1. Alt-Svc 机制
Alt-Svc响应头(Alternative Services,RFC 7838)是服务器对客户端的建议:“你可以用这些协议/端口来访问我”。客户端有选择权——支持就尝试,不支持就忽略。HTTP/3 就是靠这个头被发现的:CDN/服务器开启 QUIC 后,通过 Alt-Svc 告知浏览器"我这支持 h3"。
2. 新旧 WebKit 对 Alt-Svc 的解析差异(核心)
Alt-Svc 头里列的标识符决定了客户端会去尝试什么协议。我们配置里除了标准h3,还列了一大堆HTTP/3 草案版本标识符(h3-29、h3-27、h3-Q050、h3-T050、quic等,对应 QUIC 草案时代的协议版本)。
| iOS 26.5.2(新版 WebKit) | iOS 26.3.1(旧版 WebKit) | |
|---|---|---|
| HTTP/3 实现 | 只认最终版h3(RFC 9114) | 兼容 QUIC 草案时代 |
对h3-29、h3-Q050等标识符 | 不认识 →整体忽略 Alt-Svc | 能识别 →主动尝试 QUIC 连接 |
| 最终行为 | 继续走 HTTP/2,正常 ✅ | 尝试连接 UDP 443 → 握手失败 ❌ |
依据 RFC 7838:客户端如果不支持 Alt-Svc 中任何协议标识符,就应忽略该提示,沿用现有连接。新版 WebKit 只认h3,看到一票不认识的草案标识符直接放弃;旧版 WebKit 的 HTTP/3 栈还停留在草案兼容期,能对上h3-29/h3-Q050等,于是真的去连 UDP 443。
3. 握手失败 → Load failed
服务器虽然声明了 HTTP/3,但UDP 443 并未放行(QUIC 走 UDP,不是 TCP):
- 云安全组/防火墙只放行了 TCP 443,UDP 443 未开;
- QUIC 握手包发不出去,客户端只能等超时;
- 旧版 WebKit 的 HTTP/3 → HTTP/2 回退机制不成熟,请求被长时间阻塞,最终直接
Load failed。
4. 为什么"时好时坏"?
Alt-Svc 带了ma=86400(缓存 24 小时),WebKit 会把"该站支持 HTTP/3"这个结论缓存一天。首次访问拿到 Alt-Svc 后,后续请求直接用缓存的 HTTP/3 地址发起连接 → 这就是"同一个人这次失败、下次成功、再下次又失败"的玄学根源。
首次请求 → 响应带 Alt-Svc + ma=86400 → 客户端缓存"支持 h3" ↓ 后续请求直接尝试 QUIC(443) ↓ UDP 443 不通 → 超时 → Load failed(时好时坏)四、修复方案
方案 A:保守回退(线上采用)
# 移除所有 Alt-Svc 声明,回退纯 HTTP/2 # 不再声明 QUIC 监听、http3 on server { listen 443 ssl backlog=65535 reuseport; listen [::]:443 ssl backlog=65535 reuseport; http2 on; # 注意:不声明 Alt-Svc(避免 WebKit 尝试 HTTP/3/QUIC, # 服务器未启用 UDP 443 时会握手失败) }对微信 H5 业务,纯 HTTP/2 是最稳的选择——微信内置浏览器对 QUIC 兼容性本就不稳定。
方案 B:正确启用 HTTP/3(适合通用 Web)
如果要真正启用 HTTP/3,必须三步都做对,缺一不可:
# 1. 放行 UDP 443(云安全组 + 本机防火墙,QUIC 走 UDP) # 2. nginx >= 1.25.0 且编译 --with-http_v3_module server { listen 443 ssl; listen 443 quic reuseport; # QUIC 监听不能带 backlog 参数 listen [::]:443 ssl; listen [::]:443 quic reuseport; http2 on; http3 on; add_header Alt-Svc 'h3=":443"; ma=86400'; }两个易踩的坑:
- 标识符只用标准
h3,别列草案版本的h3-29、h3-Q050,否则兼容期内的旧内核真的会去尝试; - QUIC 监听不能带
backlog参数,否则 nginx 直接报"listen" directive "backlog" parameter is incompatible with "quic"启动失败(排查时也踩了这个坑)。
方案 C:按 UA 区分(微信走 HTTP/2,其他走 HTTP/3)
map $http_user_agent $enable_alt_svc { default 1; # 其他环境:声明 Alt-Svc,享受 HTTP/3 ~*MicroMessenger 0; # 微信内置浏览器:不声明,走 HTTP/2 } server { ... if ($enable_alt_svc) { add_header Alt-Svc 'h3=":443"; ma=86400'; } }注:
add_header在if块内作用域受限(仅当前 location),完整方案建议用map + set变量配合add_header ... always在 server 级输出。
五、经验总结
- Alt-Svc 只是"建议"——它让客户端去"试",而不是"确认"。服务器一旦声明了它无法真正支持的协议,就是给自己挖坑。
- 协议标识符要克制:只列标准版
h3,draft 标识符会让兼容期内的旧内核真的去尝试。 - UDP 和 TCP 是两套放行体系:开了 TCP 443 不等于 UDP 443 通了,这是 HTTP/3 排障最容易忽略的点。
ma缓存会放大问题的迷惑性:Alt-Svc 被缓存后,问题表现为随机偶发、复现率很低,务必在客户端侧加埋点、留 UA 证据。- 微信 H5 场景慎开 HTTP/3:微信网络栈 + 新旧 WebKit 行为差异叠加,极易翻车。业务上追求稳妥,纯 HTTP/2 足够。
排查方法论速记
| 现象特征 | 指向 |
|---|---|
Load failed/TypeError | 网络层握手失败,非 HTTP 业务错误 |
| 时好时坏、同机不同结果 | 客户端有缓存(Alt-Svcma、连接池、负缓存) |
| 版本相关、新旧表现相反 | 协议版本协商差异(HTTP/3 draft vs 最终版) |
| 无服务端日志 | 请求根本没到后端,问题在网络层 |
排查日期:2026-08 · 环境:宝塔 nginx + PHP-FPM · 相关产物:enable_nginx_quic.sh(含--remove-only保守方案)