URI.Update 误解了包含绝对 URL 的相对重定向 Location(例如 /login?redirect=https://host/),导致客户端无法执行重定向。
描述错误 protocol.URI.Update / UpdateBytes 通过在新 URI 字符串中搜索 // 来决定"绝对 URI":
// pkg/protocol/uri.go (v0.10.6)
func (u *URI) updateBytes(newURI, buf []byte) []byte {
...
n := bytes.Index(newURI, bytestr.StrSlashSlash)
if n >= 0 {
// absolute uri一个 相对 重定向 Location,其 查询字符串中包含一个绝对 URL - 经典的 SSO 登录模式 Location: /login/redirect_to_sso?redirect=https://example.com/ - 在查询中包含 //,因此被错误地归类为绝对 URI。然后整个字符串从头开始重新解析,生成一个空主机(以及协议从 https 降级为 http)。
由于 client.DoRequestFollowRedirects 通过 getRedirectURL → URI.UpdateBytes 解析每个跳转,因此 Client.DoRedirects 对于任何服务器的第二个跳转都会失败,该服务器以这种非常常见的重定向形状作出响应。根据拨号器,失败表现为 dial tcp :80: connection refused 或 请求中缺少必需的 Host 头.
最小重现(无需网络)
u := protocol.AcquireURI()
u.Update("https://example.com/")
u.Update("/login/redirect_to_sso?redirect=https://example.com/")
fmt.Printf("%q host=%q scheme=%q\n", u.FullURI(), u.Host(), u.Scheme())
// got: "http:///login/redirect_to_sso?redirect=https://example.com/" host="" scheme="http"
// want: "https://example.com/login/redirect_to_sso?redirect=https://example.com/" host="example.com"控制案例(相同路径,查询中没有 //)正确解析:
u2 := protocol.AcquireURI()
u2.Update("https://example.com/")
u2.Update("/login/plain")
// "https://example.com/login/plain" host="example.com" ✅端到端:使用任何服务器对 Location: /login?redirect=https://example.com/ 返回 302 的 Client.DoRedirects:
err = dial tcp :80: connect: connection refused
req.URI().String() = "http:///login/redirect_to_sso?redirect=https://example.com/"预期行为
根据 RFC 3986 参考解析(以及 net/url.URL.ResolveReference),新的 URI 只有在它 以 一个协议(scheme://...)开始或是协议相对的 (以 //) 才被视为绝对。一个前导 / 表示路径绝对相对引用; // 出现稍后(例如在查询中)不应影响分类。例如:
if bytes.HasPrefix(newURI, bytestr.StrSlashSlash) || schemePrefixed(newURI) {
// absolute
}(现有的 newURI[0] == '/' 分支已经正确处理了路径绝对的情况,一旦去除了误分类)。
环境
内容来源: cloudwego/hertz