Files
sub2api/backend/internal/repository/http_upstream_dial_timeout_test.go
T
li 66ad405dd6 fix(upstream): set explicit TCP dial timeout on upstream transports
http.Transport 未设置 DialContext 时使用零值 net.Dialer(Timeout=0),
DNS 解析与 TCP 握手没有任何上限,只能等内核 TCP 重传耗尽(Linux 约 130 秒)。
ResponseHeaderTimeout 只约束连接建立之后等待响应头的阶段,覆盖不到建连。

上游域名被解析到 443 不可达的 IP 时(DNS 污染 / 路由异常),单个账号就要卡满
内核超时;而多账号故障转移是串行的,一次请求会阻塞数分钟且不写中间错误,
客户端表现为"长时间无首字后直接结束"。

- buildUpstreamTransport 显式设置 DialContext(10s)与 TLSHandshakeTimeout(10s)
- proxyutil SOCKS5 分支的 forward dialer 由 proxy.Direct(零值 dialer,同样无超时)
  换成带 10s 超时的 net.Dialer;该分支会覆盖 Transport.DialContext,
  调用方设置的建连超时对它无效,必须在此补上

Fixes #5151
2026-08-03 19:59:58 +08:00

76 lines
2.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package repository
import (
"context"
"net"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/require"
)
// 回归:上游 Transport 必须显式配置建连超时。
//
// http.Transport.DialContext 为 nil 时 Go 使用零值 net.Dialer(Timeout=0),
// DNS 解析与 TCP 握手没有任何上限,只能等内核重传耗尽(Linux 约 130 秒)。
// ResponseHeaderTimeout 只覆盖连接建立之后的阶段,管不到建连。
// 上游域名被解析到不可达 IP 时,串行的多账号故障转移会把一次请求拖到数分钟。
func TestBuildUpstreamTransportSetsDialTimeout(t *testing.T) {
settings := defaultPoolSettings(nil)
transport, err := buildUpstreamTransport(settings, nil, upstreamProtocolModeDefault)
require.NoError(t, err)
require.NotNil(t, transport.DialContext, "DialContext 缺失会退化为无超时的零值 dialer")
require.Equal(t, defaultUpstreamTLSHandshakeTimeout, transport.TLSHandshakeTimeout)
}
func TestNewUpstreamDialerHasBoundedTimeout(t *testing.T) {
dialer := newUpstreamDialer()
require.Greater(t, dialer.Timeout, time.Duration(0), "建连超时必须有上限")
require.Equal(t, defaultUpstreamDialTimeout, dialer.Timeout)
require.Equal(t, defaultUpstreamDialKeepAlive, dialer.KeepAlive)
}
// 建连超时对 HTTP 代理同样生效:Transport.Proxy 走的仍是 DialContext,
// 代理地址不可达时必须快速失败而不是挂满内核超时。
func TestBuildUpstreamTransportKeepsDialTimeoutWithHTTPProxy(t *testing.T) {
proxyURL, err := url.Parse("http://127.0.0.1:1080")
require.NoError(t, err)
transport, err := buildUpstreamTransport(defaultPoolSettings(nil), proxyURL, upstreamProtocolModeDefault)
require.NoError(t, err)
require.NotNil(t, transport.Proxy)
require.NotNil(t, transport.DialContext)
}
// SOCKS5 分支会覆盖 Transport.DialContext,覆盖后仍必须是有超时的拨号器。
func TestBuildUpstreamTransportKeepsDialContextWithSOCKS5Proxy(t *testing.T) {
proxyURL, err := url.Parse("socks5h://127.0.0.1:1080")
require.NoError(t, err)
transport, err := buildUpstreamTransport(defaultPoolSettings(nil), proxyURL, upstreamProtocolModeDefault)
require.NoError(t, err)
require.NotNil(t, transport.DialContext)
}
// Timeout 字段确实被 net.Dialer 用于建连:拨一个已被 close 的本地监听端口,
// 断言 Dialer 走的是自己的超时路径而不是无限等待。
// (不依赖外网可达性,CI 中确定性执行。)
func TestUpstreamDialerRespectsContextCancellation(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
addr := listener.Addr().String()
require.NoError(t, listener.Close())
ctx, cancel := context.WithCancel(context.Background())
cancel()
conn, err := newUpstreamDialer().DialContext(ctx, "tcp", addr)
if conn != nil {
_ = conn.Close()
}
require.Error(t, err, "已取消的 context 必须立即中止拨号")
}