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

4 回复

尝试在请求之间关闭响应体。

更多关于Golang中net.http包的意外行为解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


是的,报告这个问题的人是我。net/http 包也不应该修改用户提供的 tls.Config。尤其是在没有合适的深拷贝方法、文档中也没有明确警告的情况下。况且,tls 包中已经确立了一个契约,即它不会修改用户提供的配置。

为记录在案,此问题已作为issue上报。关键在于,虽然“tls 包不会修改它”这一陈述是正确的,但它并未说明其他包的行为。结果发现net/http确实会修改 tls.Config

问题出在共享的 tls.Config 对象上。当 ForceAttemptHTTP2: true 时,HTTP/2 连接会修改 tls.ConfigNextProtos 字段,这违反了配置不可变的约定。

以下是重现问题的简化示例:

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 握手会失败,因为协议协商不一致。

回到顶部