This is an automated email from the ASF dual-hosted git repository.
juzhiyuan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new 2a5425f6f fix(grpc-web): response contains two trailer chunks (#11988)
2a5425f6f is described below
commit 2a5425f6f09f418ce10bfb3698518eb386ea0332
Author: Zeping Bai <[email protected]>
AuthorDate: Tue Feb 25 11:21:41 2025 +0800
fix(grpc-web): response contains two trailer chunks (#11988)
---
apisix/plugins/grpc-web.lua | 116 ++++++++++++++++++++++++++------------------
t/plugin/grpc-web.t | 51 ++++++++++---------
2 files changed, 94 insertions(+), 73 deletions(-)
diff --git a/apisix/plugins/grpc-web.lua b/apisix/plugins/grpc-web.lua
index 260e84c4e..d69d3e665 100644
--- a/apisix/plugins/grpc-web.lua
+++ b/apisix/plugins/grpc-web.lua
@@ -68,6 +68,46 @@ function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
+local function exit(ctx, status)
+ ctx.grpc_web_skip_body_filter = true
+ return status
+end
+
+--- Build gRPC-Web trailer chunk
+-- grpc-web trailer format reference:
+--
envoyproxy/envoy/source/extensions/filters/http/grpc_web/grpc_web_filter.cc
+--
+-- Format for grpc-web trailer
+-- 1 byte: 0x80
+-- 4 bytes: length of the trailer
+-- n bytes: trailer
+-- It using upstream_trailer_* variables from nginx, it is available since
NGINX version 1.13.10
+--
https://nginx.org/en/docs/http/ngx_http_upstream_module.html#var_upstream_trailer_
+--
+-- @param grpc_status number grpc status code
+-- @param grpc_message string grpc message
+-- @return string grpc-web trailer chunk in raw string
+local build_trailer = function (grpc_status, grpc_message)
+ local status_str = "grpc-status:" .. grpc_status
+ local status_msg = "grpc-message:" .. ( grpc_message or "")
+ local grpc_web_trailer = status_str .. "\r\n" .. status_msg .. "\r\n"
+ local len = #grpc_web_trailer
+
+ -- 1 byte: 0x80
+ local trailer_buf = string.char(0x80)
+ -- 4 bytes: length of the trailer
+ trailer_buf = trailer_buf .. string.char(
+ bit.band(bit.rshift(len, 24), 0xff),
+ bit.band(bit.rshift(len, 16), 0xff),
+ bit.band(bit.rshift(len, 8), 0xff),
+ bit.band(len, 0xff)
+ )
+ -- n bytes: trailer
+ trailer_buf = trailer_buf .. grpc_web_trailer
+
+ return trailer_buf
+end
+
function _M.access(conf, ctx)
-- set context variable mime
-- When processing non gRPC Web requests, `mime` can be obtained in the
context
@@ -76,19 +116,19 @@ function _M.access(conf, ctx)
local method = core.request.get_method()
if method == ALLOW_METHOD_OPTIONS then
- return 204
+ return exit(ctx, 204)
end
if method ~= ALLOW_METHOD_POST then
--
https://github.com/grpc/grpc-web/blob/master/doc/browser-features.md#cors-support
core.log.error("request method: `", method, "` invalid")
- return 400
+ return exit(ctx, 405)
end
local encoding = grpc_web_content_encoding[ctx.grpc_web_mime]
if not encoding then
core.log.error("request Content-Type: `", ctx.grpc_web_mime, "`
invalid")
- return 400
+ return exit(ctx, 400)
end
-- set context variable encoding method
@@ -98,7 +138,7 @@ function _M.access(conf, ctx)
if not (ctx.curr_req_matched and ctx.curr_req_matched[":ext"]) then
core.log.error("routing configuration error, grpc-web plugin only
supports ",
"`prefix matching` pattern routing")
- return 400
+ return exit(ctx, 400)
end
local path = ctx.curr_req_matched[":ext"]
@@ -110,16 +150,17 @@ function _M.access(conf, ctx)
-- set grpc body
local body, err = core.request.get_body()
- if err then
+ if err or not body then
core.log.error("failed to read request body, err: ", err)
- return 400
+ return exit(ctx, 400)
end
if encoding == CONTENT_ENCODING_BASE64 then
body = decode_base64(body)
+ ngx.log(ngx.WARN, "DECODE BODY: ", body)
if not body then
core.log.error("failed to decode request body")
- return 400
+ return exit(ctx, 400)
end
end
@@ -139,11 +180,19 @@ function _M.header_filter(conf, ctx)
if not ctx.cors_allow_origins then
core.response.set_header("Access-Control-Allow-Origin",
DEFAULT_CORS_ALLOW_ORIGIN)
end
- core.response.set_header("Content-Type", ctx.grpc_web_mime)
core.response.set_header("Access-Control-Expose-Headers",
DEFAULT_CORS_EXPOSE_HEADERS)
+
+ if not ctx.grpc_web_skip_body_filter then
+ core.response.set_header("Content-Type", ctx.grpc_web_mime)
+ core.response.set_header("Content-Length", nil)
+ end
end
function _M.body_filter(conf, ctx)
+ if ctx.grpc_web_skip_body_filter then
+ return
+ end
+
-- If the MIME extension type description of the gRPC-Web standard is not
obtained,
-- indicating that the request is not based on the gRPC Web specification,
-- the processing of the request body will be ignored
@@ -159,48 +208,21 @@ function _M.body_filter(conf, ctx)
ngx_arg[1] = chunk
end
- --[[
- upstream_trailer_* available since NGINX version 1.13.10 :
-
https://nginx.org/en/docs/http/ngx_http_upstream_module.html#var_upstream_trailer_
-
- grpc-web trailer format reference:
- envoyproxy/envoy/source/extensions/filters/http/grpc_web/grpc_web_filter.cc
-
- Format for grpc-web trailer
- 1 byte: 0x80
- 4 bytes: length of the trailer
- n bytes: trailer
-
- --]]
- local status = ctx.var.upstream_trailer_grpc_status
- local message = ctx.var.upstream_trailer_grpc_message
- if status ~= "" and status ~= nil then
- local status_str = "grpc-status:" .. status
- local status_msg = "grpc-message:" .. ( message or "")
- local grpc_web_trailer = status_str .. "\r\n" .. status_msg .. "\r\n"
- local len = #grpc_web_trailer
-
- -- 1 byte: 0x80
- local trailer_buf = string.char(0x80)
- -- 4 bytes: length of the trailer
- trailer_buf = trailer_buf .. string.char(
- bit.band(bit.rshift(len, 24), 0xff),
- bit.band(bit.rshift(len, 16), 0xff),
- bit.band(bit.rshift(len, 8), 0xff),
- bit.band(len, 0xff)
- )
- -- n bytes: trailer
- trailer_buf = trailer_buf .. grpc_web_trailer
+ if ngx_arg[2] then -- if eof
+ local status = ctx.var.upstream_trailer_grpc_status
+ local message = ctx.var.upstream_trailer_grpc_message
- if ctx.grpc_web_encoding == CONTENT_ENCODING_BINARY then
- ngx_arg[1] = ngx_arg[1] .. trailer_buf
- else
- ngx_arg[1] = ngx_arg[1] .. encode_base64(trailer_buf)
+ -- When the response body completes and still does not receive the
grpc status
+ local resp_ok = status ~= nil and status ~= ""
+ local trailer_buf = build_trailer(
+ resp_ok and status or 2,
+ resp_ok and message or "upstream grpc status not received"
+ )
+ if ctx.grpc_web_encoding == CONTENT_ENCODING_BASE64 then
+ trailer_buf = encode_base64(trailer_buf)
end
- -- clear trailer
- ctx.var.upstream_trailer_grpc_status = nil
- ctx.var.upstream_trailer_grpc_message = nil
+ ngx_arg[1] = ngx_arg[1] .. trailer_buf
end
end
diff --git a/t/plugin/grpc-web.t b/t/plugin/grpc-web.t
index 1d3a3647e..ac8f689ee 100644
--- a/t/plugin/grpc-web.t
+++ b/t/plugin/grpc-web.t
@@ -69,15 +69,14 @@ passed
=== TEST 2: Proxy unary request using APISIX with trailers gRPC-Web plugin
+Status should be printed at most once per request, otherwise this would be out
of specification.
--- exec
node ./t/plugin/grpc-web/client.js BIN UNARY
node ./t/plugin/grpc-web/client.js TEXT UNARY
--- response_body
Status: { code: 0, details: '', metadata: {} }
-Status: { code: 0, details: '', metadata: {} }
{"name":"hello","path":"/hello"}
Status: { code: 0, details: '', metadata: {} }
-Status: { code: 0, details: '', metadata: {} }
{"name":"hello","path":"/hello"}
@@ -90,11 +89,9 @@ node ./t/plugin/grpc-web/client.js TEXT STREAM
{"name":"hello","path":"/hello"}
{"name":"world","path":"/world"}
Status: { code: 0, details: '', metadata: {} }
-Status: { code: 0, details: '', metadata: {} }
{"name":"hello","path":"/hello"}
{"name":"world","path":"/world"}
Status: { code: 0, details: '', metadata: {} }
-Status: { code: 0, details: '', metadata: {} }
@@ -112,7 +109,7 @@ Access-Control-Allow-Origin: *
=== TEST 5: test non-options request
--- request
GET /grpc/web/a6.RouteService/GetRoute
---- error_code: 400
+--- error_code: 405
--- response_headers
Access-Control-Allow-Origin: *
--- error_log
@@ -128,7 +125,7 @@ Content-Type: application/json
--- error_code: 400
--- response_headers
Access-Control-Allow-Origin: *
-Content-Type: application/json
+Content-Type: text/html
--- error_log
request Content-Type: `application/json` invalid
@@ -177,7 +174,7 @@ Content-Type: application/grpc-web
--- error_code: 400
--- response_headers
Access-Control-Allow-Origin: *
-Content-Type: application/grpc-web
+Content-Type: text/html
--- error_log
routing configuration error, grpc-web plugin only supports `prefix matching`
pattern routing
@@ -226,33 +223,35 @@ passed
=== TEST 10: don't override Access-Control-Allow-Origin header in response
---- request
-POST /grpc/web/a6.RouteService/GetRoute
-{}
---- more_headers
-Origin: http://test.com
-Content-Type: application/grpc-web
---- response_headers
-Access-Control-Allow-Origin: http://test.com
-Content-Type: application/grpc-web
+--- exec
+curl -iv --location 'http://127.0.0.1:1984/grpc/web/a6.RouteService/GetRoute' \
+--header 'Origin: http://test.com' \
+--header 'Content-Type: application/grpc-web-text' \
+--data-raw 'AAAAAAcKBXdvcmxkCgo='
+--- response_body eval
+qr/HTTP\/1.1 200 OK/ and qr/Access-Control-Allow-Origin: http:\/\/test.com/
=== TEST 11: check for Access-Control-Expose-Headers header in response
---- request
-POST /grpc/web/a6.RouteService/GetRoute
-{}
---- more_headers
-Origin: http://test.com
-Content-Type: application/grpc-web
---- response_headers
-Access-Control-Allow-Origin: http://test.com
-Access-Control-Expose-Headers: grpc-message,grpc-status
-Content-Type: application/grpc-web
+--- exec
+curl -iv --location 'http://127.0.0.1:1984/grpc/web/a6.RouteService/GetRoute' \
+--header 'Origin: http://test.com' \
+--header 'Content-Type: application/grpc-web-text' \
+--data-raw 'AAAAAAcKBXdvcmxkCgo='
+--- response_body eval
+qr/Access-Control-Expose-Headers: grpc-message,grpc-status/ and
qr/Access-Control-Allow-Origin: http:\/\/test.com/
=== TEST 12: verify trailers in response
+According to the gRPC documentation, the grpc-web proxy should not retain
trailers received from upstream when
+forwarding them, as the reference implementation envoy does, so the current
test case is status quo rather
+than "correct", which is not expected to have an impact since browsers ignore
trailers.
+Currently there is no API or hook point available in nginx/lua-nginx-module to
remove specified trailers
+on demand (grpc_hide_header can do it but it affects the grpc proxy), and some
nginx patches may be needed
+to allow for code-controlled removal of the trailer at runtime.
+When we implement that, this use case will be removed.
--- exec
curl -iv --location 'http://127.0.0.1:1984/grpc/web/a6.RouteService/GetRoute' \
--header 'Content-Type: application/grpc-web+proto' \