grpc-web
浏览器网络 API 不公开原生 gRPC 所需的 HTTP/2 帧,因此浏览器无法直接调用原生 gRPC 服务。gRPC-Web 定义了浏览器兼容的协议,支持一元调用和服务端流式调用。
grpc-web 插件将 gRPC-Web 请求转换为原生 gRPC 调用并转发到上游 gRPC 服务。插件还会将响应转换回 gRPC-Web 格式,以供 浏览器应用使用。
请求处理
插件接受用于 RPC 调用的 POST 请求和用于 CORS 预检的 OPTIONS 请求,并识别以下内容类型:
application/grpc-webapplication/grpc-web-textapplication/grpc-web+protoapplication/grpc-web-text+proto
官方 gRPC-Web 浏览器客户端的二进制模式支持一元调用,而 Base64 编码的文本模式支持一元调用和服务端流式调用。插件解码请求,将其作为原生 gRPC 转发,并按照客户端请求的格式编码上游响应。
对于跨域请求,插件默认允许所有来源和 POST 请求。插件接受 content-type、x-grpc-web 和 x-user-agent 请求头,并公开 grpc-status 和 grpc-message 响应头。使用 cors_allow_headers 可扩展允许的请求头。
有关协议层面的详细信息,请参阅 gRPC-Web 协议规范和浏览器功能支持。
示例
以下示例配置浏览器客户端,通过网关向官方 gRPC-Web Echo 服务发送一元请求和服务端流式请求。
运行 APISIX 或 API7 网关。Admin API 和 ADC 示例使用 Docker;Kubernetes 示例使用已部署 API7 Ingress Controller 的 Kubernetes 集群。
在 PATH 中安装 Protocol Buffers 编译器 35.0 和 protoc-gen-grpc-web 2.0.2。如果使用 ADC 配置,请安装 ADC 0.30.4 或更高版本。
客户端使用 2.0.2 版本中的官方 gRPC-Web Echo protobuf 和服务器实现。确认编译器和 gRPC-Web 生成器可用:
protoc --version
protoc-gen-grpc-web --version
命令应分别输出 libprotoc 35.0 和 protoc-gen-grpc-web 2.0.2。
启动 Echo 服务
为 Echo 服务创建目录,并从固定的 gRPC-Web 发布提交中下载 protobuf 和服务器实现:
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"
创建一个 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"]
构建镜像:
docker build -t grpc-web-echo:2.0.2 .
在网关所使用的环境中启动服务:
- Docker
- Kubernetes
将 GATEWAY_CONTAINER 设置为正在运行的 APISIX 或 API7 网关容器。创建专用网络并将网关连接到该网络:
export GATEWAY_CONTAINER=replace-with-gateway-container-name
docker network create gateway-grpc-web-net
docker network connect gateway-grpc-web-net "$GATEWAY_CONTAINER"
在同一网络中启动 Echo 服务:
docker run -d --name grpc-web-echo \
--network gateway-grpc-web-net \
grpc-web-echo:2.0.2
使集群能够使用本地构建的镜像。对于本地 kind 集群,可直接加载镜像:
kind load docker-image grpc-web-echo:2.0.2
对于其他集群,请将镜像推送到集群可访问的镜像仓库,并将清单中的 grpc-web-echo:2.0.2 替换为该镜像引用。
创建 Echo 服务:
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
应用清单并等待 Deployment 就绪:
kubectl apply -f grpc-web-echo.yaml
kubectl rollout status -n aic deployment/grpc-web-echo
生成浏览器客户端
返回包含 echo.proto 的目录。初始化客户端项目并安装固定版本的代码生成和运行时依赖:
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
生成 protobuf 消息类和 gRPC-Web 客户端存根。本示例中的服务端流式请求需要使用文本模式:
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
创建浏览器客户端:
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}`));
创建用于显示响应的页面:
<!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>
打包浏览器客户端:
./node_modules/.bin/esbuild client.js --bundle --outfile=bundle.js
使用前缀路由代理 gRPC-Web
前缀路由可通过一条网关路由代理 Echo 服务中的所有方法。请为网关环境配置路由:
- 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
}
}
}'
❶ 匹配浏览器客户端所用基础路径下的每个 RPC 路径。
❷ 启用 gRPC-Web 转换。
❸ 使用原生 gRPC 连接上游。
❹ 将请求发送到共享 Docker 网络中的 Echo 服务。
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
❶ 匹配浏览器客户端所用基础路径下的每个 RPC 路径。
❷ 启用 gRPC-Web 转换。
❸ 使用原生 gRPC 连接上游。
❹ 将请求发送到共享 Docker 网络中的 Echo 服务。
ADC 将服务协调为所需状态。标签选择 器将本示例限定为其自身带有标签的资源。预览限定范围内的变更,并确认其中不包含非预期的更新或删除:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=grpc-web
同步已检查的服务配置:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=grpc-web
- Gateway API
- APISIX CRD
Gateway API GRPCRoute 匹配原生 gRPC 服务和方法路径,而不是浏览器客户端的基础路径。没有方法匹配条件的规则会接受所有 RPC 路径。对于此配置,请将浏览器客户端 URL 改为不包含 /grpc/web 的 http://127.0.0.1:9080,然后重新构建 bundle.js。
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: {}
应用配置:
kubectl apply -f grpc-web-ic.yaml
/grpc/web/* 路由会先移除匹配的前缀,再转发原生 gRPC 方法路径。例如,/grpc/web/grpc.gateway.testing.EchoService/Echo 会以 /grpc.gateway.testing.EchoService/Echo 转发。使用 /grpc/* 之类更宽泛的路由会在上游路径中留下 web/,从而导致 unknown service 响应。
提供客户端目录,并在浏览器中打开 http://127.0.0.1:8088:
python3 -m http.server 8088
页面应显示一元调用结果和三条流式消息,显示顺序可能不同:
unary: hello from unary
stream: hello from stream
stream: hello from stream
stream: hello from stream
stream status: 0
使用绝对路由代理单个 gRPC-Web 方法
仅需通过网关公开一个 RPC 方法时,请使用绝对路由。由于浏览器客户端会在原生 gRPC 方法路径前添加 /grpc/web,因此 proxy-rewrite 会在请求到达 Echo 服务前移除该基础路径。
此示例替换前缀路由,并且仅公开一元 Echo 方法。将浏览器客户端基础 URL 保持为 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
}
}
}'
❶ 匹配一元 Echo 方法面向浏览器的路径。
❷ 将请求重写为上游服务所需的原生 gRPC 方法路径。
❸ 重写后,更新 gRPC-Web 插件使用的 NGINX URI。
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
❶ 匹配一元 Echo 方法面向浏览器的路径。
❷ 将请求重写为上游服务所需的原生 gRPC 方法路径。
❸ 重写后,更新 gRPC-Web 插件使用的 NGINX URI。
预览标签限定范围内的服务变更:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=grpc-web
同步已检查的配置:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=grpc-web
- Gateway API
- APISIX CRD
GRPCRoute 会直接创建原生 gRPC 方法路径。请将浏览器客户端 URL 改为不包含 /grpc/web 的 http://127.0.0.1:9080,然后重新构建 bundle.js。此配置不需要重写。
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
应用配置:
kubectl apply -f grpc-web-ic.yaml
在浏览器中刷新 http://127.0.0.1:8088。Echo 响应应成功。流式请求应失败,因为绝对路由中未包含其方法路径。