metadata 就是 gRPC 的自定义请求头和响应头。键大小写怎么处理?中文能不能放?库占用的头能不能改?头多大会被拒?服务端什么时候能设响应头?这篇一项项抓包验证。
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。路径上有代理时,代理的头限制通常小得多。
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 篇)。