Skip to content

Commit 0bea3db

Browse files
authored
fix(grpc-web): missing trailers issue (#10851)
1 parent 946a17e commit 0bea3db

File tree

4 files changed

+89
-3
lines changed

4 files changed

+89
-3
lines changed

apisix/plugins/grpc-web.lua

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ local req_set_uri = ngx.req.set_uri
2121
local req_set_body_data = ngx.req.set_body_data
2222
local decode_base64 = ngx.decode_base64
2323
local encode_base64 = ngx.encode_base64
24+
local bit = require("bit")
25+
local string = string
2426

2527

2628
local ALLOW_METHOD_OPTIONS = "OPTIONS"
@@ -87,7 +89,7 @@ function _M.access(conf, ctx)
8789
-- set grpc path
8890
if not (ctx.curr_req_matched and ctx.curr_req_matched[":ext"]) then
8991
core.log.error("routing configuration error, grpc-web plugin only supports ",
90-
"`prefix matching` pattern routing")
92+
"`prefix matching` pattern routing")
9193
return 400
9294
end
9395

@@ -130,6 +132,7 @@ function _M.header_filter(conf, ctx)
130132
core.response.set_header("Access-Control-Allow-Origin", DEFAULT_CORS_ALLOW_ORIGIN)
131133
end
132134
core.response.set_header("Content-Type", ctx.grpc_web_mime)
135+
core.response.set_header("Access-Control-Expose-Headers", "grpc-message,grpc-status")
133136
end
134137

135138
function _M.body_filter(conf, ctx)
@@ -147,6 +150,50 @@ function _M.body_filter(conf, ctx)
147150
chunk = encode_base64(chunk)
148151
ngx_arg[1] = chunk
149152
end
153+
154+
--[[
155+
upstream_trailer_* available since NGINX version 1.13.10 :
156+
https://nginx.org/en/docs/http/ngx_http_upstream_module.html#var_upstream_trailer_
157+
158+
grpc-web trailer format reference:
159+
envoyproxy/envoy/source/extensions/filters/http/grpc_web/grpc_web_filter.cc
160+
161+
Format for grpc-web trailer
162+
1 byte: 0x80
163+
4 bytes: length of the trailer
164+
n bytes: trailer
165+
166+
--]]
167+
local status = ctx.var.upstream_trailer_grpc_status
168+
local message = ctx.var.upstream_trailer_grpc_message
169+
if status ~= "" and status ~= nil then
170+
local status_str = "grpc-status:" .. status
171+
local status_msg = "grpc-message:" .. ( message or "")
172+
local grpc_web_trailer = status_str .. "\r\n" .. status_msg .. "\r\n"
173+
local len = #grpc_web_trailer
174+
175+
-- 1 byte: 0x80
176+
local trailer_buf = string.char(0x80)
177+
-- 4 bytes: length of the trailer
178+
trailer_buf = trailer_buf .. string.char(
179+
bit.band(bit.rshift(len, 24), 0xff),
180+
bit.band(bit.rshift(len, 16), 0xff),
181+
bit.band(bit.rshift(len, 8), 0xff),
182+
bit.band(len, 0xff)
183+
)
184+
-- n bytes: trailer
185+
trailer_buf = trailer_buf .. grpc_web_trailer
186+
187+
if ctx.grpc_web_encoding == CONTENT_ENCODING_BINARY then
188+
ngx_arg[1] = ngx_arg[1] .. trailer_buf
189+
else
190+
ngx_arg[1] = ngx_arg[1] .. encode_base64(trailer_buf)
191+
end
192+
193+
-- clear trailer
194+
ctx.var.upstream_trailer_grpc_status = nil
195+
ctx.var.upstream_trailer_grpc_message = nil
196+
end
150197
end
151198

152199
return _M

t/plugin/grpc-web.t

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,25 +68,33 @@ passed
6868
6969
7070
71-
=== TEST 2: Proxy unary request using APISIX gRPC-Web plugin
71+
=== TEST 2: Proxy unary request using APISIX with trailers gRPC-Web plugin
7272
--- exec
7373
node ./t/plugin/grpc-web/client.js BIN UNARY
7474
node ./t/plugin/grpc-web/client.js TEXT UNARY
7575
--- response_body
76+
Status: { code: 0, details: '', metadata: {} }
77+
Status: { code: 0, details: '', metadata: {} }
7678
{"name":"hello","path":"/hello"}
79+
Status: { code: 0, details: '', metadata: {} }
80+
Status: { code: 0, details: '', metadata: {} }
7781
{"name":"hello","path":"/hello"}
7882
7983
8084
81-
=== TEST 3: Proxy server-side streaming request using APISIX gRPC-Web plugin
85+
=== TEST 3: Proxy server-side streaming request using APISIX with trailers gRPC-Web plugin
8286
--- exec
8387
node ./t/plugin/grpc-web/client.js BIN STREAM
8488
node ./t/plugin/grpc-web/client.js TEXT STREAM
8589
--- response_body
8690
{"name":"hello","path":"/hello"}
8791
{"name":"world","path":"/world"}
92+
Status: { code: 0, details: '', metadata: {} }
93+
Status: { code: 0, details: '', metadata: {} }
8894
{"name":"hello","path":"/hello"}
8995
{"name":"world","path":"/world"}
96+
Status: { code: 0, details: '', metadata: {} }
97+
Status: { code: 0, details: '', metadata: {} }
9098
9199
92100
@@ -227,3 +235,28 @@ Content-Type: application/grpc-web
227235
--- response_headers
228236
Access-Control-Allow-Origin: http://test.com
229237
Content-Type: application/grpc-web
238+
239+
240+
241+
=== TEST 11: check for Access-Control-Expose-Headers header in response
242+
--- request
243+
POST /grpc/web/a6.RouteService/GetRoute
244+
{}
245+
--- more_headers
246+
Origin: http://test.com
247+
Content-Type: application/grpc-web
248+
--- response_headers
249+
Access-Control-Allow-Origin: http://test.com
250+
Access-Control-Expose-Headers: grpc-message,grpc-status
251+
Content-Type: application/grpc-web
252+
253+
254+
255+
=== TEST 12: verify trailers in response
256+
--- exec
257+
curl -iv --location 'http://127.0.0.1:1984/grpc/web/a6.RouteService/GetRoute' \
258+
--header 'Content-Type: application/grpc-web+proto' \
259+
--header 'X-Grpc-Web: 1' \
260+
--data-binary '@./t/plugin/grpc-web/req.bin'
261+
--- response_body eval
262+
qr/grpc-status:0\x0d\x0agrpc-message:/

t/plugin/grpc-web/client.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ class gRPCWebClient {
4949
return
5050
}
5151
console.log(JSON.stringify(response.toObject()));
52+
}).on("status", function (status) {
53+
console.log("Status:", status);
5254
});
5355
}
5456

@@ -62,6 +64,10 @@ class gRPCWebClient {
6264
stream.on('end', function(end) {
6365
stream.cancel();
6466
});
67+
68+
stream.on("status", function (status) {
69+
console.log("Status:", status);
70+
});
6571
}
6672
}
6773

t/plugin/grpc-web/req.bin

14 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)