DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
搬瓦工;先看线路,再选 VPS;CN2 GIA · 对应线路套餐;查看线路 →
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS

grpc-go 报 header key 包含非法字符,检查直接构造的 metadata.MD

grpc-go 1.84.0 会拒绝直接写进 metadata.MD 的大写头名。Pairs、Set 等入口会规范化名称;修正时保留已有出站元数据、取消与截止时间,不改掉认证值。

同一服务用其他客户端可以调用,Go 客户端却立即返回:

rpc error: code = Internal desc = header key "X-Tenant-Id" contains illegal characters not in [0-9a-z-_.]

如果代码直接创建 metadata.MD{"X-Tenant-Id": []string{tenantID}},应先处理这个大写键,而不是去修改服务端字段或重建证书。grpc-go 1.84.0 会在创建调用流时验证出站元数据;这条失败发生在该 RPC 元数据送往服务端之前。

MD 字面量没有自动转换名称

HTTP/2 使用小写字段名。grpc-go 的 metadata.MD 本身是映射类型,Go 的 map 字面量不会因为它叫 metadata 就额外执行规范化。NewOutgoingContext 也不负责把该映射中的大写键逐个转换后再保存。

相反,metadata.Pairs、metadata.New、MD.Set 与 MD.Append 等公开入口会把键名转成小写。已有写法有时看似相同,却一处能工作、一处失败,差别就在构造入口。

这里需要变换的是名称,不是值。租户编号、令牌或其他业务值可能区分大小写,不能对整份键值统一调用 ToLower。grpc- 开头的名称保留给内部使用;小写也不会使这些保留名称变成任意可用的业务字段。

保留已有上下文,只更新目标字段

下面是已有 Go 调用代码中的片段,ctx 是当前出站调用使用的 context,tenantID 是已完成权限校验的租户值:

md, ok := metadata.FromOutgoingContext(ctx)
if !ok {
    md = metadata.MD{}
}
md.Set("x-tenant-id", tenantID)
ctx = metadata.NewOutgoingContext(ctx, md)

需要导入 google.golang.org/grpc/metadata。FromOutgoingContext 返回出站元数据的副本,Set 将目标字段替换成一个值;其他字段仍保留。把这个 ctx 交给原来的客户端方法,继续使用原请求、取消与截止时间。

若业务字段允许多个值,应按原契约使用 Append,而不是无意用 Set 覆盖。已经有租户字段、大小写不同但含义相同的键,或多层拦截器同时写入时,还要明确由哪一层负责最终值,不能为了消除错误就把不同租户值合并。

新建独立元数据时也可以用 Pairs,但不要把仅含一个新字段的映射直接塞回已有 context,导致原来的认证、追踪等字段丢失。也不要把收到的全部入站元数据原样转发;哪些字段可以进入下游,应继续由原来的授权和转发规则决定。

NewOutgoingContext 保存之后,不应再并发修改所传映射。多个调用需要不同租户值时,应各自取得副本并派生 context,不能共享一个随后不断修改的全局 MD。

不需要关闭元数据校验

WithLowercaseMetadataKeys 并不是 1.84.0 中可用的拨号选项,也没有可以照抄启用的通用环境开关。使用公开 helper 就能在构造阶段得到合法键,不必绕过校验或降级到旧协议。

验收时检查目标服务实际收到的小写键及正确值,并覆盖已有认证字段、多值字段和拦截器叠加。还要确认原来的取消与超时继续生效。日志可记录失败的字段名和调用方法,不应为了排错输出完整认证元数据。