kratos中跨http/grpc服务边界的请求上下文透传必须通过metadata包的fromservercontext/ newclientcontext等标准接口操作元数据,禁止手动使用context.withvalue或直接操作header/ grpc.md,以确保trace_id、tenant_id等关键字段在调用链中完整传递且被框架正确识别。

在Kratos微服务中实现跨HTTP/gRPC服务边界的请求上下文透传,必须依赖元数据(Metadata)机制完成键值对的携带与提取,否则trace_id、tenant_id等关键字段会在调用链中丢失。
服务端接收并解析元数据
服务端需从入站请求上下文中提取Metadata,而非直接读ctx.Value()——因为transport.Context做了封装,原生context.Value无法命中。
第一步:在gRPC或HTTP中间件中调用FromServerContext获取元数据实例:
md, ok := metadata.FromServerContext(ctx)
if !ok {
return nil, errors.BadRequest("metadata", "missing")
}
第二步:使用Get或Values方法读取指定key的值。注意Get只返回第一个值,Values返回全部字符串切片:
traceID := md.Get("X-Trace-ID") // 若Header中存在多个同名key,仅取首个
tenantIDs := md.Values("X-Tenant-ID") // 获取所有租户ID,可用于多租户路由
【必须用FromServerContext,不能用context.WithValue手动塞】 transport层已将Metadata绑定到特定context key,手动塞入的值无法被框架识别,下游中间件和业务逻辑将读不到。
客户端发起带元数据的调用
方法一:通过NewClientContext显式注入元数据
先构造Metadata实例,再包装原始context:
md := metadata.Metadata{"X-Trace-ID": []string{"abc123"}, "X-User-ID": []string{"u456"}}
ctx = metadata.NewClientContext(ctx, md)
方法二:在transport.ClientOption中统一设置默认元数据(适合全局透传字段)
client := http.NewClient(
http.WithMiddleware(middleware.Recovery()),
http.WithClientOptions(transport.WithMetadata(func(ctx context.Context) metadata.Metadata {
return metadata.Metadata{"X-Env": []string{"prod"}}
}))
)
方法三:在单次调用时动态追加(如鉴权token更新后重试)
md := metadata.FromClientContext(ctx)
if md == nil {
md = metadata.Metadata{}
}
md.Set("Authorization", "Bearer new-token-789")
ctx = metadata.NewClientContext(ctx, md)
跨协议保持元数据一致性
HTTP Header与gRPC Metadata自动双向映射,但字段名需遵循规范:
HTTP请求头中以X-开头的字段(如X-Trace-ID)会自动转为gRPC Metadata中的小写键(x-trace-id);反之,gRPC端写入的x-user-id也会映射为HTTP响应头X-User-ID。
不推荐手动操作Header或grpc.MD,必须走metadata包提供的Set/Add接口,否则无法参与框架级透传流程。
【禁止在HTTP handler里直接写w.Header().Set("X-Trace-ID", ...)】 这样写仅影响当前响应头,不会进入下游gRPC调用的Metadata,也无法被FromServerContext提取。
中间件中安全传递元数据
① 在自定义中间件开头提取元数据,并验证必要字段是否存在:
md, ok := metadata.FromServerContext(ctx)
if !ok || len(md.Get("X-Trace-ID")) == 0 {
return errors.Unauthorized("auth", "missing trace context")
}
② 将清洗后的元数据重新注入新context,供后续handler使用:
cleanMD := metadata.Metadata{}
for _, k := range []string{"X-Trace-ID", "X-Tenant-ID", "X-Region"} {
if v := md.Get(k); v != "" {
cleanMD.Set(k, v)
}
}
ctx = metadata.NewServerContext(ctx, cleanMD)
③ 调用next handler:
return handler(ctx, req)
这一步操作起来很简单,直接把cleanMD传进去就行。但若跳过②直接用原始ctx,下游业务层拿到的md仍是未清洗的全量数据,可能含敏感字段或非法值。











