Day 12 / 共 20 天 · 第 3 周 HTTP 与外部调用

HttpContext 能力:请求的遥控器

昨天(Day 11)我们知道了请求分六个阶段、每个阶段有钩子。今天讲这些钩子里都能拿到的那个参数——HttpContext。Day 04 见过它那一长串接口,今天逐个精讲。它不只是"存数据",更是"操作当前请求的遥控器"——读信息、存中间状态、写日志、控制路由和 body。为明天(Day 13)的外部调用打基础。

📍 你在整门课的位置(wasm-go 20 天 · 第 3 周 HTTP 与外部调用)
六阶段钩子 D11 HttpContext 遥控器 D12 Cluster D13 HttpCall D14 Redis D15
L01

请求信息

💡 本质:HttpContext = 空调遥控器 你不用懂空调内部的压缩机怎么工作,拿着遥控器按几个键就能调温度、切模式。HttpContext 就是"当前这个请求"的遥控器:一面是显示屏(读信息:Path()/Host())、一面是便签夹(存中间数据:SetContext)、还有几个功能键DisableReroute() 关路由重算、DontReadBody() 关读体)。每个请求发一只自己的遥控器,互不干扰(这正是不能用全局变量的原因)。
HttpContext 遥控器 屏:Path/Host/Method SetContext GetContext DisableReroute DontReadBody UserAttribute→写访问日志 请求 A拿自己的遥控器 请求 B、C… 各自 一只遥控器,不串数据
图注:HttpContext 是每个请求专属的"遥控器"——读信息、存便签、按功能键、写日志,四类能力都在上面。

iface/context.go:42-45Scheme()/Host()/Path()/Method()。这些值在请求头阶段被缓存到 CommonHttpCtxscheme/host/path/method 字段(plugin_wrapper.go:816-819)。

读法:为什么缓存?请求头处理完后,某些底层值可能不再能直接读(比如响应阶段)。SDK 在头阶段就缓存好,后续任何阶段都能 ctx.Path() 取到——随时可用。类似的还缓存了 Connection/Upgrade/ContentType 等头(:821-828)用于 IsWebsocket()/IsBinaryRequestBody() 判断。
L02

请求级数据存取

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 的响应阶段读出来就变成了 "李四"(❌ 数据串了)。
阶段间传数据的正确方式(重申) Day 04 强调过:跨阶段传数据一定用请求级 Context,绝不用全局变量(并发请求会串数据)。类型化的 getter(GetStringContext(key, 默认值))省去手动类型断言和判空,很贴心。这是写插件时最高频的 API 之一。
L03

UserAttribute

SetUserAttribute/GetUserAttribute/SetUserAttributeMap/GetUserAttributeMapplugin_wrapper.go:842-860):另一套 map,专门用于"要写进日志/追踪的属性"。

UserAttribute 和 Context 有啥区别? 普通 Context 是"给你自己代码用的中间数据";UserAttribute 是"要输出到日志/链路追踪的业务属性"(比如识别出的用户 ID、命中的策略、Token 数)。分开两套是因为用途不同——UserAttribute 里的东西会被 WriteUserAttributeToLog 统一序列化进访问日志。典型:鉴权插件把用户名放 UserAttribute,日志里就能看到每条请求是谁发的。
⚠️ 常见误解:小白常把两者混用——用 SetContext 存"要进日志的属性",结果日志里啥也没有;或用 SetUserAttribute 存"只给自己代码用的中间值",白白进了日志。记准:给自己代码读的用 Context,要写进日志/追踪的用 UserAttribute。
L04

自定义日志 / 追踪

WriteUserAttributeToLog()plugin_wrapper.go:862-869)把 UserAttribute 序列化成 JSON 写进访问日志的 custom_log 字段(CustomLogKey);WriteUserAttributeToLogWithKey(key) 用自定义 key;WriteUserAttributeToTrace() 写进链路追踪 span 标签(TraceSpanTagPrefix)。

读法:它读取现有的 custom_log 属性、合并你的 UserAttribute、写回——多个插件能往同一条访问日志追加各自的属性。这样一条请求的访问日志里能看到鉴权插件写的用户、限流插件写的配额、AI 插件写的 Token——全链路可观测。回想上一站 Console 的 Dashboard,数据源头就在这里。
L05

DisableReroute

iface/context.go:90-92DisableReroute()——若在请求头阶段改了请求头,Envoy 默认会重新计算路由;调这个禁止重新路由。必须在改头操作之前调。

🚨 错误驱动:不调 DisableReroute 会出什么事故? 你的插件想给去 /api/foo 的请求加个内部标记头,于是 ReplaceHttpRequestHeader(":path", "/api/foo?tag=1")Envoy 一看 path 变了 → 重新匹配路由 → 结果 ?tag=1 让它匹配到了另一条路由规则 → 请求被发去了错误的后端,报 404。加一行 ctx.DisableReroute()(在改头之前)就说:"我是故意改的,别重算,还走原来那条路由",事故消除。
为什么会"重新路由"?会有什么问题? Envoy 的路由是基于请求头(Host/Path 等)算的。如果你的插件改了这些头,Envoy 觉得"路由条件变了",会重新匹配路由——可能匹配到别的地方,出乎意料。DisableReroute() 告诉 Envoy"我改头是有意的,别重算路由,还用原来那条"。这是改请求头时的一个大坑,SDK 用这个方法让你显式控制。
L06

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 细讲。
L07

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)
这些判断有什么用? 处理 body 前先判断很重要:是二进制(图片/protobuf)就别当文本处理、是 WebSocket 就别按普通 HTTP 处理、没 body 就别等 body。这些方法用头阶段缓存的值(context.go:109-117 注释说"随时可调"),让你写出健壮的插件。比如 AI 插件只处理 JSON body,遇到二进制就跳过。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 为什么请求信息要在头阶段缓存?
  • 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
明天预告 · Day 13Cluster 集群抽象——外部调用要指定"调哪个后端",wasm-go 用 8 种 Cluster 类型(Route/K8s/Nacos/FQDN/Static/Dns/Consul)表达不同来源的后端,看它们怎么拼出 Envoy 集群名。
← Day 11 Day 13 · Cluster →