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 -lExpected:
200, then1or more. -
Docker,
curl,jq, andgrpcurlare 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_listenlist through your deployment method:config.yaml fragment apisix: node_listen: - port: 9080 - port: 9081 enable_http2: truePreserve 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.yamlwith 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
2and status404. gRPC-Web and transcoding useGATEWAY_URLand 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; }; doneADC resolves
--gateway-groupby 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.2Verify 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:
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-proxyExpected: 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-proxyWhat 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}"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-proxyBoth 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}"; doneExpected: all gateway paths return 404, the container and proto are absent, and only this guide's preflighted files are removed.
Troubleshoot
| Symptom | Cause | Fix |
|---|---|---|
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. |
Related
grpc-transcodeplugin reference (opens in Plugin Hub docs) — protobuf-backed HTTP translation.grpc-webplugin reference (opens in Plugin Hub docs) — browser protocol, prefix routing, and CORS.- Configuration Files — merge HTTP/2 listeners into existing configuration.
- Services and Routes — routing and upstream concepts.
- gRPC over HTTP/2 protocol — native method and path format.