Skip to main content

grpc-transcode

The grpc-transcode plugin lets HTTP clients call gRPC services. It encodes HTTP request data as protobuf messages, forwards the resulting gRPC call, and decodes the protobuf response as JSON.

Each plugin instance references a proto resource that defines the service, method, and message types. The plugin can also map gRPC status codes to HTTP responses, decode structured error details, and control how protobuf values are represented in JSON.

Examples​

The examples expose gRPC methods through HTTP routes and demonstrate descriptor sets, error mapping, and JSON response options.

Start the Example gRPC Server​

The examples use the maintained grpc_server_example service. Connect the gateway and example server through a dedicated Docker network so the gateway can resolve the server by container name.

Set GATEWAY_CONTAINER to the running APISIX container:

export GATEWAY_CONTAINER=replace-with-gateway-container-name

Create the network and connect the gateway:

docker network create gateway-grpc-transcode-net
docker network connect gateway-grpc-transcode-net "$GATEWAY_CONTAINER"

Start the example server on the same network:

docker run -d \
--name grpc-transcode-server \
--network gateway-grpc-transcode-net \
api7/grpc-server-example:1.0.2

The examples create proto resources through the APISIX Admin API. ADC and the Ingress Controller can configure routes that reference existing proto resources, but they cannot create the proto resources required by these examples.

API7 Gateway exposes equivalent proto and route resources through its gateway-group-scoped Admin API. See Configure gRPC Proxying.

Install jq to encode proto files in the Admin API request bodies.

If Admin API authentication is enabled, export an admin-role key before continuing:

export ADMIN_API_KEY=replace-with-admin-api-key

Transcode an HTTP Request​

The following example demonstrates how to expose a gRPC method through an HTTP route.

Create echo.proto with the service and message definitions:

echo.proto
syntax = "proto3";

package echo;

service EchoService {
rpc Echo (EchoMsg) returns (EchoMsg);
}

message EchoMsg {
string msg = 1;
}

Create a proto resource from the file:

jq -Rs '{content: .}' echo.proto | \
curl "http://127.0.0.1:9180/apisix/admin/protos/echo-proto" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d @-

Create a route that maps HTTP requests to echo.EchoService/Echo:

curl "http://127.0.0.1:9180/apisix/admin/routes/grpc-transcode-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d @- <<'EOF'
{
"uri": "/echo",
"methods": ["GET"],
"plugins": {
"grpc-transcode": {
"proto_id": "echo-proto",
"service": "echo.EchoService",
"method": "Echo"
}
},
"upstream": {
"scheme": "grpc",
"type": "roundrobin",
"nodes": {
"grpc-transcode-server:50051": 1
}
}
}
EOF

Send an HTTP request whose query parameter matches the EchoMsg.msg field:

curl "http://127.0.0.1:9080/echo?msg=Hello"

The response should be:

{"msg":"Hello"}

Load a Protobuf Descriptor Set​

When definitions import other proto files, combine them into a binary descriptor set with protoc and store its base64 representation in the proto resource.

Generate a descriptor set from echo.proto and encode it as a single-line base64 value:

protoc --include_imports \
--descriptor_set_out=echo_proto.pb \
echo.proto

export PROTO_DESCRIPTOR="$(base64 < echo_proto.pb | tr -d '\n')"

Create the proto resource from the descriptor:

curl "http://127.0.0.1:9180/apisix/admin/protos/echo-file-proto" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d @- <<EOF
{
"content": "$PROTO_DESCRIPTOR"
}
EOF

Create a route that uses the descriptor-backed proto resource:

curl "http://127.0.0.1:9180/apisix/admin/routes/grpc-file-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d @- <<'EOF'
{
"uri": "/echo-file",
"plugins": {
"grpc-transcode": {
"proto_id": "echo-file-proto",
"service": "echo.EchoService",
"method": "Echo"
}
},
"upstream": {
"scheme": "grpc",
"type": "roundrobin",
"nodes": {
"grpc-transcode-server:50051": 1
}
}
}
EOF

Send a request to verify the descriptor-backed resource:

curl "http://127.0.0.1:9080/echo-file?msg=Hello"

The response should be:

{"msg":"Hello"}

Decode Structured gRPC Errors​

The example server returns a structured UNAVAILABLE error from GetErrResp. Create hello.proto with the service and error detail definitions:

hello.proto
syntax = "proto3";

package helloworld;

service Greeter {
rpc GetErrResp (HelloRequest) returns (HelloReply);
}

message HelloRequest {
string name = 1;
repeated string items = 2;
}

message HelloReply {
string message = 1;
repeated string items = 2;
}

message ErrorDetail {
int64 code = 1;
string message = 2;
string type = 3;
}

Create the proto resource:

jq -Rs '{content: .}' hello.proto | \
curl "http://127.0.0.1:9180/apisix/admin/protos/hello-proto" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d @-

Create a route that exposes status details in the response body and decodes the embedded ErrorDetail message:

curl "http://127.0.0.1:9180/apisix/admin/routes/grpc-error-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d @- <<'EOF'
{
"uri": "/hello",
"plugins": {
"grpc-transcode": {
"proto_id": "hello-proto",
"service": "helloworld.Greeter",
"method": "GetErrResp",
"show_status_in_body": true,
"status_detail_type": "helloworld.ErrorDetail"
}
},
"upstream": {
"scheme": "grpc",
"type": "roundrobin",
"nodes": {
"grpc-transcode-server:50051": 1
}
}
}
EOF

Send a request to the route:

curl -i "http://127.0.0.1:9080/hello?name=world"

The response should include grpc-status: 14, map the gRPC status to 503 Service Temporarily Unavailable, and contain a decoded error body similar to:

{
"error": {
"code": 14,
"message": "Out of service",
"details": [
{
"code": 1,
"message": "The server is out of service",
"type": "service"
}
]
}
}

If status_detail_type is omitted, the gateway still includes the error details in the response body, but leaves each detail as a generic protobuf Any value.

Preserve 64-Bit Integer Precision​

The default protobuf JSON representation emits 64-bit integers as numbers. Configure int64_as_string when clients need the exact value represented as a string.

Create plus.proto:

plus.proto
syntax = "proto3";

package helloworld;

service Greeter {
rpc Plus (PlusRequest) returns (PlusReply);
}

message PlusRequest {
int64 a = 1;
int64 b = 2;
}

message PlusReply {
int64 result = 1;
}

Create the proto resource:

jq -Rs '{content: .}' plus.proto | \
curl "http://127.0.0.1:9180/apisix/admin/protos/plus-proto" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d @-

Create a route using the default protobuf options:

curl "http://127.0.0.1:9180/apisix/admin/routes/grpc-plus-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d @- <<'EOF'
{
"uri": "/plus",
"plugins": {
"grpc-transcode": {
"proto_id": "plus-proto",
"service": "helloworld.Greeter",
"method": "Plus"
}
},
"upstream": {
"scheme": "grpc",
"type": "roundrobin",
"nodes": {
"grpc-transcode-server:50051": 1
}
}
}
EOF

Send a request whose exact sum is not preserved by the default numeric JSON encoding:

curl "http://127.0.0.1:9080/plus?a=1237528374197491&b=1237528374197491"

The default response represents the result as a number:

{"result":2.475056748395e+15}

The numeric representation rounds the exact sum of 2475056748394982.

Update the route to use int64_as_string:

curl "http://127.0.0.1:9180/apisix/admin/routes/grpc-plus-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d @- <<'EOF'
{
"uri": "/plus",
"plugins": {
"grpc-transcode": {
"proto_id": "plus-proto",
"service": "helloworld.Greeter",
"method": "Plus",
"pb_option": ["int64_as_string"]
}
},
"upstream": {
"scheme": "grpc",
"type": "roundrobin",
"nodes": {
"grpc-transcode-server:50051": 1
}
}
}
EOF

Send the request again:

curl "http://127.0.0.1:9080/plus?a=1237528374197491&b=1237528374197491"

The exact result should now be represented as a string:

{"result":"#2475056748394982"}