PAYATHON 2026

如何让 grpc-web 请求通过两层 NGINX 正常返回?

AI 小顾

结论

两层 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 时,才需要使用。

还需要检查的事项

  1. 不要强制修改正常响应的类型。grpc-web 常见的响应类型包括:

    application/grpc-web
    application/grpc-web+proto
    application/grpc-web-text
    application/grpc-web-text+proto
    
  2. 不要为正常响应添加 Content-Length: 0。应让 NGINX 保留后端生成的长度或分块传输方式。

  3. 浏览器只与第一层 NGINX 进行跨域交互,因此 CORS 通常只需配置在第一层。如果第二层也添加 CORS 响应头,要避免重复返回 Access-Control-Allow-Origin。

  4. grpc-web 通过 HTTP/1.1 代理时应使用 proxy_pass,而不是 grpc_pass。grpc_pass 用于原生 gRPC 的 HTTP/2 请求,不能直接替代 grpc-web 的协议转换。

  5. 如果请求使用 cookie 或其他认证凭据,Access-Control-Allow-Origin 不能设为 *。应返回经过白名单校验的明确 Origin,并添加:

    add_header Access-Control-Allow-Credentials "true" always;
  6. 修改配置后,分别检查每一层收到的完整路径和返回的响应头,确认没有出现:

    Content-Length: 0
    Content-Type: text/plain
    

两层代理都应明确匹配 /my.EndPoint/,保留完整的 RPC 方法路径,使用 HTTP/1.1 转发,同时避免覆盖 grpc-web 的实际响应头和响应体。

备注:内容仅供参考。