diff --git a/apisix/plugins/grpc-web.lua b/apisix/plugins/grpc-web.lua index 18465063b343..5771604e74f0 100644 --- a/apisix/plugins/grpc-web.lua +++ b/apisix/plugins/grpc-web.lua @@ -21,6 +21,8 @@ local req_set_uri = ngx.req.set_uri local req_set_body_data = ngx.req.set_body_data local decode_base64 = ngx.decode_base64 local encode_base64 = ngx.encode_base64 +local bit = require("bit") +local string = string local ALLOW_METHOD_OPTIONS = "OPTIONS" @@ -87,7 +89,7 @@ function _M.access(conf, ctx) -- set grpc path 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") + "`prefix matching` pattern routing") return 400 end @@ -130,6 +132,7 @@ function _M.header_filter(conf, ctx) 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", "grpc-message,grpc-status") end function _M.body_filter(conf, ctx) @@ -147,6 +150,50 @@ function _M.body_filter(conf, ctx) chunk = encode_base64(chunk) 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 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) + end + + -- clear trailer + ctx.var.upstream_trailer_grpc_status = nil + ctx.var.upstream_trailer_grpc_message = nil + end end return _M diff --git a/t/plugin/grpc-web.t b/t/plugin/grpc-web.t index 7340add60fce..7069a8c2ccb8 100644 --- a/t/plugin/grpc-web.t +++ b/t/plugin/grpc-web.t @@ -68,25 +68,33 @@ passed -=== TEST 2: Proxy unary request using APISIX gRPC-Web plugin +=== TEST 2: Proxy unary request using APISIX with trailers gRPC-Web plugin --- 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"} -=== TEST 3: Proxy server-side streaming request using APISIX gRPC-Web plugin +=== TEST 3: Proxy server-side streaming request using APISIX with trailers gRPC-Web plugin --- exec node ./t/plugin/grpc-web/client.js BIN STREAM node ./t/plugin/grpc-web/client.js TEXT STREAM --- response_body {"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: {} } @@ -227,3 +235,28 @@ Content-Type: application/grpc-web --- response_headers Access-Control-Allow-Origin: http://test.com Content-Type: application/grpc-web + + + +=== 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 + + + +=== TEST 12: verify trailers in response +--- exec +curl -iv --location 'http://127.0.0.1:1984/grpc/web/a6.RouteService/GetRoute' \ +--header 'Content-Type: application/grpc-web+proto' \ +--header 'X-Grpc-Web: 1' \ +--data-binary '@./t/plugin/grpc-web/req.bin' +--- response_body eval +qr/grpc-status:0\x0d\x0agrpc-message:/ diff --git a/t/plugin/grpc-web/client.js b/t/plugin/grpc-web/client.js index 3f10a80bc100..9ec044124c95 100644 --- a/t/plugin/grpc-web/client.js +++ b/t/plugin/grpc-web/client.js @@ -49,6 +49,8 @@ class gRPCWebClient { return } console.log(JSON.stringify(response.toObject())); + }).on("status", function (status) { + console.log("Status:", status); }); } @@ -62,6 +64,10 @@ class gRPCWebClient { stream.on('end', function(end) { stream.cancel(); }); + + stream.on("status", function (status) { + console.log("Status:", status); + }); } } diff --git a/t/plugin/grpc-web/req.bin b/t/plugin/grpc-web/req.bin new file mode 100644 index 000000000000..908c829c608e Binary files /dev/null and b/t/plugin/grpc-web/req.bin differ