grpc-web
Browsers cannot call native gRPC services directly because browser networking APIs do not expose the HTTP/2 framing that gRPC requires. gRPC-Web defines a browser-compatible protocol that supports unary and server-streaming calls.
The grpc-web plugin translates gRPC-Web requests into native gRPC calls and forwards them to upstream gRPC services. It translates the responses back into gRPC-Web so that browser applications can consume them.
Request Handling
The plugin accepts POST requests for RPC calls and OPTIONS requests for CORS preflight checks. It recognizes these content types:
application/grpc-webapplication/grpc-web-textapplication/grpc-web+protoapplication/grpc-web-text+proto
The official gRPC-Web browser client supports unary calls in binary mode, while its base64-encoded text mode supports unary and server-streaming calls. The plugin decodes the request, forwards it as native gRPC, and encodes the upstream response in the format requested by the client.
For cross-origin requests, the plugin allows all origins and POST requests by default. It accepts content-type, x-grpc-web, and x-user-agent request headers, and exposes the grpc-status and grpc-message response headers. Use cors_allow_headers to extend the accepted request headers.
See the gRPC-Web protocol specification and browser feature support for protocol-level details.
Examples
The examples configure a browser client to send unary and server-streaming requests through the gateway to the official gRPC-Web Echo service.
Run APISIX or API7 Gateway and use Docker for the Admin API and ADC examples, or a Kubernetes cluster with API7 Ingress Controller for the Kubernetes examples.
Install Protocol Buffers compiler 35.0 and protoc-gen-grpc-web 2.0.2 on your PATH. If you use the ADC configuration, install ADC 0.30.4 or later.
The client uses the official gRPC-Web Echo protobuf and server implementation from release 2.0.2. Confirm that the compiler and gRPC-Web generator are available:
protoc --version
protoc-gen-grpc-web --version
The commands should report libprotoc 35.0 and protoc-gen-grpc-web 2.0.2.
Start the Echo Service
Create a directory for the Echo service and download the protobuf and server implementation from the pinned gRPC-Web release commit:
mkdir grpc-web-echo
cd grpc-web-echo
curl -fLO "https://raw.githubusercontent.com/grpc/grpc-web/9e6bf0f521ebeecf7cdde62ee10da289ef8d9b0a/net/grpc/gateway/examples/echo/echo.proto"
curl -fLo server.js "https://raw.githubusercontent.com/grpc/grpc-web/9e6bf0f521ebeecf7cdde62ee10da289ef8d9b0a/net/grpc/gateway/examples/echo/node-server/server.js"
Create a Dockerfile that builds the service for the host architecture and pins its runtime dependencies:
FROM node:24.13.1-alpine
WORKDIR /app/node-server
COPY echo.proto /app/echo.proto
COPY server.js ./server.js
RUN npm init -y && \
npm install --omit=dev --save-exact \
@grpc/grpc-js@1.14.4 \
@grpc/proto-loader@0.8.1 \
async@3.2.6 \
lodash@4.18.1
EXPOSE 9090
CMD ["node", "server.js"]
Build the image:
docker build -t grpc-web-echo:2.0.2 .
Start the service in the environment used by the gateway:
- Docker
- Kubernetes
Set GATEWAY_CONTAINER to the running APISIX or API7 Gateway container. Create a dedicated network and connect the gateway to it:
export GATEWAY_CONTAINER=replace-with-gateway-container-name
docker network create gateway-grpc-web-net
docker network connect gateway-grpc-web-net "$GATEWAY_CONTAINER"
Start the Echo service on the same network:
docker run -d --name grpc-web-echo \
--network gateway-grpc-web-net \
grpc-web-echo:2.0.2
Make the locally built image available to the cluster. For a local kind cluster, load it directly:
kind load docker-image grpc-web-echo:2.0.2
For other clusters, push the image to a registry that the cluster can access and replace grpc-web-echo:2.0.2 in the manifest with that image reference.
Create the Echo service:
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: aic
name: grpc-web-echo
spec:
replicas: 1
selector:
matchLabels:
app: grpc-web-echo
template:
metadata:
labels:
app: grpc-web-echo
spec:
containers:
- name: grpc-web-echo
image: grpc-web-echo:2.0.2
imagePullPolicy: IfNotPresent
ports:
- name: grpc
containerPort: 9090
readinessProbe:
tcpSocket:
port: grpc
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: grpc-web-echo
spec:
selector:
app: grpc-web-echo
ports:
- name: grpc
port: 9090
targetPort: grpc
Apply the manifest and wait for the deployment to become available:
kubectl apply -f grpc-web-echo.yaml
kubectl rollout status -n aic deployment/grpc-web-echo
Generate the Browser Client
Return to the directory containing echo.proto. Initialize the client project and install pinned code-generation and runtime dependencies:
npm init -y
npm install --save-exact grpc-web@2.1.1 google-protobuf@4.0.2
npm install --save-dev --save-exact \
@protocolbuffers/protoc-gen-js@4.0.2 \
esbuild@0.25.9
Generate the protobuf message classes and gRPC-Web client stubs. Text mode is required for the server-streaming request used in this example:
protoc -I=. \
--plugin=protoc-gen-js=./node_modules/.bin/protoc-gen-js \
--js_out=import_style=commonjs:. \
--grpc-web_out=import_style=commonjs,mode=grpcwebtext:. \
echo.proto
Create the browser client:
const { EchoServiceClient } = require('./echo_grpc_web_pb');
const { EchoRequest, ServerStreamingEchoRequest } = require('./echo_pb');
const output = document.querySelector('#output');
const log = (message) => {
output.textContent += `${message}\n`;
};
const client = new EchoServiceClient('http://127.0.0.1:9080/grpc/web');
const unaryRequest = new EchoRequest();
unaryRequest.setMessage('hello from unary');
client.echo(unaryRequest, {}, (error, response) => {
if (error) {
log(`unary error: ${error.message}`);
return;
}
log(`unary: ${response.getMessage()}`);
});
const streamRequest = new ServerStreamingEchoRequest();
streamRequest.setMessage('hello from stream');
streamRequest.setMessageCount(3);
streamRequest.setMessageInterval(10);
const stream = client.serverStreamingEcho(streamRequest, {});
stream.on('data', (response) => log(`stream: ${response.getMessage()}`));
stream.on('status', (status) => log(`stream status: ${status.code}`));
stream.on('error', (error) => log(`stream error: ${error.message}`));
Create a page that displays the responses:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>gRPC-Web example</title>
</head>
<body>
<pre id="output"></pre>
<script src="bundle.js"></script>
</body>
</html>
Bundle the browser client:
./node_modules/.bin/esbuild client.js --bundle --outfile=bundle.js
Proxy gRPC-Web with a Prefix Route
A prefix route can proxy every method in the Echo service through one gateway route. Configure the route for the gateway environment:
- Admin API
- ADC
- Ingress Controller
curl "http://127.0.0.1:9180/apisix/admin/routes/grpc-web-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"uri": "/grpc/web/*",
"plugins": {
"grpc-web": {}
},
"upstream": {
"scheme": "grpc",
"type": "roundrobin",
"nodes": {
"grpc-web-echo:9090": 1
}
}
}'
❶ Match every RPC path under the base path used by the browser client.
❷ Enable gRPC-Web translation.
❸ Use native gRPC for the upstream connection.
❹ Send requests to the Echo service on the shared Docker network.
services:
- name: grpc-web-echo
labels:
docs-example: grpc-web
routes:
- name: grpc-web-route
uris:
- /grpc/web/*
plugins:
grpc-web: {}
upstream:
scheme: grpc
type: roundrobin
nodes:
- host: grpc-web-echo
port: 9090
weight: 1
❶ Match every RPC path under the base path used by the browser client.
❷ Enable gRPC-Web translation.
❸ Use native gRPC for the upstream connection.
❹ Send requests to the Echo service on the shared Docker network.
ADC reconciles services as desired state. The label selector limits this example to its own labeled resources. Preview the scoped changes and confirm that they contain no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=grpc-web
Synchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=grpc-web
- Gateway API
- APISIX CRD
The Gateway API GRPCRoute matches native gRPC service and method paths rather than the browser client's base path. A rule without method matches accepts every RPC path. Change the browser client URL to http://127.0.0.1:9080 without /grpc/web, then rebuild bundle.js for this configuration.
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: grpc-web-plugin-config
spec:
plugins:
- name: grpc-web
config: {}
---
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
namespace: aic
name: grpc-web-route
spec:
parentRefs:
- name: apisix
rules:
- filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: grpc-web-plugin-config
backendRefs:
- name: grpc-web-echo
port: 9090
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: grpc-web-echo
spec:
ingressClassName: apisix
scheme: grpc
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: grpc-web-route
spec:
ingressClassName: apisix
http:
- name: grpc-web-route
match:
paths:
- /grpc/web/*
backends:
- serviceName: grpc-web-echo
servicePort: 9090
plugins:
- name: grpc-web
enable: true
config: {}
Apply the configuration:
kubectl apply -f grpc-web-ic.yaml
The /grpc/web/* route removes the matched prefix before forwarding the native gRPC method path. For example, /grpc/web/grpc.gateway.testing.EchoService/Echo is forwarded as /grpc.gateway.testing.EchoService/Echo. A broader route such as /grpc/* leaves web/ in the upstream path and causes an unknown service response.
Serve the client directory and open http://127.0.0.1:8088 in a browser:
python3 -m http.server 8088
The unary result and three streamed messages should appear. Their order can vary:
unary: hello from unary
stream: hello from stream
stream: hello from stream
stream: hello from stream
stream status: 0
Proxy One gRPC-Web Method with an Absolute Route
Use an absolute route when only one RPC method should be exposed through the gateway. Because the browser client includes /grpc/web before the native gRPC method path, proxy-rewrite removes that base path before the request reaches the Echo service.
This example replaces the prefix route and exposes only the unary Echo method. Keep the browser client base URL as http://127.0.0.1:9080/grpc/web.
- Admin API
- ADC
- Ingress Controller
curl "http://127.0.0.1:9180/apisix/admin/routes/grpc-web-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"uri": "/grpc/web/grpc.gateway.testing.EchoService/Echo",
"plugins": {
"grpc-web": {},
"proxy-rewrite": {
"uri": "/grpc.gateway.testing.EchoService/Echo",
"set_ngx_uri": true
}
},
"upstream": {
"scheme": "grpc",
"type": "roundrobin",
"nodes": {
"grpc-web-echo:9090": 1
}
}
}'
❶ Match the browser-facing path for the unary Echo method.
❷ Rewrite the request to the native gRPC method path expected by the upstream service.
❸ Update the NGINX URI used by the gRPC-Web plugin after the rewrite.
services:
- name: grpc-web-echo
labels:
docs-example: grpc-web
routes:
- name: grpc-web-route
uris:
- /grpc/web/grpc.gateway.testing.EchoService/Echo
plugins:
grpc-web: {}
proxy-rewrite:
uri: /grpc.gateway.testing.EchoService/Echo
set_ngx_uri: true
upstream:
scheme: grpc
type: roundrobin
nodes:
- host: grpc-web-echo
port: 9090
weight: 1
❶ Match the browser-facing path for the unary Echo method.
❷ Rewrite the request to the native gRPC method path expected by the upstream service.
❸ Update the NGINX URI used by the gRPC-Web plugin after the rewrite.
Preview the label-scoped service changes:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=grpc-web
Synchronize the reviewed configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=grpc-web
- Gateway API
- APISIX CRD
A GRPCRoute creates the native gRPC method path directly. Change the browser client URL to http://127.0.0.1:9080 without /grpc/web, then rebuild bundle.js. No rewrite is required.
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: grpc-web-plugin-config
spec:
plugins:
- name: grpc-web
config: {}
---
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
namespace: aic
name: grpc-web-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- method:
service: grpc.gateway.testing.EchoService
method: Echo
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: grpc-web-plugin-config
backendRefs:
- name: grpc-web-echo
port: 9090
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: grpc-web-echo
spec:
ingressClassName: apisix
scheme: grpc
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: grpc-web-route
spec:
ingressClassName: apisix
http:
- name: grpc-web-route
match:
paths:
- /grpc/web/grpc.gateway.testing.EchoService/Echo
backends:
- serviceName: grpc-web-echo
servicePort: 9090
plugins:
- name: grpc-web
enable: true
config: {}
- name: proxy-rewrite
enable: true
config:
uri: /grpc.gateway.testing.EchoService/Echo
set_ngx_uri: true
Apply the configuration:
kubectl apply -f grpc-web-ic.yaml
Refresh http://127.0.0.1:8088 in the browser. The Echo response should succeed. The streaming request should fail because its method path is not included in the absolute route.