如何让 grpc-web 请求通过两层 NGINX 正常返回?
结论
两层 NGINX 都要为 /my.EndPoint/ 配置单独的代理路由,并在转发时保留原始 URI。最外层 NGINX 处理 CORS 和预检请求。两个代理层都不应覆盖后端返回的 Content-Type 和 Content-Length,建议同时关闭响应缓冲,避免干扰 grpc-web 的流式响应。
下面两项不能用于正常的 POST 响应:
add_header Content-Type 'text/plain charset=UTF-8';
add_header Content-Length 0;
Content-Length: 0 表示响应体为空。即使后端已经返回数据,grpc-web 客户端也可能无法收到。这些设置最多只能用于 OPTIONS 预检响应。
推荐配置
第一层 NGINX:浏览器入口
第一层直接匹配 grpc-web 的服务路径,再将请求原样转发到第二层:
location /my.EndPoint/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "$http_origin" always;
add_header Access-Control-Allow-Methods "POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type,X-Grpc-Web,X-User-Agent,Grpc-Timeout" always;
add_header Access-Control-Max-Age 1728000 always;
add_header Content-Length 0;
return 204;
}
add_header Access-Control-Allow-Origin "$http_origin" always;
add_header Access-Control-Expose-Headers "Grpc-Status,Grpc-Message,Grpc-Status-Details-Bin" always;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
proxy_buffering off;
proxy_request_buffering off;
proxy_pass http://192.168.0.xxx:81;
}
这里的 proxy_pass 没有附加 URI,所以会保留原始请求路径。例如:
/my.EndPoint/SomeMethod
请求转发到第二层后,路径仍然是:
/my.EndPoint/SomeMethod
如果要在 proxy_pass 中明确写出路径,需要特别注意末尾的斜杠:
proxy_pass http://192.168.0.xxx:81/my.EndPoint/;
这种写法可以用于当前的 location,但更容易受到 NGINX 路径替换规则的影响。只需要保留原始 URI 时,优先使用不带 URI 部分的写法。
第二层 NGINX:Docker 内部代理
第二层继续匹配相同的服务路径,然后把请求转发到 tonic HTTP/1 服务:
location /my.EndPoint/ {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
proxy_buffering off;
proxy_request_buffering off;
proxy_pass http://hostname:50051;
}
这样配置后,请求路径不会在任何一层被删除,也不会被重复拼接。
如果后端监听的实际路径是 /my.EndPoint/SomeMethod,不要使用下面这种容易造成路径重复的配置:
location /my.EndPoint/ {
proxy_pass http://hostname:50051/my.EndPoint/;
}
它在当前路径下可能得到相同结果,但只要 location 或上游路径发生变化,NGINX 的 URI 替换规则就可能导致方法路径出错。
为什么使用 location / 容易出问题
理论上,下面的配置也会保留原始 URI:
location / {
proxy_pass http://192.168.0.xxx:81;
}
但它会代理这个虚拟主机下的所有请求,还可能受到其他 location、静态资源规则、重写规则或默认响应头配置的影响。为 grpc-web 单独配置路径规则,可以减少请求被其他规则优先匹配的情况:
location ^~ /my.EndPoint/ {
proxy_pass http://192.168.0.xxx:81;
}
^~ 并非必需。只有配置中还存在可能处理该请求的正则 location 时,才需要使用。
还需要检查的事项
-
不要强制修改正常响应的类型。grpc-web 常见的响应类型包括:
application/grpc-web application/grpc-web+proto application/grpc-web-text application/grpc-web-text+proto -
不要为正常响应添加
Content-Length: 0。应让 NGINX 保留后端生成的长度或分块传输方式。 -
浏览器只与第一层 NGINX 进行跨域交互,因此 CORS 通常只需配置在第一层。如果第二层也添加 CORS 响应头,要避免重复返回
Access-Control-Allow-Origin。 -
grpc-web 通过 HTTP/1.1 代理时应使用
proxy_pass,而不是grpc_pass。grpc_pass用于原生 gRPC 的 HTTP/2 请求,不能直接替代 grpc-web 的协议转换。 -
如果请求使用 cookie 或其他认证凭据,
Access-Control-Allow-Origin不能设为*。应返回经过白名单校验的明确 Origin,并添加:add_header Access-Control-Allow-Credentials "true" always; -
修改配置后,分别检查每一层收到的完整路径和返回的响应头,确认没有出现:
Content-Length: 0 Content-Type: text/plain
两层代理都应明确匹配 /my.EndPoint/,保留完整的 RPC 方法路径,使用 HTTP/1.1 转发,同时避免覆盖 grpc-web 的实际响应头和响应体。
备注:内容仅供参考。
