# httpc:复用连接的 HTTP 客户端 `httpc` 在标准 `net/http` 上提供独立连接池、网络阶段总超时、保守重试、请求追踪和有界 JSON 调用。普通接口可以直接带认证头调用;上传、下载、特殊协议仍使用标准 Request/Response。没有业务身份模型或全局认证信息。 ## 创建一次,业务重复使用 ```go 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](../examples/httpClient/README.md),只使用本地临时服务,无需业务 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 的单次请求选项 ```go 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 的流式语义。 `WithSuccessStatuses(200)` 限制本次调用只接受 200;可传多个,例如 `(200, 201)`。不传时保持全部 2xx。列表必须非空且每项位于 200–299,重复项允许;后一个状态选项替换前一个列表。构造选项时快照,之后修改原 slice 不影响请求,并发复用不串配置。 ```go err := client.JSON(ctx, http.MethodGet, endpoint, nil, &result, httpc.WithHeader("Authorization", credential), httpc.WithSuccessStatuses(http.StatusOK), ) var status *httpc.StatusError if errors.As(err, &status) { /* status.StatusCode 是实际收到的状态 */ } ``` 未允许的状态(包括 201/204)返回 StatusError,不解码、不改变 output;仍关闭响应体、按原有时间/大小预算回收连接,不读取无限错误正文。状态选项不会增加重试,原有 Do 重试规则保留。它只限制 HTTP 状态,不保证响应非空或业务字段完整,业务仍校验自身协议。严格状态、关闭与连接复用见 [示例](../examples/developerTools/README.md)。 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 中加一个字段: ```go Outbound httpc.Config `json:"outbound"` ``` 项目 Validate 中调用 `c.Outbound.Validate()`;创建时 New 也会验证。svc 初始化一次并通过生命周期登记 Close。文件例子: ```yaml 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}"`;固定值不会被环境自动覆盖。无需手写解析。详见 [统一配置](../CONFIG.md)。 ## 标准请求、特殊 TLS 与生命周期 ```go 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,没有引入新第三方依赖。