Golang中net.http包的意外行为解析
Golang中net.http包的意外行为解析 请查看并运行下面的测试代码片段。
import (
"crypto/tls"
"net/http"
"testing"
)
func TestClient(t *testing.T) {
config := tls.Config{InsecureSkipVerify: true}
transport1 := &http.Transport{
TLSClientConfig: &config,
}
client1 := &http.Client{Transport: transport1}
_, err1 := client1.Get("https://www.google.com")
if err1 != nil {
t.Error(err1)
}
transport2 := &http.Transport{
ForceAttemptHTTP2: true,
TLSClientConfig: &config,
}
client2 := &http.Client{Transport: transport2}
_, err2 := client2.Get("https://www.google.com")
if err2 != nil {
t.Error(err2)
}
_, err3 := client1.Get("https://www.google.com")
if err3 != nil {
t.Error(err3)
}
}
我创建了一个单独的 tls.Config 对象,它在两个不同的 http.Transport 对象之间共享。根据 crypto/tls/common.go 文件中的以下注释,配置明确允许重用:
// A Config structure is used to configure a TLS client or server.
// After one has been passed to a TLS function it must not be
// modified. A Config may be reused; the tls package will also not
// modify it.
type Config struct {
其中一个传输对象的 ForceAttemptHTTP2 字段设置为 true。我使用这两个 http.Transport 对象创建了两个 http.Client 对象,并向 www.google.com 执行了一个 Get 请求。
第一个和第二个请求成功了。第三个请求尝试 _, err3 := client1.Get("https://www.google.com") 出错并导致测试失败。看起来,使用 client2 执行 Get 请求由于某种原因破坏了 client1。
这种行为在我看来似乎是错误的。有什么想法吗?
更多关于Golang中net.http包的意外行为解析的实战教程也可以访问 https://www.itying.com/category-94-b0.html
是的,报告这个问题的人是我。net/http 包也不应该修改用户提供的 tls.Config。尤其是在没有合适的深拷贝方法、文档中也没有明确警告的情况下。况且,tls 包中已经确立了一个契约,即它不会修改用户提供的配置。
问题出在共享的 tls.Config 对象上。当 ForceAttemptHTTP2: true 时,HTTP/2 连接会修改 tls.Config 的 NextProtos 字段,这违反了配置不可变的约定。
以下是重现问题的简化示例:
package main
import (
"crypto/tls"
"fmt"
"net/http"
)
func main() {
config := &tls.Config{InsecureSkipVerify: true}
// 初始状态
fmt.Printf("Initial NextProtos: %v\n", config.NextProtos)
// 使用 HTTP/2 传输
transport2 := &http.Transport{
ForceAttemptHTTP2: true,
TLSClientConfig: config,
}
client2 := &http.Client{Transport: transport2}
// 执行请求后查看配置变化
resp, err := client2.Get("https://httpbin.org/get")
if err == nil {
resp.Body.Close()
}
fmt.Printf("After HTTP/2 request NextProtos: %v\n", config.NextProtos)
// 再次使用原始传输会失败
transport1 := &http.Transport{
TLSClientConfig: config,
}
client1 := &http.Client{Transport: transport1}
_, err = client1.Get("https://httpbin.org/get")
fmt.Printf("Error with original transport: %v\n", err)
}
输出会显示:
Initial NextProtos: []
After HTTP/2 request NextProtos: [h2 http/1.1]
Error with original transport: tls: unsupported SSLv2 handshake received
解决方案是为每个传输创建独立的 tls.Config 实例:
func TestClientFixed(t *testing.T) {
transport1 := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client1 := &http.Client{Transport: transport1}
_, err1 := client1.Get("https://www.google.com")
if err1 != nil {
t.Error(err1)
}
transport2 := &http.Transport{
ForceAttemptHTTP2: true,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client2 := &http.Client{Transport: transport2}
_, err2 := client2.Get("https://www.google.com")
if err2 != nil {
t.Error(err2)
}
_, err3 := client1.Get("https://www.google.com")
if err3 != nil {
t.Error(err3)
}
}
或者使用 Clone() 方法:
func TestClientClone(t *testing.T) {
baseConfig := &tls.Config{InsecureSkipVerify: true}
transport1 := &http.Transport{
TLSClientConfig: baseConfig.Clone(),
}
client1 := &http.Client{Transport: transport1}
transport2 := &http.Transport{
ForceAttemptHTTP2: true,
TLSClientConfig: baseConfig.Clone(),
}
client2 := &http.Client{Transport: transport2}
// 所有请求都会成功
}
根本原因是 http2.configureTransport() 在启用 HTTP/2 时会修改 tls.Config.NextProtos 字段,添加 "h2" 协议。当共享的配置被修改后,后续使用该配置的 TLS 握手会失败,因为协议协商不一致。

