bigywater.tech
gRPC · 30

gRPC-Web:浏览器为什么需要代理

浏览器为什么不能直接调 gRPC?grpc-web 到底改了什么?这篇用本机的 Chrome 和一个真实的 grpc-web Go 包装,把响应的每一个字节都打印出来。

读不到 grpc-status
浏览器 fetch 调原生 gRPC 时,成功和失败的调用 JS 都读不到状态
标志位 128
grpc-web 响应体里 trailers 帧的第一个字节(0x80)
3、205、407、607 ms
grpc-web 服务端流每一帧到达浏览器的时刻

问题在 trailers

实测 · 本机执行输出
  成功的调用(msg=hi):HTTP 200、协议 h2;响应头可见:content-type,trailer;grpc-status=None、grpc-message=None;响应体 14 字节 00000000090a076563686f3a6869
  被服务端拒绝的调用(msg=deny,服务端返回 Unauthenticated):HTTP 200、协议 h2;响应头可见:content-type,trailer;grpc-status=None、grpc-message=None;响应体 0 字节 

gRPC 把状态放在 HTTP/2 的 trailers 里,而浏览器的 fetch/XHR 不让 JavaScript 读 trailers。调原生 gRPC 能读到数据,读不到成功还是失败。

CORS

实测 · 本机执行输出
  fetch 的结果:TypeError: Failed to fetch;浏览器实际发出的请求方法:['POST', 'OPTIONS']

content-type 为 application/grpc 的跨域请求会触发预检,gRPC 服务端不会回应预检。需要中间层处理 CORS。

grpc-web 的做法

实测 · 本机执行输出
  成功的调用:HTTP 200、content-type=application/grpc-web+proto、响应头 grpc-status=None;响应体 35 字节,共 2 帧
      数据帧(标志位 0,长度 9):'0a076563686f3a6869'
      trailers 帧(标志位 128,长度 16):'grpc-status: 0\r\n'
  被拒绝的调用:HTTP 200、content-type=application/grpc-web+proto、响应头 grpc-status=16;响应体 0 字节,共 0 帧

用 application/grpc-web+proto,响应体里除数据帧外,多一帧标志位为 0x80 的 trailers 帧,内容是 HTTP 头文本(如 grpc-status: 0)。JS 从响应体里就能读到状态。

流式

实测 · 本机执行输出
     3 ms:标志位 0  '0a026d30'
   205 ms:标志位 0  '0a026d311001'
   407 ms:标志位 0  '0a026d321002'
   607 ms:标志位 128  'grpc-status: 0'

服务端流可以用 fetch 的 ReadableStream 边收边读。客户端流在浏览器里受限于请求体不能边发边写。

要一个代理

实测 · 本机执行输出
  HTTP 200;响应帧:[(0, '1002'), (128, 'grpc-status: 0\r\n')]

grpc-web 不是原生 gRPC,需要一个能翻译的中间层(Envoy 的 grpc_web 过滤器、improbable-eng 的 Go 包装等)放在 gRPC 服务前面。

分步演示

gRPC-Web
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。浏览器是本机的 Google Chrome 154(headless,独立 user-data-dir,--ignore-certificate-errors 只用于这个测试的自签名证书),用 CDP 驱动,代码在 pcdp.py;原生 gRPC 这一节是把 grpc.Server 作为 http.Handler 挂在 net/http 的 HTTPS 服务器上(这样页面和 gRPC 同源),grpc-web 用 improbable-eng/grpc-web v0.15.0(一个较旧但常用的 Go 包装,为此 go.mod 里新增了它的几个依赖);页面服务器、两个 gRPC 服务都在 127.0.0.1
    • 没有验证:grpc.Server 自带的 HTTP/2 传输(而不是 ServeHTTP)在错误时返回 Trailers-Only 的响应头,浏览器能不能从头里读到状态,这里没有测(同源页面的条件没有构造);Envoy 的 grpc_web 过滤器;grpc-web-text(base64)格式;官方 grpc-web JavaScript 客户端库和 connect-web 等;浏览器 fetch 的流式请求体(duplex: half)能不能用于真正的客户端流;WebSocket 传输;Safari、Firefox 的行为
    京ICP备2026067502号-1京公网安备11010502063487号