Day 11 / 共 20 天 · 第 3 周 HTTP 与外部调用
HTTP 请求/响应钩子
第 3 周开始处理真实请求。前两周(W1-W2)你学会了"怎么写插件骨架 + 怎么读配置、怎么匹配规则";从今天起进入"插件真正干活"的部分。今天理清一个请求经过插件的"六个阶段"(请求头/体、响应头/体、流式、结束),以及每个钩子返回的 Action 分别让网关做什么——这是后面 Day 12~15 的地基。
📍 你在整门课的位置(wasm-go 20 天 · 第 3 周 HTTP 与外部调用)
配置与匹配 W2→
六阶段钩子 D11→
HttpContext D12→
Cluster D13→
外部调用 D14-15
L01
请求处理的六个阶段
💡 本质:六个阶段 = 机场安检的六道关卡
把插件想成机场安检流水线,一个请求就是一位旅客。值机(请求头)→ 开箱查行李(请求体)→ 登机口通知(响应头)→ 广播内容(响应体)→ 离港结算(流结束)。每道关卡能做的事不同:值机口能拦人(还没进候机厅=还没转发后端),登机口才知道航班信息(后端已经响应)。插件只在自己关心的关卡"设一个安检员"(注册钩子),其它关卡直接放行。
一个 HTTP 请求过网关,插件能在这些点介入(对应 Day 05 的 8 个 Process 钩子):
① 请求头 ProcessRequestHeaders — 最常用,改/读请求头、鉴权、拦截
② 请求体 ProcessRequestBody / ProcessStreamingRequestBody — 检查/改请求体
③ 响应头 ProcessResponseHeaders — 后端响应回来了,改/读响应头
④ 响应体 ProcessResponseBody / ProcessStreamingResponseBody — 改响应体、统计
⑤ 流结束 ProcessStreamDone — 请求彻底结束,清理/上报
为什么分这么多阶段?
HTTP 是"流"——头先到、体后到、后端响应又是头先到体后到。插件在不同点能做的事不同:请求头阶段能鉴权拦截(还没转发),响应体阶段能统计 Token(后端已返回)。你按需注册钩子,只在关心的阶段介入。回想 Day 02 的 request-block 只用了①②。
图注:一个请求在插件里的"时间线"——请求方向(Decode,橙)在前、响应方向(Encode,绿)在后,最后 StreamDone 收尾。每个方框对应一个可注册的钩子。
👤 第一人称走一遍(你就是这个请求)
"我到了值机口(①请求头),安检员看了我的护照(Authorization 头),放行;我的行李被开箱检查(②请求体);我进候机厅、坐上飞机飞到后端;后端把回程票发给我,登机口播报航班号(③响应头);广播里念出通知内容(④响应体);我彻底离港,柜台做当日结算(⑤StreamDone)。" 每一关都可能有插件的安检员在等我。
L02
执行阶段枚举
iface/context.go:19-27:
type HTTPExecutionPhase int
const (
DecodeHeader HTTPExecutionPhase = iota // 请求头
DecodeData // 请求体
EncodeHeader // 响应头
EncodeData // 响应体
Done // 结束
)
Decode vs Encode 的术语
Envoy 术语里,"Decode" = 处理请求(客户端→后端方向解码),"Encode" = 处理响应(后端→客户端方向编码)。所以 DecodeHeader=请求头、EncodeHeader=响应头。你能通过
ctx.GetExecutionPhase()(context.go:99)知道当前在哪个阶段——写通用逻辑时有用。回想 Envoy/Istio 课里的 decode/encode filter,正是同一套概念。L03
Action 语义
头/体钩子返回 types.Action,告诉网关下一步:
ActionContinue:处理完了,继续往下(转发/返回下一阶段)。ActionPause:暂停,先别往下走——通常因为发起了异步外部调用(Day 14),等回调里ResumeHttpRequest()再继续。
👶 小白 vs 👨🏫 老师
👶:为什么不直接"等外部结果回来再 return",非要搞个 Pause?
👨🏫:因为 Wasm 插件不能阻塞(Day 03)。你"傻等"就会卡住整个 worker,别的请求都得排队。
👨🏫:因为 Wasm 插件不能阻塞(Day 03)。你"傻等"就会卡住整个 worker,别的请求都得排队。
ActionPause 是在说"我这单先挂起,你去忙别的,我结果到了会喊你"——就像餐厅点了道慢菜,服务员不会站你桌边等,而是先去接待别桌。ActionPause 是 Wasm 插件的精髓
因为不能阻塞(Day 03),插件要"等外部结果"时不能傻等,而是返回
ActionPause 让出控制权。网关先去处理别的请求,等你的外部调用回调触发、你调 ResumeHttpRequest(),网关再回来继续这个请求。"暂停—回调—恢复"是异步编程的核心节奏,Day 14 会完整演示。流式 body 钩子不返回 Action,而是返回处理后的 []byte(改写内容)。L04
body:整块 vs 流式
body 有两种处理方式:
- 整块
ProcessRequestBody(ctx, config, body []byte):等整个 body 到齐,一次性给你(简单,但大 body 占内存)。 - 流式
ProcessStreamingRequestBody(ctx, config, chunk []byte, isLastChunk bool) []byte:一块块给你,返回改写后的块(省内存,适合大 body / SSE 流)。
📝 举个例子:一段 20 字的 AI 回答
后端流式吐出
整块:等 5 个 chunk 全到齐 → 一次给你
流式:每到一个 chunk 就调一次
"根据财务制度,报销上限为3000元。"(分 5 个 chunk 到达)。整块:等 5 个 chunk 全到齐 → 一次给你
ProcessRequestBody(全部20字)(用户等 5 个 chunk 的时间才看到第一个字)。流式:每到一个 chunk 就调一次
ProcessStreamingResponseBody(chunk, isLastChunk),返回改写后的 chunk 立即转发(用户边生成边看到字,最后一块 isLastChunk=true 触发收尾)。流式为什么重要(AI 场景)
LLM 的响应是"流式"的(一个字一个字吐出来,SSE)。如果用整块,得等整个回答生成完才处理——延迟高、占内存。流式钩子能"边收边处理边转发",用户体验好。ai-statistics、ai-proxy 这类插件(上一站见过)大量用流式响应体处理。
isLastChunk 告诉你这是不是最后一块(可以做收尾统计)。L05
needBody 判定
NewHttpContext(plugin_wrapper.go:750-772)根据你注册了哪些钩子,决定要不要读 body:
if ctx.vm.onHttpRequestBody != nil || ctx.vm.onHttpStreamingRequestBody != nil {
httpCtx.needRequestBody = true
}
if ctx.vm.onHttpStreamingRequestBody != nil {
httpCtx.streamingRequestBody = true // 有流式钩子就用流式
}
// 响应体同理
读法:没注册 body 钩子 → 不读 body(省内存,呼应 Day 04);注册了流式钩子 → 用流式;只注册整块钩子 → 缓冲整块。SDK 根据你的"钩子登记"自动决定 body 读取策略,你无需手动开关(除非想覆盖,用 DontReadBody/BufferBody)。
L06
StreamDone
ProcessStreamDone(ctx, config):请求彻底结束(响应完全发完)时调用,无返回值。
StreamDone 干什么?
做"收尾工作":上报这次请求的统计(耗时、Token 数)、清理资源、写自定义日志。因为它在请求生命周期最末,此时所有数据都齐了。比如 ai-statistics 插件在这里汇总整个对话的 Token 用量并上报 metric。这是 Day 12 的自定义日志、Day 18 的 Token 统计常落地的钩子。
L07
常用宿主调用
在钩子里你会用到这些 proxywasm.*(宿主调用,Day 03):
proxywasm.GetHttpRequestHeader(":path") // 读单个请求头
proxywasm.GetHttpRequestHeaders() // 读所有请求头
proxywasm.AddHttpRequestHeader(k, v) // 加请求头
proxywasm.ReplaceHttpRequestHeader(k, v) // 改请求头
proxywasm.SendHttpResponseWithDetail(...) // 直接返回响应(拦截)
proxywasm.ResumeHttpRequest() // 恢复被 Pause 的请求
proxywasm.GetProperty([]string{"route_name"}) // 读 Envoy 属性
读法:请求头用
:path/:authority/:method 这些"伪头"(HTTP/2 约定,冒号开头)取。改请求头后若影响路由,记得 ctx.DisableReroute()(Day 12)——否则 Envoy 会重新算路由。L08
今日小结 + 动手
🧠 今天你应该能回答
- 请求处理的六个阶段?Decode vs Encode?
ActionContinuevsActionPause,Pause 何时用?- body 整块 vs 流式,流式为什么对 AI 重要?
- SDK 怎么根据注册的钩子决定读不读 body?
🎯 记忆口诀
"请求头体、响应头体、末了 Done;Decode 进、Encode 出;等外部就 Pause,回来了就 Resume。"
✋ 动手
cd /Users/bitmart/work/codes/github/higress-group/wasm-go
sed -n '19,27p' pkg/iface/context.go
sed -n '750,772p' pkg/wrapper/plugin_wrapper.go
grep -n "ActionContinue\|ActionPause\|ResumeHttpRequest" examples/*/main.go
明天预告 · Day 12:HttpContext 能力——精讲那一长串能力方法:UserAttribute、自定义日志/追踪、DontReadBody、DisableReroute、RouteCall 等,看请求级上下文这个"遥控器"能做什么。