bigywater.tech
gRPC · 7

metadata:头和 trailer 里能放什么,大小写与二进制值

metadata 就是 gRPC 的自定义请求头和响应头。键大小写怎么处理?中文能不能放?库占用的头能不能改?头多大会被拒?服务端什么时候能设响应头?这篇一项项抓包验证。

x-name-bin: 5byg5LiJ
中文值放进 -bin 键时线上的样子(无填充 base64);放进普通键则调用本地失败
16 MiB
请求头总大小的分界:16000 KiB 成功,16400 KiB 起连接被断开
SendHeader called multiple times
先 Send 了消息再 SetHeader 时服务端得到的错误

键

实测 · 本机执行输出
  err=<nil>
  线上自定义头:[content-type: application/grpc user-agent: grpc-go/1.84.0 grpc-timeout: 2999968u authorization: Bearer tkn x-request-id: abc-123]
  服务端 metadata.FromIncomingContext 的键:x-request-id=[abc-123] authorization=[Bearer tkn]

键统一小写,允许的字符是 [0-9a-z-_.]。大写的键(如 X-Request-ID)会被转成小写后发出。

值

实测 · 本机执行输出
  普通键 + 中文值              → 客户端 code=Internal  线上:[]
      message="header key \"x-name\" contains value with non-printable ASCII characters"
  普通键 + 含换行的值            → 客户端 code=Internal  线上:[]
      message="header key \"x-name\" contains value with non-printable ASCII characters"
  -bin 键 + 中文值           → 客户端 code=OK        线上:[content-type: application/grpc user-agent: grpc-go/1.84.0 grpc-timeout: 2999960u x-name-bin: 5byg5LiJ]
  -bin 键 + 随机字节          → 客户端 code=OK        线上:[content-type: application/grpc user-agent: grpc-go/1.84.0 grpc-timeout: 2999969u x-raw-bin: AP8QgA]
  键里有空格                  → 客户端 code=Internal  线上:[]
      message="header key \"bad key\" contains illegal characters not in [0-9a-z-_.]"
  键里有大写和下划线              → 客户端 code=OK        线上:[content-type: application/grpc user-agent: grpc-go/1.84.0 grpc-timeout: 2999982u x_under_score: v]
  值为空字符串                 → 客户端 code=OK        线上:[content-type: application/grpc user-agent: grpc-go/1.84.0 grpc-timeout: 2999976u x-empty: ]

普通键的值只能是可打印 ASCII,放中文或换行会让调用在客户端本地失败(Internal)。-bin 结尾的键可以放任意字节,线上用无填充的 base64。

保留头

实测 · 本机执行输出
  设置 user-agent    → code=OK 线上相关头:[content-type: application/grpc user-agent: grpc-go/1.84.0 grpc-timeout: 2999978u]
  设置 content-type  → code=OK 线上相关头:[content-type: application/grpc user-agent: grpc-go/1.84.0 grpc-timeout: 2999987u]
  设置 grpc-timeout  → code=OK 线上相关头:[content-type: application/grpc user-agent: grpc-go/1.84.0 grpc-timeout: 2999978u]
  设置 grpc-encoding → code=OK 线上相关头:[content-type: application/grpc user-agent: grpc-go/1.84.0 grpc-timeout: 2999987u]

user-agent、content-type、grpc-timeout、grpc-encoding 等库占用的头不能用 metadata 修改,设置了也被静默丢弃。要改 UA,grpc-go 有 grpc.WithUserAgent 选项(没有验证)。

多值

实测 · 本机执行输出
  线上:[content-type: application/grpc user-agent: grpc-go/1.84.0 grpc-timeout: 2999982u x-tag: a x-tag: b]
  服务端看到 x-tag = ["a" "b"](2 个值)

同一个键写多次,线上是多行头,服务端 Get 返回多个值。

大小

实测 · 本机执行输出
       1 KiB 的头 → code=OK                
       8 KiB 的头 → code=OK                
      64 KiB 的头 → code=OK                
     512 KiB 的头 → code=OK                
    4096 KiB 的头 → code=OK                
    8192 KiB 的头 → code=OK                
   15000 KiB 的头 → code=OK                
   16000 KiB 的头 → code=OK                
   16400 KiB 的头 → code=Unavailable       error reading from server: EOF
   20000 KiB 的头 → code=Unavailable       error reading from server: read tcp 127.0.0.1:54432->127.0.0.1:9801: read: connection rese…

服务端默认接受约 16 MiB 的请求头;更大时连接被断开,客户端看到 Unavailable。路径上有代理时,代理的头限制通常小得多。

响应头与 trailers

实测 · 本机执行输出
  Unary:客户端 grpc.Header 得到 {content-type=["application/grpc"] x-mixed-case=["Value"] x-server=["node-3"]};grpc.Trailer 得到 {x-late=["t"]}
     线上响应头 [content-type: application/grpc x-mixed-case: Value x-server: node-3]
     线上 trailers [grpc-status: 0 grpc-message:  x-late: t]
  流式 early:stream.Header() 在 0.3 ms 返回,得到 {content-type=["application/grpc"] x-early=["1"]};线上响应头 [content-type: application/grpc x-early: 1]
  流式 late :stream.Header() 在 0.3 ms 返回,得到 {content-type=["application/grpc"]};线上响应头 [content-type: application/grpc]
  服务端 SendHeader 错误:"rpc error: code = Internal desc = transport: SendHeader called multiple times"
  服务端 SetHeader 错误:"rpc error: code = Internal desc = transport: SendHeader called multiple times"

Unary 里 SetHeader 与第一条响应一起发;流式里可以用 SendHeader 提前发。第一条消息发出后再设置响应头会失败。需要响应结束后才知道的信息,放 trailers(第 4 篇)。

分步演示

metadata
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1 / grpc-go v1.84.0 / protobuf-go v1.36.12 / golang.org/x/net v0.57.0 / protoc 36.2(Homebrew);服务端和客户端都是本机起的 Go 进程(127.0.0.1,端口 9800 以上,不访问任何外部服务);需要看线上字节时用我自己写的 TCP 抓包代理加 golang.org/x/net/http2 的 Framer 解析 HTTP/2 帧;计时用 time.Now 或 perf_counter,数字每次运行会有小幅抖动。线上头来自抓包代理解析的 HPACK;第五节只测了列出的几个大小,分界在 16000 与 16400 KiB 之间,没有进一步二分;服务端用默认 grpc.NewServer()
    • 没有验证:grpc-go 其它版本对保留头的处理;WithUserAgent 的实际线上值;服务端 MaxHeaderListSize 选项改小后的错误形式;客户端 MaxHeaderListSize 对响应头的限制;非 grpc-go 实现对 -bin 键与大小写的处理;超大头时 nginx 等中间层的行为
    京ICP备2026067502号-1京公网安备11010502063487号