API 网关专栏 · 第 78 章

通过 API 网关实现 gRPC-Web 与 gRPC 转码

2026年09月17日
通过 API 网关实现 gRPC-Web 与 gRPC 转码

API 网关可以通过三种不同契约暴露同一个 gRPC 服务:原生 gRPC 代理、面向浏览器的 gRPC-Web 转换,以及 HTTP/JSON 到 gRPC 的转码。这三种模式不能互换,它们面对的客户端、Wire Format、流式支持、Schema、错误处理与运维风险都不同。

应该选择客户端真正需要的最窄契约,再进行端到端测试。协议转换不会让所有 HTTP 插件自动理解 Protocol Buffers,也不能保证保留每种流式模式,更不会把内部 gRPC 服务自动变成设计良好的公共 REST API。

核心要点

  • 客户端能够通过 HTTP/2 使用 gRPC 时,优先选择原生 gRPC。
  • 浏览器使用 gRPC-Web 协议和生成的 Stub 时,选择 gRPC-Web。
  • HTTP/JSON 客户端需要映射到特定 gRPC 方法的精心设计契约时,选择转码。
  • 明确设置 Deadline 与消息大小限制,并测试 gRPC Status、Trailer、取消与流式行为。
  • 把认证 Metadata、CORS 与公共 JSON Schema 视为彼此独立的安全和兼容契约。

先选择协议契约

客户端需求网关模式上游主要边界
服务间 gRPC原生代理gRPC 或 gRPCS保留 HTTP/2 gRPC 语义
使用生成的 gRPC-Web 客户端的浏览器gRPC-Web 插件原生 gRPC转换浏览器兼容的 Framing 与 CORS
REST 风格的 HTTP/JSON 客户端gRPC 转码原生 gRPC把一个公共 HTTP 契约映射到一个 Proto Service Method

原生代理的转换最少。客户端与服务端共享 Protobuf Service Definition 与 gRPC Status Model。网关仍需要配置 HTTP/2、TLS、路由、认证与可观测性,并验证插件兼容性。

浏览器 API 无法直接使用原生 gRPC 传输,因此需要 gRPC-Web。官方 gRPC-Web 2.1.1 项目 README记录了 Binary 或 Text 模式的 Unary RPC,以及 grpcwebtext 模式的服务端流;这个固定版本的客户端模型不支持客户端流与双向流。应确认具体客户端库与版本,不能把原生 gRPC 能力直接推广到浏览器。

转码会创建一套独立的公共 HTTP API。路径、HTTP 方法、Query 与 Body 映射、JSON 数字表示、错误映射、Deadline 与授权策略都会成为该 API 兼容契约的一部分。

让转换边界保持可见

1flowchart LR
2    N[原生 gRPC 客户端] -->|HTTP/2 上的 gRPC| G[API 网关]
3    B[浏览器客户端] -->|gRPC-Web| G
4    H[HTTP 与 JSON 客户端] -->|HTTP 与 JSON| G
5    G -->|原生 gRPC| S[gRPC 服务]
6    G -. gRPC-Web Framing 与 CORS .-> B
7    G -. 基于 Proto 的 HTTP 与 JSON 映射 .-> H

实线箭头表示请求路径,虚线箭头标记网关负责的转换契约。每一份公共契约都必须执行认证与授权。CORS 接受某个浏览器 Origin,并不代表用户已经通过认证;JSON 字段成功解码,也不代表其内容可信。

为浏览器客户端代理 gRPC-Web

Apache APISIX 3.19.0 的 grpc-web 插件接受文档列出的 gRPC-Web Content-Type,解码 Binary 或 Base64 Text 消息,再把原生 gRPC 转发给上游。下面使用 Prefix Route,因为请求路径包含 Package、Service 与 Method。它不是完整生产路由:还需加入认证、授权、上游 TLS、健康检查,以及适合应用的 Origin 策略。

1{
2  "uri": "/grpc/web/*",
3  "methods": ["POST", "OPTIONS"],
4  "plugins": {
5    "grpc-web": {
6      "max_req_body_size": 1048576,
7      "cors_allow_headers": "content-type,x-grpc-web,x-user-agent,authorization"
8    }
9  },
10  "upstream": {
11    "scheme": "grpc",
12    "type": "roundrobin",
13    "nodes": {
14      "greeter.internal:50051": 1
15    }
16  }
17}

插件的 max_req_body_size 默认为 67,108,864 字节。本例主动把它降低到 1 MiB;限制值应根据 Protobuf 契约与工作负载确定,而不是直接沿用默认值。超大或无法读取的 Body 会在服务执行字段级验证之前被拒绝。

插件会处理 gRPC-Web CORS Preflight,并默认允许所有 Origin。cors_allow_headers 控制允许的请求头列表,但不会认证 Origin 或用户。应评估是否需要由独立浏览器边缘层或应用控制限制 Origin,并避免把 Bearer Token 放在 URL 中。CORS 约束的是浏览器行为,不能保护服务免受非浏览器客户端调用。

测试生成客户端实际使用的所有 Content-Type。Binary application/grpc-web+proto 与 Base64 Text application/grpc-web-text+proto 的 Framing 不同,在标准客户端中的流式支持也不同。还要测试取消、大响应、Trailer 传递、Token 过期,以及请求准确 Authorization Header 的 Preflight。

有意设计一份 HTTP 转码契约

Apache APISIX 的 grpc-transcode 插件要求配置 Proto 资源,以及精确的 Service 与 Method。首先注册 Service Definition。下面是最小 Unary Method;包含 Import 的 Proto 文件应按照 APISIX 文档编译成带依赖的 Descriptor Set。

1{
2  "content": "syntax = \"proto3\"; package orders; service OrderService { rpc GetOrder (GetOrderRequest) returns (GetOrderReply); } message GetOrderRequest { string order_id = 1; } message GetOrderReply { string order_id = 1; int64 created_at_unix_ms = 2; }"
3}

然后把一条公共路由绑定到该 Proto、Service 与 Method:

1{
2  "uri": "/v1/orders",
3  "methods": ["GET"],
4  "plugins": {
5    "grpc-transcode": {
6      "proto_id": "orders-v1",
7      "service": "orders.OrderService",
8      "method": "GetOrder",
9      "deadline": 1000,
10      "max_resp_body_size": 1048576,
11      "pb_option": [
12        "enum_as_name",
13        "int64_as_string",
14        "auto_default_values",
15        "disable_hooks"
16      ]
17    }
18  },
19  "upstream": {
20    "scheme": "grpc",
21    "type": "roundrobin",
22    "nodes": {
23      "orders.internal:50051": 1
24    }
25  }
26}

这同样是配置片段。请求应按照插件记录的字段映射提供 order_id。deadline 为 1,000 ms,int64_as_string 避免把 64 位整数强制转换为 JavaScript 客户端可能无法精确表示的 JSON Number。公共 OpenAPI 或其他 HTTP 契约应描述最终的 String 类型,而不是内部 Protobuf Integer 类型。

max_resp_body_size 默认为 67,108,864 字节。本路由把内存中的转码 Buffer 降至 1 MiB。只有在上游契约或其他已执行的上游限制能保证完整的编码后 gRPC 响应(包括 Framing)严格小于 1 MiB 时,才应使用此设置;否则应分页、限制上游结果,或选择经过验证的更大 Buffer。达到上限时,APISIX 的响应缓冲逻辑可能把截断的字节前缀交给 Protobuf 解码逻辑。解码可能失败,但如果前缀恰好停在字段边界,也可能在原 HTTP 成功状态下产生有效却不完整的 JSON。插件不会可靠地报告截断或自动触发告警。应测试低于、等于和高于上限的响应,并独立监测上游响应大小限制。

show_status_in_body 是 grpc-transcode 中会改变行为的布尔参数,默认值为 false。取值为 false 时,APISIX 不会把错误状态对应的已解码 grpc-status-details-bin 内容加入响应体;取值为 true 时,则会解码这些详情并在 JSON Body 中暴露。只有在审查信息披露与客户端兼容性后,才应启用 true 路径。两种取值都不能取代明确的公共错误 Schema。

协同设计 Deadline、取消与重试

gRPC Deadline 指南指出,客户端默认不会设置 Deadline。没有 Deadline 时,调用可能无限期等待。应定义客户端总预算,扣除网关与网络耗时,在支持的情况下传递剩余 Deadline,并确保取消后上游停止工作。

不要在浏览器、网关、转码器与服务上分别配置互不协调的完整超时。它们叠加后,可能在调用方已经离开时仍让高成本工作继续运行。应分别观测客户端取消、网关终止、上游取消与服务完成。

只有语义允许的方法才可以重试。Unary Read 可以在同一 Deadline 内重试;会改变状态的方法需要应用层幂等。流式连接若不能丢失或重复消息,重连就需要 Resume Token 或应用 Offset。网关无法仅根据 rpc 语法推断这些契约。

保留 gRPC Status 与公共错误含义

原生 gRPC 通过 Trailer 报告 Status。gRPC-Web 通过浏览器兼容 Framing 传递协议状态。转码则可能生成 HTTP Status 与 JSON Body。只监控 HTTP 200 会漏掉应用层 gRPC 失败;把所有 gRPC Code 盲目改写成同一个 HTTP Code,也会破坏有价值的语义。

为公共契约明确设计并测试以下映射:

  • 未认证与权限不足;
  • 参数无效与前置条件失败;
  • 资源不存在与已经存在;
  • 资源耗尽与过载;
  • Deadline Exceeded 与 Unavailable;
  • 经过脱敏的内部错误。

关联 Metadata 必须与可信身份分开。如果调用方可以提供 Request ID,应验证其大小与格式,且不能把它当作已认证主体的证明。

测试插件与可观测性兼容性

处理 HTTP Header 的插件可能适用于三种模式,但要求 JSON Body 的插件未必能够理解 Binary gRPC 或 gRPC-Web Frame。应针对准确的协议模式与消息大小测试认证、限流、日志、Body 限制、转换与追踪。

对于原生 gRPC 和 gRPC-Web,在不记录敏感 Protobuf Payload 的前提下,采集 Service 与 Method、gRPC Status、持续时间、请求与响应字节、Deadline 结果与 Stream 生命周期。对于转码,在保留上游 gRPC Status 用于诊断的同时,再加入公共 Route 与 HTTP 结果。

gRPC 网关验证清单

  • 每条路由是否明确属于原生 gRPC、gRPC-Web 或转码后的 HTTP/JSON?
  • 所选客户端模式是否支持需要的流式类型?
  • 能否保证完整的编码后响应严格小于转码 Buffer 上限?
  • CORS 与认证是否作为不同控制处理?
  • 每个转码字段与 JSON 数字表示是否有文档?
  • 是否跨所有 Hop 测试 Deadline 与取消行为?
  • 是否只对已证明语义安全的方法重试?
  • gRPC Status、Trailer 与公共 HTTP 错误是否可观测?
  • 每个已配置插件是否在准确协议模式下通过测试?

常见问题

浏览器可以直接调用原生 gRPC 服务吗?

普通浏览器 API 使用 gRPC-Web,而不是原生 gRPC 传输。API 网关或兼容代理会为服务把 gRPC-Web 转换成原生 gRPC。

转码会自动创建 REST API 吗?

转码只创建协议映射,不会完成 API 设计。资源命名、HTTP 语义、Schema、错误、认证、兼容性与文档仍由团队负责。

是否应该把所有 gRPC 错误都转换成 HTTP 500?

不应该。应为公共契约定义有意设计的映射,同时保留原始 gRPC Status 用于诊断。不要暴露可能泄漏敏感实现信息的内部状态详情。

后续步骤

在所选协议契约中加入超时与重试预算、细粒度授权以及API 访问日志证据。

获取方案