a6-plugin-grpc-transcode
概览
grpc-transcode 插件会将 HTTP/JSON 请求转换为 gRPC 调用,并将 gRPC 响应返回为 JSON。客户端发送标准 HTTP 请求,Apache APISIX使用预先上传的 protobuf 定义将其转换为 gRPC 请求,转发到 gRPC 上游,再将响应返回为 JSON。gRPC 服务无需修改。
适用场景
- 通过 RESTful HTTP 端点公开 gRPC 服务。
- 允许浏览器或移动客户端在没有 gRPC 客户端库的情况下调用 gRPC 服务。
- 为 gRPC 服务添加 HTTP API 网关功能(认证、限流、日志)。
- 逐步从 REST 迁移到 gRPC。
- 将 gRPC 错误详情解码为人类可读的 JSON。
插件配置参考(路由/服务)
| 字段 | 类型 | 是否必填 | 默认值 | 说明 |
|---|---|---|---|---|
proto_id | string/integer | 是 | — | proto 资源 ID(通过 a6 proto create 上传)。 |
service | string | 是 | — | 完全限定的 gRPC 服务名称(例如 helloworld.Greeter)。 |
method | string | 是 | — | gRPC 方法名称(例如 SayHello)。 |
deadline | number | 否 | 0 | gRPC 调用的截止时间,单位为毫秒。0 表示无截止时间。 |
pb_option | array[string] | 否 | — | Protobuf 序列化选项(见下表)。 |
show_status_in_body | boolean | 否 | false | 出错时将解析后的 grpc-status-details-bin 包含在 JSON 响应体中。 |
status_detail_type | string | 否 | — | gRPC 错误状态中 details 字段的消息类型。解码错误详情时必须设置。 |
pb 选项值
| 选项 | 描述 |
|---|---|
enum_as_name | 返回 enum 字段作为字符串名称( 如"PENDING") |
enum_as_value | 返回 enum 字段为整数(例如1) |
int64_as_number | 将 int64 作为 JSON 数字返回(JavaScript 可能丢失精度) |
int64_as_string | 返回 int64 作为字符串( JavaScript 客户端的安全性) |
int64_as_hexstring | 返回十六进制字符串 。 |
auto_default_values | 自动填充未设置字段的默认值 |
no_default_values | 不为未设置字段添加默认值 |
use_default_values | 使用原定义的默认值 |
use_default_metatable | 默认值使用元表 |
enable_hooks | 启用 Proto hooks |
disable_hooks | 禁用 Proto hooks |
可合并多个选项:["int64_as_string", "enum_as_name"]
分步指南:设置 gRPC 转码
1. 上传 Proto 定义
给定一个原始文件:
syntax = "proto3";
package helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
上传 Proto 文件:
a6 proto create -f - <<'EOF'
{
"id": "1",
"content": "syntax = \"proto3\";\npackage helloworld;\nservice Greeter {\n rpc SayHello (HelloRequest) returns (HelloReply) {}\n}\nmessage HelloRequest {\n string name = 1;\n}\nmessage HelloReply {\n string message = 1;\n}"
}
EOF
2. 使用 grpc-transcode 创建路由
a6 route create -f - <<'EOF'
{
"id": "grpc-hello",
"methods": ["GET", "POST"],
"uri": "/grpc/hello",
"plugins": {
"grpc-transcode": {
"proto_id": "1",
"service": "helloworld.Greeter",
"method": "SayHello"
}
},
"upstream": {
"scheme": "grpc",
"type": "roundrobin",
"nodes": {
"grpc-server:50051": 1
}
}
}
EOF
**重要:**该上游的 scheme 必须为 "grpc"(使用 TLS 时为 "grpcs")。
3. 测试端点
# Pass parameters via query string
curl "http://localhost:9080/grpc/hello?name=world"
# Response: {"message":"Hello world"}
# Or via POST body
curl -X POST http://localhost:9080/grpc/hello \
-H "Content-Type: application/json" \
-d '{"name": "world"}'
# Response: {"message":"Hello world"}
常见模式
Proto 使用 imports(使用已编译的 .pb 文件)
如果 proto 包含 import 语句,请先将其编译为 .pb 文件:
protoc --include_imports --descriptor_set_out=service.pb proto/service.proto
然后上传 Base64 编码的 .pb 文件:
a6 proto create -f - <<EOF
{
"id": "2",
"content": "$(base64 -i service.pb)"
}
EOF
JavaScript 客户端的安全 int64 处理
{
"plugins": {
"grpc-transcode": {
"proto_id": "1",
"service": "order.OrderService",
"method": "GetOrder",
"pb_option": ["int64_as_string"]
}
}
}
返回{"order_id": "9223372036854775807"}而不是{"order_id": 9223372036854775807}( JavaScript会损坏)。
人类可读枚举
{
"plugins": {
"grpc-transcode": {
"proto_id": "1",
"service": "order.OrderService",
"method": "GetOrder",
"pb_option": ["enum_as_name", "int64_as_string"]
}
}
}
返回{"status": "PENDING"}而不是{"status": 1}。
gRPC 错误细节解码
{
"plugins": {
"grpc-transcode": {
"proto_id": "1",
"service": "helloworld.Greeter",
"method": "SayHello",
"show_status_in_body": true,
"status_detail_type": "helloworld.ErrorDetail"
}
}
}
错误响应正文:
{
"error": {
"code": 14,
"message": "Out of service",
"details": [
{
"type": "service",
"message": "The server is out of service",
"code": 1
}
]
}
}
使用 deadline 控制超时
{
"plugins": {
"grpc-transcode": {
"proto_id": "1",
"service": "slow.SlowService",
"method": "LongRunning",
"deadline": 5000
}
}
}
如果gRPC服务在5秒内没有响应,则请求失败。
不同路由上的多种gRPC方法
// Route 1: GET /api/users/:id → user.UserService/GetUser
{
"id": "grpc-get-user",
"uri": "/api/users/*",
"methods": ["GET"],
"plugins": {
"grpc-transcode": {
"proto_id": "3",
"service": "user.UserService",
"method": "GetUser"
}
},
"upstream": {"scheme": "grpc", "type": "roundrobin", "nodes": {"user-svc:50051": 1}}
}
// Route 2: POST /api/users → user.UserService/CreateUser
{
"id": "grpc-create-user",
"uri": "/api/users",
"methods": ["POST"],
"plugins": {
"grpc-transcode": {
"proto_id": "3",
"service": "user.UserService",
"method": "CreateUser"
}
},
"upstream": {"scheme": "grpc", "type": "roundrobin", "nodes": {"user-svc:50051": 1}}
}
故障排查
| 症状 | 原因 | 解决方法 |
|---|---|---|
Proto not found | Proto ID 不存在 | 使用 a6 proto get <id> 检查 |
| "没有找到方法" | 服务或方法名称不匹配 | 使用完全合格的名称:package.Service, 对大小写敏感 |
| 拒绝连接到上游 | 错误的方案或端口 | 设置上游scheme到grpc, 验证端口为 gRPC 端口 |
| Proto import 错误 | Proto 包含 import,但上传的是原始内容 | 使用 protoc --include_imports 编译为 .pb |
| int64 值已损坏 | JavaScript 精度丢失 | 使用 pb_option: ["int64_as_string"] |
| Enum 显示数字而不是 名称 | 默认行为 | 使用pb_option: ["enum_as_name"] |
| 未解码的错误细节 | 未设置 | 设置show_status_in_body: true和status_detail_type |
| gRPC 调用长时间无响应 | 未设置超时时间 | 设置以毫秒计的 deadline |
| 502 Bad Gateway | gRPC 服务无法运行或无法达到 | 检查 gRPC 服务已启动, 从 APISIX 访问端口 |
配置同步示例
version: "1"
protos:
- id: helloworld-proto
content: |
syntax = "proto3";
package helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
routes:
- id: grpc-hello
methods:
- GET
- POST
uri: /grpc/hello
plugins:
grpc-transcode:
proto_id: helloworld-proto
service: helloworld.Greeter
method: SayHello
pb_option:
- int64_as_string
- enum_as_name
upstream_id: grpc-backend
upstreams:
- id: grpc-backend
scheme: grpc
type: roundrobin
nodes:
"grpc-server:50051": 1
本文根据 api7/a6 仓库中的 a6-plugin-grpc-transcode/SKILL.md 生成。可在 AI Agent Skills 页面浏览全部 Skill。