http_upstream_http2_keepalive_test.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. package repository
  2. import (
  3. "context"
  4. "crypto/x509"
  5. "net/http"
  6. "net/http/httptest"
  7. "net/url"
  8. "testing"
  9. "time"
  10. "github.com/stretchr/testify/require"
  11. )
  12. func http2KeepAliveTestPoolSettings() poolSettings {
  13. return poolSettings{
  14. maxIdleConns: 10,
  15. maxIdleConnsPerHost: 5,
  16. maxConnsPerHost: 10,
  17. idleConnTimeout: 90 * time.Second,
  18. responseHeaderTimeout: time.Minute,
  19. }
  20. }
  21. // requireHTTP2Configured 断言 http2 已显式挂到 http.Transport 上。
  22. // x/net/http2 在 go1.27 && !http2legacy 下是标准库 HTTP/2 的包装:ConfigureTransports 通过
  23. // Transport.RegisterProtocol("http/2") 注册配置并打开 Protocols.HTTP2(TLSNextProto 不承载 h2 入口),
  24. // ReadIdleTimeout/PingTimeout 在建连时映射为 http.HTTP2Config.SendPingTimeout/PingTimeout。
  25. func requireHTTP2Configured(t *testing.T, tr *http.Transport, msg string) {
  26. t.Helper()
  27. require.NotNil(t, tr.Protocols, msg)
  28. require.True(t, tr.Protocols.HTTP2(), msg)
  29. }
  30. // 长流 / OpenAI 上游改走 HTTP/2 后,池化连接被代理/NAT 静默掐断会成为“死连接”:
  31. // 两端都以为连接存活,请求落上去会挂到 TCP 重传超时(分钟级)才失败。Go 的
  32. // http2.Transport 默认 ReadIdleTimeout=0(不发健康 PING),无法检测这种死连接。
  33. // 必须显式启用主动 PING 探测,让死连接被提前剔除,而不是只靠 ResponseHeaderTimeout
  34. // 事后兜底。
  35. func TestEnableHTTP2KeepAlive_EnablesPingHealthCheck(t *testing.T) {
  36. tr := &http.Transport{}
  37. h2, err := enableHTTP2KeepAlive(tr)
  38. require.NoError(t, err)
  39. require.NotNil(t, h2, "必须返回已配置的 *http2.Transport")
  40. require.Positive(t, h2.ReadIdleTimeout, "必须启用空闲 PING 探测以剔除死连接")
  41. require.Equal(t, longStreamHTTP2ReadIdleTimeout, h2.ReadIdleTimeout)
  42. require.Equal(t, longStreamHTTP2PingTimeout, h2.PingTimeout, "PING 无响应必须有超时判定")
  43. requireHTTP2Configured(t, tr, "http2 必须已挂到底层 http.Transport 上")
  44. }
  45. // long_stream_h2 模式构建的 Transport 必须带上 H2 PING 健康探测,从源头剔除死连接。
  46. func TestBuildUpstreamTransport_LongStreamH2_EnablesPingHealthCheck(t *testing.T) {
  47. tr, err := buildUpstreamTransport(http2KeepAliveTestPoolSettings(), nil, upstreamProtocolModeLongStreamH2)
  48. require.NoError(t, err)
  49. require.True(t, tr.ForceAttemptHTTP2, "long_stream_h2 必须启用 HTTP/2")
  50. requireHTTP2Configured(t, tr, "long_stream_h2 必须显式配置 http2 以启用 ReadIdleTimeout")
  51. }
  52. // 非 H2 模式(default/h1)不应因本次改动被误配置:default 走 Go 自动 H2(惰性配置,
  53. // 构建时 Protocols/TLSNextProto 仍为空),h1 模式显式禁用 H2。避免波及 Claude/Gemini 热路径。
  54. func TestBuildUpstreamTransport_NonLongStreamH2_NotEagerlyConfigured(t *testing.T) {
  55. tr, err := buildUpstreamTransport(http2KeepAliveTestPoolSettings(), nil, upstreamProtocolModeDefault)
  56. require.NoError(t, err)
  57. require.Nil(t, tr.Protocols, "default 模式不应在构建期主动配置 http2 keepalive")
  58. require.Nil(t, tr.TLSNextProto["h2"], "default 模式不应在构建期主动配置 http2 keepalive")
  59. }
  60. // long_stream_h2 模式构建的 Transport 必须真正以 HTTP/2 与上游通信,PING 健康探测才有载体:
  61. // 自定义 DialContext 下 Go 不会自动启用 H2,全靠 enableHTTP2KeepAlive 的显式配置。
  62. func TestBuildUpstreamTransport_LongStreamH2_NegotiatesHTTP2(t *testing.T) {
  63. srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
  64. w.WriteHeader(http.StatusOK)
  65. }))
  66. srv.EnableHTTP2 = true
  67. srv.StartTLS()
  68. defer srv.Close()
  69. tr, err := buildUpstreamTransport(http2KeepAliveTestPoolSettings(), nil, upstreamProtocolModeLongStreamH2)
  70. require.NoError(t, err)
  71. defer tr.CloseIdleConnections()
  72. require.NotNil(t, tr.TLSClientConfig)
  73. roots := x509.NewCertPool()
  74. roots.AddCert(srv.Certificate())
  75. tr.TLSClientConfig.RootCAs = roots
  76. req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL, nil)
  77. require.NoError(t, err)
  78. resp, err := tr.RoundTrip(req)
  79. require.NoError(t, err)
  80. require.NoError(t, resp.Body.Close())
  81. require.Equal(t, http.StatusOK, resp.StatusCode)
  82. require.Equal(t, 2, resp.ProtoMajor, "long_stream_h2 必须协商到 HTTP/2")
  83. }
  84. // 死连接在经 HTTP 代理(CONNECT 隧道)时最高发,这是带 proxy 账号的真实生产路径:
  85. // 显式 http2 配置须与 Transport.Proxy 同时正确生效,不能相互干扰。
  86. func TestBuildUpstreamTransport_LongStreamH2_WithHTTPProxy_EnablesKeepAlive(t *testing.T) {
  87. proxyURL, err := url.Parse("http://127.0.0.1:8080")
  88. require.NoError(t, err)
  89. tr, err := buildUpstreamTransport(http2KeepAliveTestPoolSettings(), proxyURL, upstreamProtocolModeLongStreamH2)
  90. require.NoError(t, err)
  91. require.True(t, tr.ForceAttemptHTTP2)
  92. requireHTTP2Configured(t, tr, "经代理的 long_stream_h2 也必须启用 http2 keepalive")
  93. require.NotNil(t, tr.Proxy, "HTTP 代理仍须通过 Transport.Proxy 生效")
  94. }