Files
go-omni/httpc/README.md

10 KiB
Raw Permalink Blame History

httpc:复用连接的 HTTP 客户端

httpc 在标准 net/http 上提供独立连接池、网络阶段总超时、保守重试、请求追踪和有界 JSON 调用。普通接口可以直接带认证头调用;上传、下载、特殊协议仍使用标准 Request/Response。没有业务身份模型或全局认证信息。

创建一次,业务重复使用

client, err := httpc.New(httpc.Config{
    Proxy: httpc.ProxyConfig{Mode: httpc.ProxyDirect},
    Timeout: 3 * time.Second,
    DialTimeout: time.Second,
    TLSHandshakeTimeout: time.Second,
    ResponseHeaderTimeout: time.Second,
    MaxConnsPerHost: 32,
    MaxIdleConnsPerHost: 16,
    MaxIdleConns: 64,
    IdleConnTimeout: 60 * time.Second,
    MaxResponseBytes: 1 << 20,
})
if err != nil { return err }
// 在 svc 初始化并持有 client,向生命周期登记 client.Close。
// 普通独立程序也可 defer client.Close();不要每次请求重新 New。

err = client.JSON(ctx, http.MethodGet, serviceURL, nil, &result,
    httpc.WithHeader("Authorization", "Bearer "+token),
    httpc.WithHeaders(http.Header{"Accept": {"application/json"}}),
)

Client 可并发使用。每个调用的 input/output 及其读写同步由调用方负责,不要让多个请求同时写同一个 output。

直接运行 examples/httpClient,只使用本地临时服务,无需业务 Token 或外部基础设施。

代理规则

配置 行为
不填写 Proxy 保留被克隆 DefaultTransport 的代理策略;未改动的标准库默认使用环境代理
Mode: httpc.ProxyEnvironment 明确使用 HTTP_PROXY / HTTPS_PROXY / NO_PROXY(及小写形式)
Mode: httpc.ProxyDirect 设置 Proxy=nil,明确直连,不受环境代理影响
Mode: httpc.ProxyURL, URL: "http://proxy.example:8080" 固定代理地址,不通过 NO_PROXY 排除

支持 http、https、socks5、socks5h;底层行为沿用 Go Transport。固定地址必须是代理 origin,不接受业务路径、查询、fragment 或无效端口。凭证可由 URL userinfo 提供,但配置错误不输出 URL/密码;不要打印整个 Config。环境代理规则由标准库读取并缓存,不提供运行时环境代理热更新。

内部服务要直连,只设置 proxy.mode: direct,无需创建 Transport。需要自定义路由、代理认证握手等复杂逻辑时注入自己的 Transport。

超时和连接池

除原有字段外,新字段都可选:0 保留默认行为,正数覆盖,负数拒绝。需要明确关闭某个阶段超时、关闭 TCP keepalive 或特殊 dialer 时,使用自定义 Transport。

字段 默认/含义
Timeout 10s;从 Do 开始,覆盖 DNS/建连、等待连接、重试、退避、响应头和读取响应体
DialTimeout 不填时保留原 dialer;标准库默认 30s;配置后使用标准 net.Dialer
DialKeepAlive 标准 net.Dialer 的 TCP keepalive 探测间隔,标准默认 30s;不是 HTTP 空闲连接超时
TLSHandshakeTimeout 沿用克隆 Transport;标准库默认 10s
ResponseHeaderTimeout 沿用 Transport,标准默认不单独限制;从完整发送请求后等响应头,总 Timeout 仍生效
ExpectContinueTimeout 沿用 Transport;标准默认 1s,等待 100-continue 的时间
IdleConnTimeout 沿用 Transport;标准默认 90s,只回收空闲连接
MaxConnsPerHost 默认 64;每个目标的建连中、活动与空闲连接总上限
MaxIdleConnsPerHost 默认等于 MaxConnsPerHost,可单独配置
MaxIdleConns 默认 MaxConnsPerHost×2,可单独配置;所有目标合计的空闲连接上限
MaxResponseBytes JSON 成功响应默认最多 1 MiB,最大允许配置 1 GiB;应按实际接口保持较小限额

每主机上限最多 65536,总空闲连接最多 131072。总空闲上限可以小于各主机上限之和。MaxConnsPerHost 不等于整个进程的连接上限,也不等于 HTTP/2 的请求并发上限。

配置 DialTimeout / DialKeepAlive 会创建标准 net.Dialer;未填的另一个值使用标准默认 30s。不要通过全局修改 DefaultTransport 来塞入项目的特殊拨号逻辑;请显式使用借用的 Transport。若 DefaultTransport 有自定义 TLS dialer,相关阶段覆盖会报错,避免 TLS timeout 被悄悄忽略。

调用者 context 的更早截止时间优先。总超时不能抢占 Go 的 JSON 编解码或不遵守 context 的自定义 RoundTripper;计时从 Do 开始,不包括调用前的 json.Marshal。TLS 校验默认保持开启,没有新增 InsecureSkipVerify 配置。

JSON 的单次请求选项

err := client.JSON(ctx, "POST", endpoint, input, &output,
    httpc.WithHeader("Authorization", "Bearer "+token),
    httpc.WithHeader("X-Tenant", tenant),
    httpc.WithMaxResponseBytes(64<<10),
)
  • WithHeader 设置一个头;WithHeaders 支持多值 http.Header。后出现的选项覆盖同名头,不影响其他头。
  • WithHeaders 在创建选项时快照原 map,每次调用复制值。构建后修改原 map 不影响选项,选项可以并发复用;构建时原 map 的读写同步由调用方负责。
  • 不提供 Client 全局 Authorization,避免跨用户调用共享凭证。框架不会把请求头写进日志。
  • 有 input 时默认 Content-Type 为 application/json;显式请求头可覆盖。请求头名称/值会校验,拒绝 CRLF 等控制字符和歧义的大小写重复键。
  • Host、Content-Length、Transfer-Encoding、Trailer 属于请求目标/分帧控制,请用标准 Request 配合 Do;JSON 选项中不允许伪装成普通头。
  • WithMaxResponseBytes 是本次 JSON 的正数限额,不修改 Client 默认值,不改变 Do 的流式语义。

JSON 自动关闭响应体。仅 2xx 解码;空成功响应(例如 204)保留 output 原值。非空响应使用标准 json.Unmarshal,拒绝多个 JSON 文档,类型错误保留 Go 的错误信息与行为。

output == nil 按限额流式丢弃成功响应,不把整个 body 读进内存,也不检查 JSON 语法。响应限额按实际读取的 body 计算;标准库自动 gzip 解压后仍受上限约束。input 会先 json.Marshal 到内存,大文件上传不适合用 JSON helper。

非 2xx 返回 *httpc.StatusError,只包含状态码,不包含错误响应体。为连接复用,只尝试读完声明长度已知且不超过 min(32 KiB, 本次响应限额) 的小错误体,最多等待 25ms(或更早总截止时间);失败仍返回原 HTTP 状态。未知长度/大错误体立即关闭,优先及时返回。重试过程的错误体另受 32 KiB 大小及整体请求截止时间约束,不无界读取。

配置文件与生成项目

在项目 Config 中加一个字段:

Outbound httpc.Config `json:"outbound"`

项目 Validate 中调用 c.Outbound.Validate();创建时 New 也会验证。svc 初始化一次并通过生命周期登记 Close。文件例子:

outbound:
  timeout: 3s
  proxy:
    mode: direct
  dialTimeout: 500ms
  tlsHandshakeTimeout: 1s
  responseHeaderTimeout: 1s
  maxConnsPerHost: 32
  maxIdleConnsPerHost: 16
  maxIdleConns: 64
  idleConnTimeout: 60s
  maxResponseBytes: 1048576

统一加载入口读取 JSON/YAML/XML。需要环境变量时在文件明确写 mode: "${HTTP_PROXY_MODE:-direct}"、dialTimeout: "${HTTP_DIAL_TIMEOUT:-500ms}";固定值不会被环境自动覆盖。无需手写解析。详见 统一配置。

标准请求、特殊 TLS 与生命周期

req, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil)
if err != nil { return err }
req.Header.Set("Authorization", "Bearer "+token)
res, err := client.Do(req)
if err != nil { return err }
defer res.Body.Close()
// 自行判断状态,按项目要求限流/限制大小,流式写入目标;不要无界 ReadAll 大文件。
_, err = io.Copy(destination, res.Body)

Do 不自动限制 body,允许下载/流式处理,调用方必须关闭。响应体消费/关闭前网络超时持续有效。

Config.Transport 可注入任意 http.RoundTripper,适配自定义 dialer、mTLS、根证书、监控包装或测试替身。框架借用它,不修改、不调用其 CloseIdleConnections。代理、连接池、TLS/建连阶段配置不能同时填入,避免无效配置;总 Timeout、JSON 限额、框架重试/日志仍有效。借用实现必须遵守标准请求和 body 所有权、并发及 context 约定。

Close 幂等:拒绝新调用,关闭自有池的空闲连接,不主动打断已开始的请求。服务关闭应先排空 HTTP/后台调用,再关闭 Client;它不是后台 drain 的替代品。生成项目可通过 svc 的生命周期登记清理。

重试、日志与兼容

MaxRetries 默认 0,最多 5。仅可重放的 GET/HEAD/OPTIONS 对网络错误、429/502/503/504 做框架重试。POST/PUT/PATCH/DELETE 不自动做应用层重试。标准库或自定义 Transport 自己的重放规则仍可能生效,不能承诺业务“恰好执行一次”。

Backoff 默认 100ms,MaxBackoff 默认 2s,采用指数退避与抖动。Retry-After 要求等待超过 MaxBackoff 时返回当前响应,不提前请求;总截止时间限制全部尝试。成功或无重试的调用不计算退避。

不跟随重定向,避免自动向其他地址转发认证信息;3xx 由调用者判断。默认传递 context 中的 X-Request-ID,显式请求头优先。日志只包含 method、hostname、status、attempt、收到响应头的耗时等,不打印路径、查询、用户名密码、认证头或响应体;标准 net/http 返回的 URL 错误可能携带地址,项目不要把 URL 中的密钥当作安全日志字段。

v0.21 兼容说明:

  • 原来的 JSON 五参数直接调用继续有效;新方法末尾增加 ...httpc.RequestOption,自行定义的 interface/方法值类型需要同步签名。
  • 原默认代理、总超时、重试、池大小和成功响应含义保留;新字段未设置时不改变它们。
  • Transport 与显式池/代理/阶段参数混用现在报错;此前 MaxConnsPerHost 在自定义 Transport 下实际被忽略。迁移时把参数放到自己的 Transport,或删掉 Transport 改用内置配置。
  • 没有移除 Do、JSON、StatusError 或自定义 Transport,没有引入新第三方依赖。