HttpContext 能力:请求的遥控器
昨天(Day 11)我们知道了请求分六个阶段、每个阶段有钩子。今天讲这些钩子里都能拿到的那个参数——HttpContext。Day 04 见过它那一长串接口,今天逐个精讲。它不只是"存数据",更是"操作当前请求的遥控器"——读信息、存中间状态、写日志、控制路由和 body。为明天(Day 13)的外部调用打基础。
请求信息
Path()/Host())、一面是便签夹(存中间数据:SetContext)、还有几个功能键(DisableReroute() 关路由重算、DontReadBody() 关读体)。每个请求发一只自己的遥控器,互不干扰(这正是不能用全局变量的原因)。iface/context.go:42-45:Scheme()/Host()/Path()/Method()。这些值在请求头阶段被缓存到 CommonHttpCtx 的 scheme/host/path/method 字段(plugin_wrapper.go:816-819)。
ctx.Path() 取到——随时可用。类似的还缓存了 Connection/Upgrade/ContentType 等头(:821-828)用于 IsWebsocket()/IsBinaryRequestBody() 判断。请求级数据存取
SetContext/GetContext + 类型化的 GetBoolContext/GetStringContext/GetByteSliceContext(带默认值)。实现在 plugin_wrapper.go:834-840,底层是 CommonHttpCtx.userContext map。
// 请求头阶段解析出用户名
ctx.SetContext("user", username)
// 请求体阶段取出用(同一请求内有效)
user := ctx.GetStringContext("user", "anonymous")
Authorization: Bearer abc → 得出 user="张三" → ctx.SetContext("user","张三")。响应体阶段(同一请求):
ctx.GetStringContext("user","anonymous") → 得到 "张三"。若换成
var user string 全局变量:并发时请求 B 把它覆盖成 "李四",请求 A 的响应阶段读出来就变成了 "李四"(❌ 数据串了)。GetStringContext(key, 默认值))省去手动类型断言和判空,很贴心。这是写插件时最高频的 API 之一。UserAttribute
SetUserAttribute/GetUserAttribute/SetUserAttributeMap/GetUserAttributeMap(plugin_wrapper.go:842-860):另一套 map,专门用于"要写进日志/追踪的属性"。
WriteUserAttributeToLog 统一序列化进访问日志。典型:鉴权插件把用户名放 UserAttribute,日志里就能看到每条请求是谁发的。SetContext 存"要进日志的属性",结果日志里啥也没有;或用 SetUserAttribute 存"只给自己代码用的中间值",白白进了日志。记准:给自己代码读的用 Context,要写进日志/追踪的用 UserAttribute。自定义日志 / 追踪
WriteUserAttributeToLog()(plugin_wrapper.go:862-869)把 UserAttribute 序列化成 JSON 写进访问日志的 custom_log 字段(CustomLogKey);WriteUserAttributeToLogWithKey(key) 用自定义 key;WriteUserAttributeToTrace() 写进链路追踪 span 标签(TraceSpanTagPrefix)。
custom_log 属性、合并你的 UserAttribute、写回——多个插件能往同一条访问日志追加各自的属性。这样一条请求的访问日志里能看到鉴权插件写的用户、限流插件写的配额、AI 插件写的 Token——全链路可观测。回想上一站 Console 的 Dashboard,数据源头就在这里。DisableReroute
iface/context.go:90-92:DisableReroute()——若在请求头阶段改了请求头,Envoy 默认会重新计算路由;调这个禁止重新路由。必须在改头操作之前调。
/api/foo 的请求加个内部标记头,于是 ReplaceHttpRequestHeader(":path", "/api/foo?tag=1")。Envoy 一看 path 变了 → 重新匹配路由 → 结果 ?tag=1 让它匹配到了另一条路由规则 → 请求被发去了错误的后端,报 404。加一行 ctx.DisableReroute()(在改头之前)就说:"我是故意改的,别重算,还走原来那条路由",事故消除。DisableReroute() 告诉 Envoy"我改头是有意的,别重算路由,还用原来那条"。这是改请求头时的一个大坑,SDK 用这个方法让你显式控制。body 控制回顾
Day 04/11 提过的 body 控制方法(context.go:61-96)汇总:
DontReadRequestBody()/DontReadResponseBody():不读。BufferRequestBody()/BufferResponseBody():缓冲整块。SetRequestBodyBufferLimit(byteSize):设单请求缓冲上限。NeedPauseStreamingResponse()+PushBuffer/PopBuffer/BufferQueueSize:暂停流式响应、用内部缓冲队列(配合异步 HTTP 调用改写流式响应,Day 19)。
PushBuffer/PopBuffer 那套是给"流式响应期间要调外部服务再改写"的高级场景用的(context.go:69-89 有详细注释)——先 NeedPauseStreamingResponse,异步调用完再 InjectEncodedDataToFilterChain 注入。Day 19 细讲。RouteCall 与各种判断
RouteCall(method, url, headers, body, callback)(context.go:98):向当前路由的后端发起调用(区别于 Day 14 的 HttpCall 指定任意集群)。还有一批判断方法:
HasRequestBody() / HasResponseBody() // 有没有 body(看头阶段的 endOfStream)
IsWebsocket() // 是不是 WebSocket 升级请求
IsBinaryRequestBody() / IsBinaryResponseBody() // body 是不是二进制
GetExecutionPhase() // 当前在哪个阶段(Day 11)
context.go:109-117 注释说"随时可调"),让你写出健壮的插件。比如 AI 插件只处理 JSON body,遇到二进制就跳过。今日小结 + 动手
🧠 今天你应该能回答
- 为什么请求信息要在头阶段缓存?
- Context 和 UserAttribute 的区别?后者干什么用?
- 自定义日志怎么让多插件往同一条日志追加属性?
- DisableReroute 解决什么坑?何时必须调?
✋ 动手
cd /Users/bitmart/work/codes/github/higress-group/wasm-go
sed -n '811,870p' pkg/wrapper/plugin_wrapper.go
sed -n '41,118p' pkg/iface/context.go
grep -n "WriteUserAttributeToLog\|DisableReroute" pkg/wrapper/plugin_wrapper.go