Docs
API7 GatewayManage traffic and APIsProtocol ProxyConfigure gRPC Proxying

Configure gRPC Proxying

Proxy native gRPC, expose an HTTP endpoint with gRPC transcoding, and accept gRPC-Web requests.

Applies to:API7 Gateway 3.10.x

Roles:Platform engineer

Goal

One gRPC upstream exposed in three ways: native gRPC at /helloworld.Greeter/SayHello, an HTTP endpoint at /hello, and a gRPC-Web prefix at /grpc/web/*.

The paths and methods do not overlap. Native clients use HTTP/2 and POST; ordinary HTTP clients use grpc-transcode (opens in Plugin Hub docs); browser clients use grpc-web (opens in Plugin Hub docs) over HTTP/1.1 or HTTP/2.

Before you begin

Set the variables used by this guide:

export API7_ADMIN_URL="https://localhost:7443"
export API7_GATEWAY_GROUP_ID="default"   # only the group the quickstart creates; otherwise copy the gateway group's UUID from Gateway Groups in the Dashboard
read -rp "Gateway group name for ADC (if used): " API7_GATEWAY_GROUP_NAME && export API7_GATEWAY_GROUP_NAME
export GATEWAY_URL="http://127.0.0.1:9080"
export GRPC_GATEWAY_ADDRESS="127.0.0.1:9081"
read -rsp "Dashboard token: " API7_ADMIN_KEY && export API7_ADMIN_KEY && echo
  • Confirm that the control plane and group are ready:

    curl -ks -o /dev/null -w '%{http_code}\n' "${API7_ADMIN_URL}/api/gateway_groups" -H "X-API-KEY: ${API7_ADMIN_KEY}"
    curl -ks "${API7_ADMIN_URL}/api/gateway_groups/${API7_GATEWAY_GROUP_ID}/instances" -H "X-API-KEY: ${API7_ADMIN_KEY}" | grep -o '"status": *"Healthy"' | wc -l

    Expected: 200, then 1 or more.

  • Docker, curl, jq, and grpcurl are available.

  • The gateway can reach host.docker.internal:50051. Docker Desktop provides this name; use a gateway-reachable address throughout on another runtime.

  • Native gRPC requires a published HTTP/2 listener. Merge an entry like this into the existing apisix.node_listen list through your deployment method:

    config.yaml fragment
    apisix:
      node_listen:
        - port: 9080
        - port: 9081
          enable_http2: true

    Preserve every other setting, back up the deployment values, apply the change through Docker Compose, Helm, or your service manager, and wait for rollout. Do not replace the whole config.yaml with this fragment. Confirm the published endpoint negotiates cleartext HTTP/2:

    curl -sS --connect-timeout 3 --max-time 10 --http2-prior-knowledge "http://${GRPC_GATEWAY_ADDRESS}/" -o /dev/null -w '%{http_version} %{http_code}\n'

    Expected before routes exist: HTTP version 2 and status 404. gRPC-Web and transcoding use GATEWAY_URL and do not require this extra listener.

  • Confirm the example's local file, container name, and port are free:

    test ! -e helloworld.proto || { echo 'Stop: helloworld.proto already exists'; exit 1; }
    if docker container inspect grpc-example-server >/dev/null 2>&1; then echo 'Stop: container grpc-example-server already exists'; exit 1; fi
    if docker container ls --format '{{.Ports}}' | grep -Eq '(^|, |:)50051->'; then echo 'Stop: a Docker container already publishes local port 50051'; exit 1; fi
  • For ADC, confirm label-selector support and unused paths:

    adc --version
    adc sync --help | grep -- '--label-selector'
    for file in grpc-adc.yaml grpc-cleanup.yaml diff.yaml; do test ! -e "${file}" || { echo "Stop: ${file} already exists"; exit 1; }; done

    ADC resolves --gateway-group by display name; the Admin API uses the group UUID. Use the display name for the same group.

This guide creates grpc-service, three routes, and helloworld-proto. Check every ID:

curl -ks -w '\nservice: %{http_code}\n' "${API7_ADMIN_URL}/apisix/admin/services/grpc-service?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
for id in grpc-native-route grpc-transcode-route grpc-web-route; do curl -ks -w "\nroute ${id}: %{http_code}\n" "${API7_ADMIN_URL}/apisix/admin/routes/${id}?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"; done
curl -ks -w '\nproto: %{http_code}\n' "${API7_ADMIN_URL}/apisix/admin/protos/helloworld-proto?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"

Expected: every resource is absent. A 200 with a matching resource means the ID is in use; stop or change every occurrence. Correct other errors until absence is unambiguous because PUT replaces matching IDs. For ADC, continue only if its diff creates exactly the expected service and routes without updating or deleting an existing object.

Steps

Step 1: Create the proto file and start the upstream

cat > helloworld.proto <<'EOF'
syntax = "proto3";

package helloworld;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
EOF

docker run -d --name grpc-example-server -p 50051:50051 api7/grpc-server-example:1.0.2

Verify the definition against the upstream:

grpcurl -max-time 10 -plaintext -proto helloworld.proto -d '{"name":"World"}' 127.0.0.1:50051 helloworld.Greeter.SayHello
{ "message": "Hello World" }

What changed: the server is running, and one visible proto file defines the client and transcoding contract.

Step 2: Upload the proto resource

ADC does not manage proto resources, so both paths perform this step with the Admin API. Build the body from the local file:

jq -Rs '{content: ., labels: {guide: "grpc-proxy"}}' helloworld.proto \
  | curl -ksS "${API7_ADMIN_URL}/apisix/admin/protos/helloworld-proto?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
      -H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" --data-binary @-
{ "value": { "id": "helloworld-proto", … } }

What changed: grpc-transcode can resolve the protobuf definition by ID.

Step 3: Create the service and routes with the Admin API

Choose this path or step 4; do not apply both.

curl -ksS "${API7_ADMIN_URL}/apisix/admin/services/grpc-service?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
  -H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" \
  -d '{"name":"grpc-service","labels":{"guide":"grpc-proxy"},"upstream":{"name":"default","type":"roundrobin","scheme":"grpc","nodes":[{"host":"host.docker.internal","port":50051,"weight":100}]}}'
curl -ksS "${API7_ADMIN_URL}/apisix/admin/routes/grpc-native-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
  -H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" \
  -d '{"name":"grpc-native-route","labels":{"guide":"grpc-proxy"},"methods":["POST"],"paths":["/helloworld.Greeter/SayHello"],"service_id":"grpc-service"}'
curl -ksS "${API7_ADMIN_URL}/apisix/admin/routes/grpc-transcode-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
  -H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" \
  -d '{"name":"grpc-transcode-route","labels":{"guide":"grpc-proxy"},"methods":["GET"],"paths":["/hello"],"service_id":"grpc-service","plugins":{"grpc-transcode":{"proto_id":"helloworld-proto","service":"helloworld.Greeter","method":"SayHello"}}}'
curl -ksS "${API7_ADMIN_URL}/apisix/admin/routes/grpc-web-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
  -H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" \
  -d '{"name":"grpc-web-route","labels":{"guide":"grpc-proxy"},"methods":["POST","OPTIONS"],"paths":["/grpc/web/*"],"service_id":"grpc-service","plugins":{"grpc-web":{}}}'

What changed: native, transcoded HTTP, and gRPC-Web traffic have distinct selectors and share one gRPC upstream.

Step 4: Apply the service and routes with ADC

Skip this alternative if you completed step 3. Save the complete ADC-managed state:

grpc-adc.yaml
services:
  - name: grpc-service
    labels: {guide: grpc-proxy}
    upstream:
      name: default
      scheme: grpc
      type: roundrobin
      nodes:
        - {host: host.docker.internal, port: 50051, weight: 100}
    routes:
      - name: grpc-native-route
        labels: {guide: grpc-proxy}
        uris: [/helloworld.Greeter/SayHello]
        methods: [POST]
      - name: grpc-transcode-route
        labels: {guide: grpc-proxy}
        uris: [/hello]
        methods: [GET]
        plugins:
          grpc-transcode: {proto_id: helloworld-proto, service: helloworld.Greeter, method: SayHello}
      - name: grpc-web-route
        labels: {guide: grpc-proxy}
        uris: [/grpc/web/*]
        methods: [POST, OPTIONS]
        plugins:
          grpc-web: {}

Lint, validate, and inspect the diff:

adc lint -f grpc-adc.yaml
adc validate -f grpc-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=grpc-proxy
adc diff -f grpc-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=grpc-proxy

Expected: validation succeeds and the diff creates one labeled service with three nested routes. Apply the reviewed diff:

adc sync -f grpc-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=grpc-proxy

What changed: ADC reconciled all three routes as one desired state. The proto remains separately managed from step 2.

Validate

Call the native route through the HTTP/2 listener:

grpcurl -max-time 10 -plaintext -proto helloworld.proto -d '{"name":"API7"}' "${GRPC_GATEWAY_ADDRESS}" helloworld.Greeter.SayHello
{ "message": "Hello API7" }

Call the transcoded route:

curl -fsS --connect-timeout 3 --max-time 15 "${GATEWAY_URL}/hello?name=API7"
{ "message": "Hello API7" }

Check gRPC-Web route selection and CORS preflight handling:

curl -isS --connect-timeout 3 --max-time 15 "${GATEWAY_URL}/grpc/web/helloworld.Greeter/SayHello" -X OPTIONS \
  -H 'Origin: https://client.example' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: content-type,x-grpc-web,x-user-agent'

Expected: a successful response permits POST, permits the requested headers, and exposes grpc-status and grpc-message. This proves routing and preflight handling only. It does not send a framed gRPC-Web message or prove a browser RPC; use a client generated from helloworld.proto for that runtime test.

Roll back / Clean up

For the Admin API path, remove routes and service. For ADC, use the empty selected state instead:

for id in grpc-web-route grpc-transcode-route grpc-native-route; do curl -ks -X DELETE "${API7_ADMIN_URL}/apisix/admin/routes/${id}?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"; done
curl -ks -X DELETE "${API7_ADMIN_URL}/apisix/admin/services/grpc-service?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
grpc-cleanup.yaml
services: []
adc diff -f grpc-cleanup.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=grpc-proxy
adc sync -f grpc-cleanup.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=grpc-proxy

Both paths then remove the separately managed proto, container, and local files:

curl -ks -X DELETE "${API7_ADMIN_URL}/apisix/admin/protos/helloworld-proto?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
docker rm -f grpc-example-server
rm helloworld.proto
for file in grpc-adc.yaml grpc-cleanup.yaml diff.yaml; do test ! -e "${file}" || rm "${file}"; done

Expected: all gateway paths return 404, the container and proto are absent, and only this guide's preflighted files are removed.

Troubleshoot

SymptomCauseFix
Native grpcurl reports an HTTP/1.x or framing error.The endpoint is not a published HTTP/2 listener.Recheck the deployment-specific listener merge and curl --http2-prior-knowledge.
The gateway returns 502 or gRPC Unavailable.It cannot reach host.docker.internal:50051.Use an address reachable from the gateway and repeat the direct upstream call.
Transcoding cannot find the proto, service, or method.The proto is absent from this group or names differ.Fetch helloworld-proto and compare helloworld.Greeter and SayHello with the file.
gRPC reports unknown service.The upstream path includes the wrong prefix or service name.Native uses /helloworld.Greeter/SayHello; browser clients use base URL /grpc/web, which the plugin removes before proxying.
Browser preflight lacks a header.The request missed /grpc/web/* or asks for an additional header.Use the exact prefix and configure cors_allow_headers (opens in Plugin Hub docs) when needed.