Go 限制 HTTP/1 后仍协商 h2:检查克隆配置里的 ALPN
从 DefaultTransport 克隆后只启用 HTTP/1,TLS 配置却可能仍带 h2。服务端选中 HTTP/2 后,HTTP/1 解析器会把帧当响应而失败。
从 http.DefaultTransport 克隆一份传输器,再通过 http.Protocols 只启用 HTTP/1,HTTPS 请求仍可能失败:服务端报 bogus greeting,客户端报 malformed HTTP response,后面跟着 HTTP/2 的二进制帧。Go 1.27.3 中,这个特定组合需要同时检查继承的 TLSClientConfig.NextProtos。
TLS 的 ALPN 协商与 HTTP 请求解析必须一致。克隆默认传输器时,初始化过的配置可能带着 h2 和 http/1.1;只改变 Protocols 并不一定把这个遗留列表清掉。服务端偏好 h2 并选中它后,客户端却沿 HTTP/1 路径发送和解析,双方就说了不同协议。
保留 TLS 条件,只收窄协议列表
先确认应用确实只需要 HTTP/1,再检查克隆后配置中的 NextProtos。下面是创建独立传输器的示例,使用 net/http:
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.Protocols = new(http.Protocols)
tr.Protocols.SetHTTP1(true)
if tr.TLSClientConfig != nil {
tr.TLSClientConfig = tr.TLSClientConfig.Clone()
tr.TLSClientConfig.NextProtos = []string{"http/1.1"}
}
client := &http.Client{Transport: tr}
应在开始并发请求之前完成设置。若应用本来有自定义 CA、客户端证书、服务器名称或 TLS 版本限制,应在它自己的克隆配置上收窄 ALPN,保留这些条件。不要直接把整份 TLSClientConfig 设为 nil,导致私有 CA 或双向认证设置丢失;也不需要使用 InsecureSkipVerify。
如果原本就允许 HTTP/2,另一条处理路径是保留彼此一致的 HTTP/1 与 HTTP/2 支持,而不是强制关闭其中一个。应依据应用的代理、服务端和连接要求选择,不能为了让报错消失而随意改变协议契约。
检查协商结果与真实响应
验收使用原来会选中 h2 的 HTTPS 服务,确认握手协商和 resp.Proto 与设计一致,并验证正常响应内容。只换成不支持 HTTP/2 的测试服务,可能隐藏原来的错位。
还要检查所有传输器的创建入口和连接池生命周期。已经并发使用的共享传输器不应边请求边改 TLS 配置;按应用既有流程切换到新的实例并处理旧空闲连接。若错误来自连错端口、代理返回非 HTTP 数据或其他响应格式,继续按实际字节与协议排查。本文限定的是克隆配置残留 ALPN 的分支,不是所有 malformed HTTP response 的共同原因。


