Skip to main content

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-web
  • application/grpc-web-text
  • application/grpc-web+proto
  • application/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:

Dockerfile
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:

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

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:

client.js
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:

index.html
<!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:

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.

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.

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.

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.