httpjson

package
v0.38.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 8, 2026 License: MIT Imports: 17 Imported by: 0

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

View Source
const DefaultETagStoreEntries = 1024

DefaultETagStoreEntries NewMemETagStore 缺省条目数上限(#289)。 ETag 键空间随数据集自然增长(分页页码/时间过滤/cursor 都在键里),无界 map 会让长驻轮询进程内存无界增长——缺省给 LRU 上限,需要更多条目的调用 方显式 WithETagStoreCapacity 放开。

View Source
const DefaultMaxETagBodySize = 1 << 20 // 1 MiB

DefaultMaxETagBodySize NewMemETagStore 缺省单条缓存体上限(#289)。 ETag 端点多是页面级 JSON 元数据(页面级缓存受益);tarball/大 diff/大日志 这类大响应不值得驻留内存——超限条目直接不入存储,每次全量取。

View Source
const MaxServerRetryAfter = 5 * time.Minute

MaxServerRetryAfter 服务端退避建议的可信上限:超过 5 分钟的建议不直接采信 (长时间限流应交由上层 failover/切换处置,而非原地等)。

Variables

This section is empty.

Functions

func CanonicalETagKey added in v0.38.2

func CanonicalETagKey(method, url string, varyValues ...string) string

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

func NormalizeBaseURL(raw string) string

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

func RetryAfterFrom(ctx context.Context) time.Duration

RetryAfterFrom 读取最近一次 429 的服务端建议(0 = 无)。重试环在计算退避时调用。 sink 不随尝试清除:仅对本次失败确为限流的场景采信(陈旧建议不得套用到 非限流错误上),门控归调用方的重试策略。

func SelectRetryDelay

func SelectRetryDelay(local, hint time.Duration) time.Duration

SelectRetryDelay 退避取舍(「合理性钳制」):服务端建议 >0 且 (≤MaxServerRetryAfter 或 < 本地曲线值)→ 采信建议;否则本地曲线。

func WithRetryAfterSink

func WithRetryAfterSink(ctx context.Context) context.Context

WithRetryAfterSink 在 ctx 注入 sink(重试环上游调用一次;传输层据此回写)。

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

func DoRaw(ctx context.Context, client *http.Client, req Request, limit int64) (RawResponse, error)

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。

func (RetryAfterTransport) RoundTrip

func (t RetryAfterTransport) RoundTrip(req *http.Request) (*http.Response, error)

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

type StatusError struct {
	Code int
	Body string
}

StatusError 非 2xx 响应错误:供重试判定等按状态码分类(errors.As 提取)。

func (*StatusError) Error

func (e *StatusError) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL