Day 14 / 共 20 天 · 第 3 周 HTTP 与外部调用
HTTP 外部调用
插件常需要"调个外部服务"——鉴权中心、AI 后端、配置服务。今天拆 HttpCall:用昨天(Day 13)的 Cluster 指定"调谁",异步发起、回调处理,配合 Day 11 学的 ActionPause → Resume 的节奏。这是 Wasm 异步编程的核心,也是明天 Redis 调用(Day 15)的同一套思路。
📍 你在整门课的位置(wasm-go 20 天 · 第 3 周 HTTP 与外部调用)
Cluster D13→
HttpCall 异步 D14→
Redis D15→
日志 D16→
Leader D17
L01
为什么要外部调用
💡 本质:异步回调 = 餐厅取餐叫号
你在快餐店点了餐,店员给你一个叫号器(发起 HttpCall)就让你去坐(
ActionPause 让出线程)——不会让你堵在柜台前干等。后厨做好了,叫号器一响(回调触发),你再去取餐、开吃(回调里处理 + ResumeHttpRequest)。柜台(worker 线程)因此能同时服务很多人,而不是被你一个人占住。这就是"不能阻塞"逼出来的异步模型。插件常常不能"就地解决"
很多逻辑需要问别人:鉴权要问鉴权中心"这个 token 有效吗"、AI 网关要调 LLM、限流要问 Redis 当前计数。这些都要在处理请求的过程中调用外部服务。但 Wasm 不能阻塞(Day 03)——所以外部调用是"异步回调式"的:发起后立刻返回
ActionPause 让出线程,结果到了在回调里继续。适应这个思维是写复杂插件的关键。图注:Pause 期间 worker 不空等——它去服务别人;外部结果到了才回调、Resume,请求接着走。这是"叫号器"模型的时间线版。
L02
ClusterClient
http_wrapper.go:46-86:ClusterClient[C Cluster] 包住一个 Cluster,提供 Get/Post/Put/Delete 等方法:
client := wrapper.NewClusterClient(wrapper.FQDNCluster{FQDN: "api.example.com", Port: 443})
client.Post("/v1/check", headers, body, callback, 5000) // 5s 超时
// Get/Head/Options/Post/Put/Patch/Delete/Connect/Trace/Call 都有(:54-82)
读法:
ClusterClient 是对 HttpCall 的友好封装——你不用记 HTTP 方法字符串,直接 .Post(...)。泛型参数 C Cluster 让它接受任意一种 Cluster(Day 13)。这是写插件时发起 HTTP 调用的推荐方式。L03
http-call 例子
examples/http-call/main.go 完整演示(parseConfig 里建 client,请求头钩子里发起调用):
func parseConfig(json gjson.Result, config *HttpCallConfig) error {
cluster := wrapper.FQDNCluster{FQDN: json.Get("fqdn").String(), Port: json.Get("port").Int()}
config.client = wrapper.NewClusterClient(cluster) // 建好 client 存进配置
config.requestPath = json.Get("path").String()
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config HttpCallConfig) types.Action {
err := config.client.Post(config.requestPath, headers, body,
func(statusCode int, responseHeaders http.Header, responseBody []byte) { // 回调
proxywasm.AddHttpRequestHeader("X-External-Response", string(responseBody))
proxywasm.ResumeHttpRequest() // 结果到了 → 恢复请求
}, 5000)
if err != nil { return types.ActionContinue }
return types.ActionPause // 先暂停,等回调
}
读法:注意两个关键点:① 请求头钩子返回
ActionPause(先别转发,等外部结果);② 回调里处理完调 ResumeHttpRequest()(恢复请求继续转发)。client 在 parseConfig 阶段建好、存进配置,请求时复用。L04
Pause / Resume 节奏
钩子发起 HttpCall→
returnActionPause→
网关去处理别的请求→
响应到触发回调→
回调处理+Resume→
网关继续转发
这个节奏一定要记牢
发起异步调用 → 返回 Pause(让出线程)→ 网关空出来干别的 → 你的调用有结果了 → 回调被触发 → 回调里处理结果 + Resume → 请求从暂停处继续。忘了 Resume 请求就永远卡住(超时);不返回 Pause 请求会在结果没到时就往下走(拿不到结果)。Pause 和 Resume 必须成对——这是异步插件最容易出 bug 的地方。
📝 单步走查:一个请求闯"鉴权后端"的全过程
t1 请求头钩子被调 → 发起
t2 worker 去处理别的请求;本请求的
t3 鉴权后端返回
t4 回调里判断通过 →
t5 网关把请求转发给真正的后端。✅ 全程 worker 没被"干等"占用。
client.Post("/check", ...),返回 ActionPause。此刻请求状态 = 暂停。t2 worker 去处理别的请求;本请求的
ctx 挂起等待。t3 鉴权后端返回
200 {"ok":true} → 回调触发,statusCode=200。t4 回调里判断通过 →
proxywasm.ResumeHttpRequest()。请求状态 = 继续。t5 网关把请求转发给真正的后端。✅ 全程 worker 没被"干等"占用。
L05
HttpCall 内部
http_wrapper.go:90-160 的 HttpCall 底层用 proxywasm.DispatchHttpCall:
// 先剔除用户传的 :method/:path/:authority 伪头(避免冲突,:91-96)
// 解析 rawURL → authority/path
authority := cluster.HostName() // 默认用 Cluster 的 HostName
if parsedURL.Host != "" { authority = parsedURL.Host }
// 补上伪头
headers = append(headers, {":method",method}, {":path",path}, {":authority",authority})
// 发起(默认超时 500ms)
proxywasm.DispatchHttpCall(cluster.ClusterName(), headers, body, nil, timeout, func(...) {
// 响应回调(见 L06)
})
读法:看到 Day 13 的伏笔回收了——
cluster.ClusterName() 定"发给谁"、cluster.HostName() 定 :authority。DispatchHttpCall 是 proxy-wasm 的宿主调用,把请求交给 Envoy 发出,Envoy 收到响应后触发你的回调。每次调用生成 requestID(uuid)用于日志追踪。L06
回调解析响应
http_wrapper.go:121-152 回调里解析响应:
respBody, _ := proxywasm.GetHttpCallResponseBody(0, bodySize)
respHeaders, _ := proxywasm.GetHttpCallResponseHeaders()
code := http.StatusBadGateway
for _, h := range respHeaders {
if h[0] == ":status" { code, _ = strconv.Atoi(h[1]) } // 从 :status 伪头取状态码
headers.Add(h[0], h[1])
}
callback(code, headers, respBody) // 调你注册的回调,交给你处理
SDK 帮你把原始响应"翻译"成好用的形式
底层拿到的是原始头/体,SDK 帮你解析出状态码(从
:status 伪头)、组装成 http.Header,再调你的回调 callback(code, headers, body)。你只管在回调里用这三个值做业务(判断成功、读 body、改请求)。注意日志用 log.UnsafeInfof(Day 16 讲)——因为响应体可能含敏感信息。L07
超时与陷阱
- 默认超时 500ms(
:116-119),可传参覆盖(timeoutMillisecond ...uint32)。 - 超时/失败时回调仍会被调用(
code可能是502 BadGateway),要处理错误分支。 - IO 周期陷阱:插件逻辑太重 + 高并发时,回调可能来不及在本 IO 周期处理——用
WithMaxRequestsPerIoCycle(Day 10/19)缓解。
⚠️ 常见误解:小白常以为"回调只在成功时被调用"。其实超时、连接失败也会触发回调,只是
code 是 502 之类。所以别把 Resume 只写在 if code==200 里——否则失败分支的请求会一直悬着直到超时。别在回调里忘了 Resume
无论成功失败,回调里都要有明确的下一步:成功用结果 + Resume,失败也要 Resume(或 SendHttpResponse 返回错误)——绝不能让请求悬着。常见 bug:只在成功分支 Resume,失败时请求卡到超时。写异步插件的黄金法则:每条路径都要终结请求。
L08
今日小结 + 动手
🧠 今天你应该能回答
- 为什么外部调用必须异步回调?
- Pause/Resume 的完整节奏?忘了 Resume 会怎样?
- HttpCall 怎么用 Cluster 的 ClusterName/HostName?
- 回调里怎么拿状态码和 body?默认超时多少?
✋ 动手
cd /Users/bitmart/work/codes/github/higress-group/wasm-go
cat examples/http-call/main.go
sed -n '90,160p' pkg/wrapper/http_wrapper.go
cat examples/complex-http-call/main.go | head -80
明天预告 · Day 15(第 3 周收官):Redis 调用——
RedisClusterClient 怎么 Init、Get/Set/Incr/Eval 等命令、以及为什么它也是异步回调式的。限流、计数类插件的必备能力。