gRPC-Web 协议详解:gRPC 浏览器传输协议与原生 gRPC over HTTP/2 的差异分析
本文基于 gRPC 仓库中的 gRPC-Web 协议文档,完整解读 gRPC-Web 传输协议的设计目标、与原生 gRPC over HTTP/2 协议 的全部差异点,包括 Content-Type 协商、消息分帧、trailers 内嵌于响应体的编码方式、文本流编码规则,以及重试、缓存、keep-alive 等特性的协议现状。读完本文,你将能够准确理解 gRPC-Web 请求/响应在浏览器与代理(如 Envoy、nginx 网关)之间的线上格式,并能为 gRPC-Web 的代理层实现或调试工作提供协议依据。
协议定位:为浏览器而生的“delta”协议
gRPC-Web 提供 JS 客户端库,支持与 gRPC-Node 相同的 API 来访问 gRPC 服务。但由于浏览器的限制,Web 客户端库实现的并不是原生的 gRPC 协议(即 PROTOCOL-HTTP2.md 所定义的协议),而是一个不同的协议。该协议在设计上刻意降低了实现复杂度,使得一个代理(proxy)能够方便地在两种协议之间做转换——这正是 gRPC-Web 最可能的部署形态:浏览器通过 HTTP 与 gRPC-Web 代理对话,代理再将请求翻译为原生 gRPC(HTTP/2)发给后端服务。
需要特别注意的是原文档的表述方式:本文档描述的是一种“delta”(增量差异),即 gRPC-Web 协议相对原生 gRPC over HTTP/2 协议细节的差异清单,而非一份自包含的完整协议规范。因此阅读 gRPC-Web 协议时,必须同时对照 gRPC over HTTP/2 协议文档,两者结合才能构成完整的协议视图。
设计目标
根据 PROTOCOL-WEB.md 的 Design goals 一节,gRPC-Web 协议确定了以下三条设计目标:
- 尽可能沿用 "application/grpc" 的分帧方式(framing):保证与原生 gRPC 的最大兼容性,降低代理转译的实现成本;
- 与 HTTP/2 分帧解耦:因为浏览器现在、也(在协议制定时)永远不会直接暴露 HTTP/2 分帧给 JavaScript 层;
- 支持文本流(如 base64):以提供跨浏览器支持(文档中明确提到的目标是覆盖 IE-10 这类不直接支持二进制流的浏览器环境)。
原文档还给出了该协议的两个长期演进预期:
- gRPC-Web 协议会持续演化,主要为浏览器客户端优化,或支持 Web 特有特性(如 CORS、XSRF);
- 当浏览器能够通过新的 whatwg streams API 直接说原生 gRPC 协议后,这个协议在 1-2 年内会变得可选(become optional)。原文档同时强调:该协议会公开发布/评审,但 gRPC-Web 方面也意图将其保留为 gRPC-Web 的内部细节(internal detail),即它不是要求所有实现都必须永久遵守的强约束。
与 gRPC over HTTP/2 的协议差异
下面按 PROTOCOL-WEB.md 的章节顺序,完整列出并逐条讲解各差异点。
Content-Type 差异
原生 gRPC 使用 application/grpc(可选 +proto / +json 后缀);gRPC-Web 则使用两种 Content-Type:
application/grpc-web- 例如
application/grpc-web+proto、application/grpc-web+json、application/grpc-web+thrift; - 发送方应始终指明消息格式(
+proto、+json等后缀); - 接收方在 Content-Type 为裸的
application/grpc-web(缺失消息格式后缀)时,应假定默认为+proto。
- 例如
application/grpc-web-text- 语义为 "
application/grpc-web的文本编码流",例如application/grpc-web-text+proto、application/grpc-web-text+thrift。
- 语义为 "
这一差异的意义在于:代理层只需通过 Content-Type 前缀即可识别 gRPC-Web 流量,而 text 变体让代理知道响应体需要做 base64 文本化后再发往浏览器。
HTTP 线上协议(wire protocol)差异
- 支持任意 HTTP/*:不依赖任何 HTTP/2 特有的分帧机制。也就是说 gRPC-Web 请求可以跑在 HTTP/1.1 之上,这直接决定了它能在不暴露 HTTP/2 的浏览器环境中工作;
- 头部大小写规则:在 HTTP/1.1 上传输时,头部名可以为大写或混合大小写;但编码在最后一个长度前缀消息(length-prefixed message)里的 trailers 必须始终使用小写名称。这是因为 trailers 在 gRPC-Web 中是"伪装成 HTTP/1 头块"嵌在 body 里传输的(见下文分帧一节),接收端按小写 HTTP 头解析;
- 使用 EOF(请求体/响应体的结束)来关闭流,替代原生 gRPC 中依赖 HTTP/2 END_STREAM 帧标志位的做法。
HTTP/2 相关行为的缺失
相对 PROTOCOL-HTTP2.md 中规定的 HTTP/2 行为,gRPC-Web 明确不支持也不使用:
- stream-id:不存在多路复用的流标识。这意味着单条 HTTP 连接上的请求/响应处理退化为 HTTP/1 语义,多路复用交给浏览器与代理层(如 HTTP/2 代理)负责;
- GOAWAY 帧:不使用。优雅关闭依赖 HTTP 层自身的连接管理。
消息分帧:状态与 trailers 移入响应体
这是 gRPC-Web 相对原生协议最核心的结构性变化。在原生 gRPC over HTTP/2 中,响应由 Response-Headers、若干 Length-Prefixed-Message 和独立的 Trailers 帧组成(参见 PROTOCOL-HTTP2.md 的 Response 一节);而 gRPC-Web 中:
-
响应状态编码进响应体:gRPC 状态(grpc-status 等)不再作为独立的 trailers 帧,而是以"一个 HTTP/1 头部块(不带终止空行)的键值对形式"编码在响应 body 中,头块格式遵循 RFC 7230 Section 3.2,例如:
key1: foo\r\n key2: bar\r\n代理转译时,等价于把原生协议的 Trailers 帧内容序列化成这段"文本头块"塞进 body;
-
首帧标志位的复用:gRPC 长度前缀消息的第一字节最高位(MSB)原本表示压缩标志(compressed flag,见 PROTOCOL-HTTP2.md 的 Length-Prefixed-Message 定义),gRPC-Web 将其重新解释为:
- 0:data(数据消息);
- 1:trailers(trailer 消息,作为 body 的一部分发送)。
文档给出的位图示例:
10000000b: an uncompressed trailer (as part of the body) 10000001b: a compressed trailer即
10000000b(0x80)表示一个未压缩的 trailer,10000001b(0x81)表示压缩的 trailer; -
trailers 必须是响应的最后一条消息,由实现强制保证(enforced by the implementation);
-
Trailers-only 响应:相对 gRPC 协议规范无变化。trailers 可以与响应头一起发送,body 中没有任何消息——例如调用在发送数据前就立即出错时,代理直接把带 gRPC 状态的头块作为 trailers-only 响应返回。
这条设计目标第 1 条("尽可能沿用 application/grpc 分帧")在这里体现得很彻底:消息体的 5 字节头(1 字节标志 + 4 字节大端长度)保持不变,只是把标志位的语义从"压缩"扩展为"trailers/压缩 trailers",并借 body 尾部的文本头块替代了 HTTP/2 的 trailers 帧。
User-Agent 头
- 不要使用
User-Agent头——该头由浏览器默认设置,客户端库不应自行设置; - 改用
X-User-Agent: grpc-web-javascript/0.1,格式与 gRPC over HTTP/2 协议 中规定的 user-agent 结构化字符串格式保持一致。
这个约定让服务端可以从头字段识别出请求来自 gRPC-Web 客户端而非原生客户端,从而在日志、监控和特性协商上做出区分。
文本编码流(base64)
为覆盖不支持二进制流的浏览器/接口(例如 XHR 受安全策略限制只能处理文本),协议定义了文本编码的响应流:
-
客户端通过
Accept头声明需要文本编码,例如在使用 XHR 或受 XHR 安全策略限制时:Accept: application/grpc-web-text -
默认文本编码是 base64。文档对此有两点重要的实现约束:
- 不应使用
Content-Transfer-Encoding: base64头。原因是 gRPC 消息以帧为单位做 base64 时,每个消息块自身会带 base64 填充(padding),导致整个响应体不一定构成一个合法的单一 base64 实体——即不能把 body 当成一整段 base64 解码; - 虽然服务端运行时总是按消息原子地(atomically)做 base64 编码并刷新(flush),但客户端库不能假设 base64 填充一定发生在消息帧边界处。实现可能在运行时每次需要刷新字节缓冲时,就发送一段带有潜在填充的 base64 "chunks"。客户端解码器必须逐 chunk 处理并拼接,而不是等到 body 结束才整体解码。
- 不应使用
这段描述是 gRPC-Web 客户端与代理实现中最容易踩坑的部分:base64 流是"按 flush 分片、片内可能带 padding"的流式编码,而非整体编码。
其他特性的协议现状
PROTOCOL-WEB.md 的 Other features 一节还给出了一批特性的状态说明,逐条整理如下:
- 重试与缓存(Retries, caching):待 gRPC 规范中对应扩展定型后再制定 gRPC-Web 的细则。方向上:安全重试用 PUT(Safe retries: PUT);缓存考虑对头部编码请求的支持,或一个 Web 专属规范。
- Keep-alive:不支持也不使用 HTTP/2 PING;也不支持 send-beacon(GET 方式的心跳)。长连接保活依赖代理与浏览器自身的 HTTP 机制。
- 双向流(Bidi-streaming,带流控):当时取决于 whatwg fetch/streams 规范定型并在现代浏览器落地;文档给出的方向是——一旦现代浏览器支持,gRPC-Web 客户端将直接支持原生 gRPC 协议,而不再依赖该 delta 协议做双向流。
- 版本化(Versioning):未来可能引入特殊头来支持会破坏兼容性的新特性,即通过协议头进行特性/版本协商。
- 浏览器专属特性:对浏览器或 HTML 客户端独有的特性,原文档指向 grpc-web 仓库(grpc/grpc-web)中发布的浏览器特性规范文档(browser-features.md),该文档不在本仓库内,这里仅作为协议文档的指引记录。
在 gRPC 仓库中的佐证与配套材料
从本仓库的结构看,gRPC-Web 协议本身不落在 gRPC 核心库(C++/Python 等运行时)的实现范围内——application/grpc-web 这一 Content-Type 在整个仓库中仅出现于 doc/PROTOCOL-WEB.md 本身,这与原文档"协议保留为 gRPC-Web 内部细节"的定位一致:核心仓库负责原生 gRPC 协议的规范与实现,gRPC-Web 客户端与代理转译由独立的 grpc-web 仓库承担。
仓库内可以对照参考的材料包括:
- examples/php/echo/README.md:端到端 PHP 示例。其中"Run the Server"一节给出了通过 grpc-web 仓库构建
grpcweb/node-server镜像并暴露 9090 端口的步骤,可作为观察 gRPC-Web 后端服务形态的实操入口(该示例中的 gRPC 服务通过 grpc-web 提供的 Node 代理对外提供); - examples/node/README.md:说明 Node 相关示例已迁移至 grpc/grpc-node 仓库——gRPC-Web 的 JS 客户端库同样属于 grpc-node / grpc-web 生态而非本核心仓库;
- doc/PROTOCOL-HTTP2.md:gRPC-Web 所有 delta 描述的基线协议,其中
Length-Prefixed-Message、Compressed-Flag、Trailers等 ABNF 定义是理解 gRPC-Web 分帧差异的前置知识,建议配合阅读。
小结
gRPC-Web 协议是一份以"delta"方式定义的传输规范,其核心思想可归纳为三点:
- 分帧尽量不变——沿用 application/grpc 的长度前缀消息格式,仅把首字节标志位的含义扩展为 data/trailers,最大限度降低代理转译成本;
- HTTP 层全面降级兼容——支持任意 HTTP/*、用 EOF 关闭流、去掉 stream-id 与 GOAWAY,将 gRPC 状态和 trailers 以文本头块形式内嵌进响应体;
- 面向浏览器约束做文本化——通过
Accept: application/grpc-web-text协商 base64 文本流,并明确以"按 flush 分片的 base64"语义约束客户端解码。
对于需要实现或调试 gRPC-Web 代理转译的工程师,本文覆盖的 Content-Type 判定、trailers 内嵌编码(0x80/0x81 标志)、trailers 小写要求、EOF 关流与 base64 分片规则,构成了一份可直接对照检查的协议要点清单。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00