Documentation
¶
Overview ¶
Package httpjson stdlib-only 的 HTTP+JSON 调用纪律单源:ctx 感知构造请求 → 执行 → 限容读体(读错误不吞)→ 状态码检查(错误体 rune 安全截断,不腰斩 UTF-8)→ JSON 解码(out 非 nil 时;out==nil 跳过解码,只判状态码),外加 可选的指数退避重试与服务端 Retry-After 建议捕获。
与 network/httpx 的分工(同名不同物):那侧是 Hertz 底座的服务间调用基础 设施(服务发现/熔断/指标/追踪,编译期即拉 hertz+prometheus+gobreaker+otel 依赖链);本包纯函数、调用方注入 *http.Client——被嵌入的 SDK 与外部 API 调用方(LLM 网关、第三方开放平台)复用,不把微服务全家桶拖进依赖树。 收编自 agentkit/httpx(v0.10.41 起单源在此,agentkit 侧消费本包)。
Index ¶
- Constants
- func CanonicalETagKey(method, url string, varyValues ...string) string
- func DoJSON(ctx context.Context, client *http.Client, req Request, out any, ...) error
- func DoJSONWithRetry(ctx context.Context, client *http.Client, reqFn func() (Request, error), ...) error
- func NormalizeBaseURL(raw string) string
- func RetryAfterFrom(ctx context.Context) time.Duration
- func SelectRetryDelay(local, hint time.Duration) time.Duration
- func WithRetryAfterSink(ctx context.Context) context.Context
- type ETagStore
- type ETagStoreOption
- type RawResponse
- type Request
- type RequestOption
- type RetryAfterTransport
- type RetryConfig
- type StatusError
Constants ¶
const DefaultETagStoreEntries = 1024
DefaultETagStoreEntries NewMemETagStore 缺省条目数上限(#289)。 ETag 键空间随数据集自然增长(分页页码/时间过滤/cursor 都在键里),无界 map 会让长驻轮询进程内存无界增长——缺省给 LRU 上限,需要更多条目的调用 方显式 WithETagStoreCapacity 放开。
const DefaultMaxETagBodySize = 1 << 20 // 1 MiB
DefaultMaxETagBodySize NewMemETagStore 缺省单条缓存体上限(#289)。 ETag 端点多是页面级 JSON 元数据(页面级缓存受益);tarball/大 diff/大日志 这类大响应不值得驻留内存——超限条目直接不入存储,每次全量取。
const MaxServerRetryAfter = 5 * time.Minute
MaxServerRetryAfter 服务端退避建议的可信上限:超过 5 分钟的建议不直接采信 (长时间限流应交由上层 failover/切换处置,而非原地等)。
Variables ¶
This section is empty.
Functions ¶
func CanonicalETagKey ¶ added in v0.38.2
CanonicalETagKey 构造规范 ETag 缓存键(#290):method、url 与各 vary 头 值以 "\n" 分隔拼接——分段拼接 + 分隔符保证各段边界无歧义("a"+"bc" 与 "ab"+"c" 不会撞键)。method 规范为大写("get" 与 "GET" 同键);url 与 各值 TrimSpace 后原样入键。
自行拼键的调用方(自管 vary 集合、或 store 侧需要同形键)用它替代手写 "GET "+url——影响响应的头(Authorization/Accept 最小集)必须作为 varyValues 参与;带 Vary 语义的调用方按响应的 Vary 头自行扩展。顺序由 调用方固定(同组头的值按同一顺序传入即稳定)。
func DoJSON ¶
func DoJSON(ctx context.Context, client *http.Client, req Request, out any, opts ...RequestOption) error
DoJSON 执行请求并把 2xx 响应体解码进 out。非 2xx 返回带状态码与响应体摘要 (rune 安全截断到 200 字符)的错误;解码失败返回包装后的解析错误。 错误不带包前缀返回,调用方以自己的包前缀包装一次。
out 为 nil 时**跳过响应体解码**:2xx 即返回 nil。这是 fire-and-forget 调用方 的语义——只关心请求送达与状态码,不关心响应体。此前无条件 json.Unmarshal(raw, nil) 恒定拿到 `json: Unmarshal(nil)` 的 InvalidUnmarshalError ⇒ **服务端已 返回 2xx 的成功请求被误报为失败**,真失败反而淹没在假失败里(#158)。 非 2xx 的判定与 out 无关:照样返回 StatusError(错误体摘要照旧带上)。
out 非 nil 时行为不变:解码失败仍返回「响应解析失败」错误。
opts 是单次请求的可选行为注入(RequestOption;变参零选项时调用面与旧 签名逐字节一致)。目前支持 WithETag / WithETagVary(条件请求:If-None- Match/304 重放为 200 形响应,#269;vary 头分区键与 no-store 门禁见 #290/#288)——304 在函数内部消化,调用方拿到的 out/err 与 200 路径完全 同形,无需感知条件请求的存在。
func DoJSONWithRetry ¶
func DoJSONWithRetry(ctx context.Context, client *http.Client, reqFn func() (Request, error), cfg RetryConfig, out any, opts ...RequestOption) error
DoJSONWithRetry 带重试的 HTTP+JSON 调用。
reqFn 每次尝试重新构造 Request(请求体流不可重读,且调用方可能希望每轮 刷新签名头);reqFn 自身报错视为调用方参数问题,立即返回不重试。 重试间指数退避 + Jitter 抖动;ctx 取消/超时立即返回 ctx.Err()。 配合 WithRetryAfterSink + RetryAfterTransport 可让 429 的服务端建议 参与退避取舍(调用方在 wait 前经 SelectRetryDelay(RetryAfterFrom(ctx)) 采信)。
out 的语义与 DoJSON 完全一致:out==nil 时跳过响应体解码,2xx 即成功返回 (fire-and-forget);非 2xx 仍按可重试判定退避并最终返回 StatusError(#158)。
opts 逐轮透传给 DoJSON(每次尝试都套用)——轮询消费方把 WithETag 与 重试组合:条件命中的 304 重放对重试层透明,限流预算敏感的场景两件套 各管各的(#269)。
func NormalizeBaseURL ¶ added in v0.36.0
NormalizeBaseURL 规范化「SDK base 配置」形态的站点地址(zentao/内部平台 类客户端的 server 字段高频踩点):补默认 scheme、去结尾斜杠、去首尾空白。
规则(字符级,不整体解析 URL——畸形输入按同样的字符规则走一遍。刻意不走 url.Parse 重组:u.String() 会重排/百分号转义输入,违背「小写输入零变化」; 这里只做三段字符级切分):
- 首尾空白去除;空串原样返回(调用方的「未配置」语义不被改写);
- scheme 前缀判定按 RFC 3986 §3.1 大小写不敏感(#239):HTTP://、 Https:// 与小写形态等价,命中后 scheme 归一为小写(RFC 3986 的规范 形态),不再被二次补 http://(此前 `HTTPS://EXAMPLE.COM` 会产出 `http://HTTPS://EXAMPLE.COM`——scheme 降级 https→http、host 被截成 `HTTPS:`)。只认 http/https 两个 scheme:`host:port`、`mailto:x` 等 其余形态一律视为无 scheme 补 `http://`(v0.36.0 起的既有行为不变);
- 不带 scheme 时补 `http://`——不猜 https:内网自建服务占多数, 显式配置 https 的调用方不受影响;
- 尾斜杠只裁 path 段(#240):scheme 的 `//` 永不动(scheme-only 输入 `http://`、`http:///` 保持 `http://`,不再截成 `http:`);`?query` 与 `#fragment` 原样保留(首个 ?/# 之后的内容不参与裁剪——此前 `http://host/?x=/` 的 query 值 `/` 被整串 TrimRight 静默吃成空串)。
收编自 zentao-mini/zentao-max 四份同源内联规范化(上游 zentao 矩阵#22 下沉)。
func RetryAfterFrom ¶
RetryAfterFrom 读取最近一次 429 的服务端建议(0 = 无)。重试环在计算退避时调用。 sink 不随尝试清除:仅对本次失败确为限流的场景采信(陈旧建议不得套用到 非限流错误上),门控归调用方的重试策略。
func SelectRetryDelay ¶
SelectRetryDelay 退避取舍(「合理性钳制」):服务端建议 >0 且 (≤MaxServerRetryAfter 或 < 本地曲线值)→ 采信建议;否则本地曲线。
Types ¶
type ETagStore ¶ added in v0.38.0
type ETagStore interface {
// Get 返回 key 对应的 (etag, 缓存体, 是否命中)。
Get(key string) (etag, body string, ok bool)
// Put 写入 key 的 (etag, 缓存体)。
Put(key, etag, body string)
}
ETagStore 按请求键存取响应 ETag 与缓存体(#269)。
存储与客户端分离:进程内用 NewMemETagStore(有界:LRU 条目上限 + 单条 体积上限),跨进程的调用方自行实现本接口接 Redis 等后端——Get/Put 都 可能被并发调用,实现须并发安全。key 由调用方给(推荐经 CanonicalETagKey / WithETagVary 构造,把「影响响应的头必须进键」变成构造期保证);同一个 store 允许多 key 并存。
func NewMemETagStore ¶ added in v0.38.0
func NewMemETagStore(opts ...ETagStoreOption) ETagStore
NewMemETagStore 进程内 ETagStore(#289 有界化:LRU 条目上限 + 单条体积 上限,缺省 DefaultETagStoreEntries / DefaultMaxETagBodySize,经 ETagStoreOption 覆盖)。body 以 string 持有(Go string 不可变,Get 返回 的副本与存储内部天然隔离)。
与旧版(无淘汰无上限的裸 map)的差异:条目超限按 LRU 淘汰最久未用者, 超大响应不入存储——被淘汰/未入存的 key 下次请求按无条件执行,行为退化为 「无缓存」,不是错误。
type ETagStoreOption ¶ added in v0.38.2
type ETagStoreOption func(*memETagStoreConfig)
ETagStoreOption NewMemETagStore 的构造选项(仓规禁裸 Option)。
func WithETagStoreCapacity ¶ added in v0.38.2
func WithETagStoreCapacity(entries int) ETagStoreOption
WithETagStoreCapacity 覆盖条目数上限(LRU 淘汰)。entries < 1 是配置 错误,构造期 panic(仓规总则①)——容量 0 的 store 什么都不缓存,静默 生效等于「配了缓存却不工作」。
func WithETagStoreMaxBodySize ¶ added in v0.38.2
func WithETagStoreMaxBodySize(n int64) ETagStoreOption
WithETagStoreMaxBodySize 覆盖单条缓存体上限(字节)。超限响应不入存储 (每次全量取——ETag 缓存的受益面是页面级元数据,不是大载荷)。n < 1 是配置错误,构造期 panic(仓规总则①);不提供「无上限」档——无界正是 #289 要收口的缺陷,确需无界的调用方自行实现 ETagStore。
type RawResponse ¶ added in v0.29.0
type RawResponse struct {
// StatusCode 是 HTTP 状态码(不判 2xx,由调用方按自己的语义映射)。
StatusCode int
// Body 是响应体原始字节(最多 limit 字节)。
Body []byte
// Truncated 报告 Body 是否因超过 limit 而被截断。
//
// 截断本身是刻意的(limit 防内存放大),但**必须可观测**——否则调用方
// 会把「半截报文」当成完整报文用。
Truncated bool
}
RawResponse 是 DoRaw 的原始响应结果。
func DoRaw ¶ added in v0.28.1
DoRaw 执行请求并返回**原始**状态码与响应体:不判 2xx、不解 JSON。
适用面(DoJSON 不适用的那些):
- 调用方要**按状态码映射自己的语义**(如 404/409 → 各自的哨兵错误);
- 调用方要把**原文**报文带进错误文案(DoJSON 的错误体是 rune 安全截断到 200 字符的**摘要**,不是原文)。
limit 是读体上限(字节):<=0 时用包级 maxBody。调用方应按自己的报文体量 给一个够用又防内存放大的值(如 8KB)。**超过 limit 的部分会被截断**, 且截断**在 RawResponse.Truncated 里如实标记**——调用方据此判断拿到的是 完整报文还是半截(截断对调用方不可见是最容易埋雷的形态:拿半截 JSON 去 解析会得到难以定位的解析错误,拿半截报文进错误文案会丢服务端说明)。
传输层错误(连不上/超时)返回 err,status/body 无意义;HTTP 层错误(任何状态码) 一律返回 nil err,由调用方按状态码自行判定。
type Request ¶
type Request struct {
// Method HTTP 方法(空 = GET)。
Method string
// URL 完整地址(含 query)。
URL string
// Body 请求体(nil = 无请求体;JSON 调用方自行 Marshal)。
Body []byte
// Header 鉴权/Content-Type 等请求头注入钩子(可 nil)。
Header func(h http.Header)
// contains filtered or unexported fields
}
Request 一次 HTTP+JSON 调用的描述。
type RequestOption ¶ added in v0.38.0
type RequestOption func(*Request)
RequestOption 单次请求的可选行为注入(仓规禁裸 Option:多包 co-import 冲突)。DoJSON/DoJSONWithRetry 以变参收下,零选项调用面与旧签名一致。
func WithETag ¶ added in v0.38.0
func WithETag(store ETagStore, key string) RequestOption
WithETag 为本次请求启用条件请求(#269):命中缓存(store.Get(key) 有值) 时注入 If-None-Match;服务端 304 时重放缓存体为 200 形响应(out 按正常 200 路径解码);响应带 ETag 头且**未带 Cache-Control: no-store**(#288) 时把 (etag, body) 回写存储;ETag 头缺席则不回写(下次请求也就无从注入)。
边界:
- store 为 nil 或 key 为空串是调用方配置错误,构造期 panic(仓规总则 ①:fail-fast,与 cron 构造期校验同口径)——静默按无条件执行会把 「配错了但没生效」藏到限流预算烧穿才发现;
- 只对 GET/HEAD 生效(写方法的 304 语义不同,见包文档第 3 条);
- 304 时回查存储若已被驱逐,降级为无条件重发一次(竞态纪律第 2 条);
- 来源标记(X-From-Cache 之类)刻意未做:DoJSON 的出参面只有 out 与 err,调用方要区分来源可在 store 实现侧埋点。
⚠️ 键形态纪律(#290):key 必须覆盖**影响响应的一切入参**。只含 method+URL 的键在 Authorization 轮换/换发场景会把凭据 A 的响应体重放给 凭据 B(跨凭据缓存串味),Accept 版本协商变体同理。带鉴权的请求 (Request.Header 是鉴权注入钩子)请用 WithETagVary 声明 vary 头,或以 CanonicalETagKey 构造含头分区的键。
用法:
store := httpjson.NewMemETagStore()
err := httpjson.DoJSON(ctx, client, req, &resp,
httpjson.WithETagVary(store, "GET "+url, "Accept", "Authorization"))
func WithETagVary ¶ added in v0.38.2
func WithETagVary(store ETagStore, key string, varyHeaders ...string) RequestOption
WithETagVary 条件请求 + vary 头分区(#290):行为与 WithETag 一致,另把 varyHeaders 各头(大小写不敏感,按 canonical MIME 名取值)在**本次请求 实际生效的请求头**里的值并入缓存键——键形如 CanonicalETagKey(method, key, 值...)。凭据轮换(Authorization 换发)与 版本协商(Accept 变体)天然分区,互不串缓存。
varyHeaders 为空等价 WithETag;头名空串/纯空白是配置错误,构造期 panic (仓规总则①)。Request.Header 钩子会为键派生**多调一次**(作用于独立 的临时 http.Header),钩子应保持无副作用。头在请求里缺席按空串计—— 「没带该头」的请求共用一个分区,与 HTTP Vary 语义一致。
带鉴权的轮询请求(最常见组合):
httpjson.DoJSON(ctx, client, req, &out,
httpjson.WithETagVary(store, "GET "+url, "Accept", "Authorization"),
/* req.Header 里注入 Bearer token */)
type RetryAfterTransport ¶
type RetryAfterTransport struct {
Base http.RoundTripper
}
RetryAfterTransport 透明捕获 429 的 Retry-After 头。响应体不做任何改写—— 上层错误构造不受影响。Base 为 nil 时用 http.DefaultTransport。
type RetryConfig ¶
type RetryConfig struct {
// MaxAttempts 总尝试次数(含首次;<1 视为 1)。
MaxAttempts int
// BaseDelay 首次重试延迟(<=0 取 200ms),按尝试轮次指数退避。
BaseDelay time.Duration
// MaxDelay 单次重试延迟上限(<=0 取 2s)。
MaxDelay time.Duration
// Jitter 随机抖动幅度(防惊群;<0 取 0)。
Jitter time.Duration
// Retryable 自定义可重试判定(nil = 默认:网络超时或 HTTP 5xx/429 可重试;
// 4xx 参数错、响应解析失败不重试)。
Retryable func(err error) bool
}
RetryConfig 一次重试策略(零值 = 仅执行一次,不重试)。
type StatusError ¶
StatusError 非 2xx 响应错误:供重试判定等按状态码分类(errors.As 提取)。
func (*StatusError) Error ¶
func (e *StatusError) Error() string