Vue.js+protobuf-ts调用Envoy代理的gRPC-web服务报错排查
问题:Vue3+TS中gRPC-Web调用出现503/连接重置错误
一、Proto文件定义
syntax = "proto3"; package extractor; service Extract { rpc PutEntries (EntriesRequest) returns (EntriesResponse); } message EntriesRequest { repeated string value = 1; } message EntriesResponse { repeated string value = 1; }
二、环境配置
- gRPC服务运行在50051端口
- Envoy代理运行在8080端口,已开启CORS
- 外部客户端(Kreya/BloomRPC)可正常调用,但浏览器中调用报错
- 前端、后端、代理均部署在同一机器
三、初始Vue3+TS组件代码
<script setup lang="ts"> import { ref } from "vue"; import { ExtractClient } from "./grpc/extractor.client"; import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport"; import type { EntriesRequest, EntriesResponse } from "./grpc/extractor"; import type { GrpcWebOptions } from '@protobuf-ts/grpcweb-transport'; const HOST = "http://localhost:8080"; const TIMEOUT = Date.now() + 2000; let extracted = ref(false); const fileInput = ref<HTMLInputElement | null>(null); let filename = ref(""); let data = ref(""); let options: GrpcWebOptions = { baseUrl: HOST, timeout: TIMEOUT, format: 'binary', meta: {} }; const handleFileChange = (event: Event) => { const input = event.target as HTMLInputElement; fileInput.value = input; }; function extract() { if (!fileInput.value) { return; } const file = fileInput.value.files![0]; filename.value = file.name; const reader = new FileReader(); reader.readAsText(file); reader.onload = async () => { const transport = new GrpcWebFetchTransport(options); const client = new ExtractClient(transport); const content = reader.result as string; const entries = content.split("\n").filter((entry) => entry !== ""); // Convert the entries to the EntriesRequest format let request: EntriesRequest = { value: entries } // Make the grpc-web call to the PutEntries endpoint let call = client.putEntries(request, options); let response: EntriesResponse = await call.response; data.value = response.value.join("\n"); extracted.value = true; }; } </script> <template> <form> <input type="file" @change="handleFileChange" /> </form> <button @click="extract">Extract</button> <br /><br /> <span v-show="extracted">Should show data from {{ filename }} <br />{{ data }} </span> </template>
四、更新后的情况
调整超时设置后,错误变为503 Uncaught (in promise) RpcError: Service Unavailable。测试文件小于1KB,外部客户端响应仅需2.1秒,更新后的extract方法代码如下:
function extract() { if (!fileInput.value) { return; } const file = fileInput.value.files![0]; filename.value = file.name; const reader = new FileReader(); reader.readAsText(file); reader.onload = async () => { const content = reader.result as string; const entries = content.split("\n").filter((entry) => entry !== ""); // Convert the entries to the EntriesRequest format let request: EntriesRequest = { value: entries } // Configure Grpc-Web client let options: GrpcWebOptions = { baseUrl: HOST, timeout: Date.now() + 10000, format: 'binary', meta: {} }; const transport = new GrpcWebFetchTransport(options); const client = new ExtractClient(transport); // Make the grpc-web call to the PutEntries endpoint let call = client.putEntries(request, options); let response: EntriesResponse = await call.response; data.value = response.value.join("\n"); extracted.value = true; }; }
排查方向与解决方案
1. Envoy代理配置问题
- 检查路由映射:确认Envoy路由的
prefix是否匹配proto中的服务路径(如/extractor.Extract/),且cluster指向gRPC服务的50051端口。 - 验证gRPC-Web过滤器:确保Envoy配置中启用了
grpc_web过滤器,同时CORS的allow_origin覆盖前端本地开发域名(如http://localhost:5173)。 - 查看Envoy日志:启动Envoy时开启调试日志,排查是否存在路由匹配失败、上游连接异常的具体信息。
2. gRPC-Web客户端配置错误
- 修正超时参数:
@protobuf-ts/grpcweb-transport的timeout参数是毫秒数,不是未来时间戳,应直接写10000(代表10秒),之前的Date.now() + 10000会导致参数无效。 - 移除重复options传递:创建
GrpcWebFetchTransport时已传入配置,调用client.putEntries时无需再次传递options,避免配置冲突。 - 测试格式兼容性:尝试将
format改为text,验证是否因格式不兼容导致调用失败(外部客户端能正常调用则大概率没问题,但可作为排查手段)。
3. 前端请求路径验证
通过浏览器开发者工具的Network面板,检查实际请求URL是否为http://localhost:8080/extractor.Extract/PutEntries,若路径错误,Envoy无法路由到后端服务。
4. 代码优化示例
修正上述问题后的extract方法:
function extract() { if (!fileInput.value) { return; } const file = fileInput.value.files![0]; filename.value = file.name; const reader = new FileReader(); reader.readAsText(file); reader.onload = async () => { const content = reader.result as string; const entries = content.split("\n").filter(entry => entry !== ""); const request: EntriesRequest = { value: entries }; // 修正超时参数,移除冗余配置 const options: GrpcWebOptions = { baseUrl: HOST, timeout: 10000, format: 'binary' }; const transport = new GrpcWebFetchTransport(options); const client = new ExtractClient(transport); try { // 调用时不再重复传递options const call = client.putEntries(request); const response: EntriesResponse = await call.response; data.value = response.value.join("\n"); extracted.value = true; } catch (err) { // 增加错误捕获,方便调试 console.error("gRPC调用失败:", err); } }; }
内容的提问来源于stack exchange,提问作者staticdev
相关产品推荐
相关产品推荐

