This is an automated email from the ASF dual-hosted git repository.

spacewander 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 ddb9cd2  feat(grpc-web): support gRPC-Web Proxy (#5964)
ddb9cd2 is described below

commit ddb9cd28bf33799652d252df0546aa0832933bcf
Author: 帅进超 <[email protected]>
AuthorDate: Sun Jan 9 23:59:32 2022 +0800

    feat(grpc-web): support gRPC-Web Proxy (#5964)
---
 apisix/plugins/grpc-web.lua                | 147 ++++++
 ci/centos7-ci.sh                           |   9 +
 ci/common.sh                               |  12 +
 ci/linux_openresty_common_runner.sh        |   9 +
 conf/config-default.yaml                   |   1 +
 docs/en/latest/config.json                 |   1 +
 docs/en/latest/plugins/grpc-web.md         |  85 ++++
 docs/zh/latest/config.json                 |   1 +
 docs/zh/latest/plugins/grpc-web.md         |  84 ++++
 t/admin/plugins.t                          |   1 +
 t/plugin/grpc-web.t                        | 221 +++++++++
 t/plugin/grpc-web/a6/routes.pb.go          | 513 +++++++++++++++++++
 t/plugin/grpc-web/a6/routes.proto          |  49 ++
 t/plugin/grpc-web/a6/routes_grpc.pb.go     |  97 ++++
 t/plugin/grpc-web/a6/routes_grpc_web_pb.js | 443 +++++++++++++++++
 t/plugin/grpc-web/a6/routes_pb.js          | 766 +++++++++++++++++++++++++++++
 t/plugin/grpc-web/client.js                | 178 +++++++
 t/plugin/grpc-web/go.mod                   |  13 +
 t/plugin/grpc-web/go.sum                   | 125 +++++
 t/plugin/grpc-web/package-lock.json        |  52 ++
 t/plugin/grpc-web/package.json             |   8 +
 t/plugin/grpc-web/server.go                |  53 ++
 t/plugin/grpc-web/setup.sh                 |  25 +
 23 files changed, 2893 insertions(+)

diff --git a/apisix/plugins/grpc-web.lua b/apisix/plugins/grpc-web.lua
new file mode 100644
index 0000000..428b641
--- /dev/null
+++ b/apisix/plugins/grpc-web.lua
@@ -0,0 +1,147 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+local ngx               = ngx
+local ngx_arg           = ngx.arg
+local core              = require("apisix.core")
+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 ALLOW_METHOD_OPTIONS = "OPTIONS"
+local ALLOW_METHOD_POST = "POST"
+local CONTENT_ENCODING_BASE64 = "base64"
+local CONTENT_ENCODING_BINARY = "binary"
+local DEFAULT_CORS_CONTENT_TYPE = "application/grpc-web-text+proto"
+local DEFAULT_CORS_ALLOW_ORIGIN = "*"
+local DEFAULT_CORS_ALLOW_METHODS = ALLOW_METHOD_POST
+local DEFAULT_CORS_ALLOW_HEADERS = "content-type,x-grpc-web,x-user-agent"
+local DEFAULT_PROXY_CONTENT_TYPE = "application/grpc"
+
+
+local plugin_name = "grpc-web"
+
+local schema = {
+    type = "object",
+    properties = {},
+}
+
+local grpc_web_content_encoding = {
+    ["application/grpc-web"] = CONTENT_ENCODING_BINARY,
+    ["application/grpc-web-text"] = CONTENT_ENCODING_BASE64,
+    ["application/grpc-web+proto"] = CONTENT_ENCODING_BINARY,
+    ["application/grpc-web-text+proto"] = CONTENT_ENCODING_BASE64,
+}
+
+local _M = {
+    version = 0.1,
+    priority = 505,
+    name = plugin_name,
+    schema = schema,
+}
+
+function _M.check_schema(conf)
+    return core.schema.check(schema, conf)
+end
+
+function _M.access(conf, ctx)
+    local method = core.request.get_method()
+    if method == ALLOW_METHOD_OPTIONS then
+        return 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
+    end
+
+    local mimetype = core.request.header(ctx, "Content-Type")
+    local encoding = grpc_web_content_encoding[mimetype]
+    if not encoding then
+        core.log.error("request Content-Type: `", mimetype, "` invalid")
+        return 400
+    end
+
+    -- 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")
+        return 400
+    end
+
+    local path = ctx.curr_req_matched[":ext"]
+    if path:byte(1) ~= core.string.byte("/") then
+        path = "/" .. path
+    end
+
+    req_set_uri(path)
+
+    -- set grpc body
+    local body, err = core.request.get_body()
+    if err then
+        core.log.error("failed to read request body, err: ", err)
+        return 400
+    end
+
+    if encoding == CONTENT_ENCODING_BASE64 then
+        body = decode_base64(body)
+        if not body then
+            core.log.error("failed to decode request body")
+            return 400
+        end
+    end
+
+    -- set grpc content-type
+    core.request.set_header(ctx, "Content-Type", DEFAULT_PROXY_CONTENT_TYPE)
+    -- set grpc body
+    req_set_body_data(body)
+
+    -- set context variable
+    ctx.grpc_web_mime = mimetype
+    ctx.grpc_web_encoding = encoding
+end
+
+function _M.header_filter(conf, ctx)
+    local method = core.request.get_method()
+    if method == ALLOW_METHOD_OPTIONS then
+        core.response.set_header("Access-Control-Allow-Methods", 
DEFAULT_CORS_ALLOW_METHODS)
+        core.response.set_header("Access-Control-Allow-Headers", 
DEFAULT_CORS_ALLOW_HEADERS)
+    end
+    core.response.set_header("Access-Control-Allow-Origin", 
DEFAULT_CORS_ALLOW_ORIGIN)
+    core.response.set_header("Content-Type", ctx.grpc_web_mime or 
DEFAULT_CORS_CONTENT_TYPE)
+end
+
+function _M.body_filter(conf, ctx)
+    -- 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
+    -- https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md
+    -- 
https://github.com/grpc/grpc-web/blob/master/doc/browser-features.md#cors-support
+    if not ctx.grpc_web_mime then
+        return
+    end
+
+    if ctx.grpc_web_encoding == CONTENT_ENCODING_BASE64 then
+        local chunk = ngx_arg[1]
+        chunk = encode_base64(chunk)
+        ngx_arg[1] = chunk
+    end
+end
+
+return _M
diff --git a/ci/centos7-ci.sh b/ci/centos7-ci.sh
index 3167fce..7c74eba 100755
--- a/ci/centos7-ci.sh
+++ b/ci/centos7-ci.sh
@@ -75,6 +75,15 @@ install_dependencies() {
     # installing grpcurl
     install_grpcurl
 
+    # install nodejs
+    install_nodejs
+
+    # grpc-web server && client
+    cd t/plugin/grpc-web
+    ./setup.sh
+    # back to home directory
+    cd ../../../
+
     # install dependencies
     git clone https://github.com/iresty/test-nginx.git test-nginx
     create_lua_deps
diff --git a/ci/common.sh b/ci/common.sh
index 51bee69..fe015b1 100644
--- a/ci/common.sh
+++ b/ci/common.sh
@@ -62,4 +62,16 @@ install_vault_cli () {
     unzip vault_${VAULT_VERSION}_linux_amd64.zip && mv ./vault /usr/local/bin
 }
 
+install_nodejs () {
+    NODEJS_PREFIX="/usr/local/node"
+    NODEJS_VERSION="16.13.1"
+    wget 
https://nodejs.org/dist/v${NODEJS_VERSION}/node-v${NODEJS_VERSION}-linux-x64.tar.xz
+    tar -xvf node-v${NODEJS_VERSION}-linux-x64.tar.xz
+    rm -f /usr/local/bin/node
+    rm -f /usr/local/bin/npm
+    mv node-v${NODEJS_VERSION}-linux-x64 ${NODEJS_PREFIX}
+    ln -s ${NODEJS_PREFIX}/bin/node /usr/local/bin/node
+    ln -s ${NODEJS_PREFIX}/bin/npm /usr/local/bin/npm
+}
+
 GRPC_SERVER_EXAMPLE_VER=20210819
diff --git a/ci/linux_openresty_common_runner.sh 
b/ci/linux_openresty_common_runner.sh
index 9cfee16..ced0b83 100755
--- a/ci/linux_openresty_common_runner.sh
+++ b/ci/linux_openresty_common_runner.sh
@@ -57,6 +57,15 @@ do_install() {
     # install grpcurl
     install_grpcurl
 
+    # install nodejs
+    install_nodejs
+
+    # grpc-web server && client
+    cd t/plugin/grpc-web
+    ./setup.sh
+    # back to home directory
+    cd ../../../
+
     # install vault cli capabilities
     install_vault_cli
 }
diff --git a/conf/config-default.yaml b/conf/config-default.yaml
index cbf97dd..e1ae179 100644
--- a/conf/config-default.yaml
+++ b/conf/config-default.yaml
@@ -362,6 +362,7 @@ plugins:                          # plugin list (sorted by 
priority)
   - response-rewrite               # priority: 899
   #- dubbo-proxy                   # priority: 507
   - grpc-transcode                 # priority: 506
+  - grpc-web                       # priority: 505
   - prometheus                     # priority: 500
   - datadog                        # priority: 495
   - echo                           # priority: 412
diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json
index dcbfe68..3f86445 100644
--- a/docs/en/latest/config.json
+++ b/docs/en/latest/config.json
@@ -51,6 +51,7 @@
             "plugins/response-rewrite",
             "plugins/proxy-rewrite",
             "plugins/grpc-transcode",
+            "plugins/grpc-web",
             "plugins/fault-injection"
           ]
         },
diff --git a/docs/en/latest/plugins/grpc-web.md 
b/docs/en/latest/plugins/grpc-web.md
new file mode 100644
index 0000000..17c0111
--- /dev/null
+++ b/docs/en/latest/plugins/grpc-web.md
@@ -0,0 +1,85 @@
+---
+title: grpc-web
+---
+
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+## Summary
+
+- [**Name**](#name)
+- [**How To Enable**](#how-to-enable)
+- [**Test Plugin**](#test-plugin)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The `grpc-web` plugin is a proxy plugin used to process [gRPC 
Web](https://github.com/grpc/grpc-web) client requests to `gRPC Server`.
+
+gRPC Web Client -> APISIX -> gRPC server
+
+## How To Enable
+
+To enable the `gRPC Web` proxy plugin, routing must use the `Prefix matching` 
pattern (for example: `/*` or `/grpc/example/*`),
+Because the `gRPC Web` client will pass the `package name`, `service interface 
name`, `method name` and other information declared in the `proto` in the URI 
(for example: `/path/a6.RouteService/Insert`) ,
+When using `Absolute Match`, it will not be able to hit the plugin and extract 
the `proto` information.
+
+```bash
+curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: 
edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "uri":"/grpc/web/*",
+    "plugins":{
+        "grpc-web":{}
+    },
+    "upstream":{
+        "scheme":"grpc",
+        "type":"roundrobin",
+        "nodes":{
+            "127.0.0.1:1980":1
+        }
+    }
+}'
+```
+
+## Test Plugin
+
+- The request method only supports `POST` and `OPTIONS`, refer to: [CORS 
support](https://github.com/grpc/grpc-web/blob/master/doc/browser-features.md#cors-support).
+- The `Content-Type` supports `application/grpc-web`, 
`application/grpc-web-text`, `application/grpc-web+proto`, 
`application/grpc-web-text+proto`, refer to: [Protocol differences vs gRPC over 
HTTP2](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2).
+- Client deployment, refer to: [gRPC-Web Client Runtime 
Library](https://www.npmjs.com/package/grpc-web) or [Apache APISIX gRPC Web 
Test Framework](https://github.com/apache/apisix/tree/master/t/plugin/grpc-web).
+- After the `gRPC Web` client is deployed, you can initiate a `gRPC Web` proxy 
request to `APISIX` through `browser` or `node`.
+
+## Disable Plugin
+
+Just delete the JSON configuration of `grpc-web` in the plugin configuration.
+The APISIX plug-in is hot-reloaded, so there is no need to restart APISIX.
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: 
edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "uri":"/grpc/web/*",
+    "plugins":{},
+    "upstream":{
+        "scheme":"grpc",
+        "type":"roundrobin",
+        "nodes":{
+            "127.0.0.1:1980":1
+        }
+    }
+}'
+```
diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json
index 7ac810c..dbe2a00 100644
--- a/docs/zh/latest/config.json
+++ b/docs/zh/latest/config.json
@@ -51,6 +51,7 @@
             "plugins/response-rewrite",
             "plugins/proxy-rewrite",
             "plugins/grpc-transcode",
+            "plugins/grpc-web",
             "plugins/fault-injection"
           ]
         },
diff --git a/docs/zh/latest/plugins/grpc-web.md 
b/docs/zh/latest/plugins/grpc-web.md
new file mode 100644
index 0000000..206a931
--- /dev/null
+++ b/docs/zh/latest/plugins/grpc-web.md
@@ -0,0 +1,84 @@
+---
+title: grpc-web
+---
+
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+## 摘要
+
+- [**定义**](#定义)
+- [**如何开启**](#如何开启)
+- [**测试插件**](#测试插件)
+- [**禁用插件**](#禁用插件)
+
+## 定义
+
+`grpc-web` 插件是一个代理插件,用于转换 [gRPC Web](https://github.com/grpc/grpc-web) 客户端到 
`gRPC Server` 的请求。
+
+gRPC Web Client -> APISIX -> gRPC server
+
+## 如何开启
+
+启用 `gRPC Web` 代理插件,路由必须使用 `前缀匹配` 模式(例如:`/*` 或 `/grpc/example/*`),
+因为 `gRPC Web` 客户端会在 URI 中传递 `proto` 
中声明的`包名称`、`服务接口名称`、`方法名称`等信息(例如:`/path/a6.RouteService/Insert`),
+使用 `绝对匹配` 时将无法命中插件和提取 `proto` 信息。
+
+```bash
+curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: 
edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "uri":"/grpc/web/*",
+    "plugins":{
+        "grpc-web":{}
+    },
+    "upstream":{
+        "scheme":"grpc",
+        "type":"roundrobin",
+        "nodes":{
+            "127.0.0.1:1980":1
+        }
+    }
+}'
+```
+
+## 测试插件
+
+- 请求方式仅支持 `POST` 和 `OPTIONS`,参考:[CORS 
support](https://github.com/grpc/grpc-web/blob/master/doc/browser-features.md#cors-support)
 。
+- 内容类型支持 
`application/grpc-web`、`application/grpc-web-text`、`application/grpc-web+proto`、`application/grpc-web-text+proto`,参考:[Protocol
 differences vs gRPC over 
HTTP2](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2)
 。
+- 客户端部署,参考:[gRPC-Web Client Runtime 
Library](https://www.npmjs.com/package/grpc-web) 或 [Apache APISIX gRPC Web 
测试框架](https://github.com/apache/apisix/tree/master/t/plugin/grpc-web) 。
+- 完成 `gRPC Web` 客户端部署后,即可通过 `浏览器` 或 `node` 向 `APISIX` 发起 `gRPC Web` 代理请求。
+
+## 禁用插件
+
+只需删除插件配置中 `grpc-web` 的JSON配置即可。 APISIX 插件是热加载的,所以不需要重启 APISIX。
+
+```bash
+curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: 
edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "uri":"/grpc/web/*",
+    "plugins":{},
+    "upstream":{
+        "scheme":"grpc",
+        "type":"roundrobin",
+        "nodes":{
+            "127.0.0.1:1980":1
+        }
+    }
+}'
+```
diff --git a/t/admin/plugins.t b/t/admin/plugins.t
index 2495b53..29038ea 100644
--- a/t/admin/plugins.t
+++ b/t/admin/plugins.t
@@ -98,6 +98,7 @@ traffic-split
 redirect
 response-rewrite
 grpc-transcode
+grpc-web
 prometheus
 datadog
 echo
diff --git a/t/plugin/grpc-web.t b/t/plugin/grpc-web.t
new file mode 100644
index 0000000..37e1dac
--- /dev/null
+++ b/t/plugin/grpc-web.t
@@ -0,0 +1,221 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+use t::APISIX 'no_plan';
+
+no_long_string();
+no_shuffle();
+no_root_location();
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    if (!$block->request) {
+        $block->set_value("request", "GET /t");
+    }
+
+    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {
+        $block->set_value("no_error_log", "[error]");
+    }
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: set route (default grpc web proxy route)
+--- config
+    location /t {
+        content_by_lua_block {
+
+            local config = {
+                uri = "/grpc/*",
+                upstream = {
+                    scheme = "grpc",
+                    type = "roundrobin",
+                    nodes = {
+                        ["127.0.0.1:50001"] = 1
+                    }
+                },
+                plugins = {
+                    ["grpc-web"] = {}
+                }
+            }
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, 
config)
+
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 2: Flush all data through APISIX gRPC-Web Proxy
+--- exec
+node ./t/plugin/grpc-web/client.js FLUSH
+--- response_body
+[]
+
+
+
+=== TEST 3: Insert first data through APISIX gRPC-Web Proxy
+--- exec
+node ./t/plugin/grpc-web/client.js POST 1 route01 path01
+--- response_body
+[["1",{"name":"route01","path":"path01"}]]
+
+
+
+=== TEST 4: Update data through APISIX gRPC-Web Proxy
+--- exec
+node ./t/plugin/grpc-web/client.js PUT 1 route01 hello
+--- response_body
+[["1",{"name":"route01","path":"hello"}]]
+
+
+
+=== TEST 5: Insert second data through APISIX gRPC-Web Proxy
+--- exec
+node ./t/plugin/grpc-web/client.js POST 2 route02 path02
+--- response_body
+[["1",{"name":"route01","path":"hello"}],["2",{"name":"route02","path":"path02"}]]
+
+
+
+=== TEST 6: Insert third data through APISIX gRPC-Web Proxy
+--- exec
+node ./t/plugin/grpc-web/client.js POST 3 route03 path03
+--- response_body
+[["1",{"name":"route01","path":"hello"}],["2",{"name":"route02","path":"path02"}],["3",{"name":"route03","path":"path03"}]]
+
+
+
+=== TEST 7: Delete first data through APISIX gRPC-Web Proxy
+--- exec
+node ./t/plugin/grpc-web/client.js DEL 1
+--- response_body
+[["2",{"name":"route02","path":"path02"}],["3",{"name":"route03","path":"path03"}]]
+
+
+
+=== TEST 8: Get second data through APISIX gRPC-Web Proxy
+--- exec
+node ./t/plugin/grpc-web/client.js GET 2
+--- response_body
+{"name":"route02","path":"path02"}
+
+
+
+=== TEST 9: Get all data through APISIX gRPC-Web Proxy
+--- exec
+node ./t/plugin/grpc-web/client.js all
+--- response_body
+[["2",{"name":"route02","path":"path02"}],["3",{"name":"route03","path":"path03"}]]
+
+
+
+=== TEST 10: test options request
+--- request
+OPTIONS /grpc/a6.RouteService/GetAll
+--- error_code: 204
+--- response_headers
+Access-Control-Allow-Methods: POST
+Access-Control-Allow-Headers: content-type,x-grpc-web,x-user-agent
+Access-Control-Allow-Origin: *
+
+
+
+=== TEST 11: test non-options request
+--- request
+GET /grpc/a6.RouteService/GetAll
+--- error_code: 400
+--- response_headers
+Access-Control-Allow-Origin: *
+Content-Type: application/grpc-web-text+proto
+--- error_log
+request method: `GET` invalid
+
+
+
+=== TEST 12: test non gRPC Web MIME type request
+--- request
+POST /grpc/a6.RouteService/GetAll
+--- more_headers
+Content-Type: application/json
+--- error_code: 400
+--- response_headers
+Access-Control-Allow-Origin: *
+Content-Type: application/grpc-web-text+proto
+--- error_log
+request Content-Type: `application/json` invalid
+
+
+
+=== TEST 13: set route (absolute match)
+--- config
+    location /t {
+        content_by_lua_block {
+
+            local config = {
+                uri = "/grpc2/a6.RouteService/GetAll",
+                upstream = {
+                    scheme = "grpc",
+                    type = "roundrobin",
+                    nodes = {
+                        ["127.0.0.1:50001"] = 1
+                    }
+                },
+                plugins = {
+                    ["grpc-web"] = {}
+                }
+            }
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, 
config)
+
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 14: test route (absolute match)
+--- request
+POST /grpc2/a6.RouteService/GetAll
+--- more_headers
+Content-Type: application/grpc-web
+--- error_code: 400
+--- response_headers
+Access-Control-Allow-Origin: *
+Content-Type: application/grpc-web-text+proto
+--- error_log
+routing configuration error, grpc-web plugin only supports `prefix matching` 
pattern routing
diff --git a/t/plugin/grpc-web/a6/routes.pb.go 
b/t/plugin/grpc-web/a6/routes.pb.go
new file mode 100644
index 0000000..b00d62a
--- /dev/null
+++ b/t/plugin/grpc-web/a6/routes.pb.go
@@ -0,0 +1,513 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package a6
+
+import (
+       context "context"
+       fmt "fmt"
+       proto "github.com/golang/protobuf/proto"
+       grpc "google.golang.org/grpc"
+       codes "google.golang.org/grpc/codes"
+       status "google.golang.org/grpc/status"
+       math "math"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+// A compilation error at this line likely means your copy of the
+// proto package needs to be updated.
+const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
+
+type Empty struct {
+       XXX_NoUnkeyedLiteral struct{} `json:"-"`
+       XXX_unrecognized     []byte   `json:"-"`
+       XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *Empty) Reset()         { *m = Empty{} }
+func (m *Empty) String() string { return proto.CompactTextString(m) }
+func (*Empty) ProtoMessage()    {}
+func (*Empty) Descriptor() ([]byte, []int) {
+       return fileDescriptor_078f480fb67d0ab3, []int{0}
+}
+
+func (m *Empty) XXX_Unmarshal(b []byte) error {
+       return xxx_messageInfo_Empty.Unmarshal(m, b)
+}
+func (m *Empty) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+       return xxx_messageInfo_Empty.Marshal(b, m, deterministic)
+}
+func (m *Empty) XXX_Merge(src proto.Message) {
+       xxx_messageInfo_Empty.Merge(m, src)
+}
+func (m *Empty) XXX_Size() int {
+       return xxx_messageInfo_Empty.Size(m)
+}
+func (m *Empty) XXX_DiscardUnknown() {
+       xxx_messageInfo_Empty.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Empty proto.InternalMessageInfo
+
+type Route struct {
+       Name                 string   `protobuf:"bytes,1,opt,name=name,proto3" 
json:"name,omitempty"`
+       Path                 string   `protobuf:"bytes,2,opt,name=path,proto3" 
json:"path,omitempty"`
+       XXX_NoUnkeyedLiteral struct{} `json:"-"`
+       XXX_unrecognized     []byte   `json:"-"`
+       XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *Route) Reset()         { *m = Route{} }
+func (m *Route) String() string { return proto.CompactTextString(m) }
+func (*Route) ProtoMessage()    {}
+func (*Route) Descriptor() ([]byte, []int) {
+       return fileDescriptor_078f480fb67d0ab3, []int{1}
+}
+
+func (m *Route) XXX_Unmarshal(b []byte) error {
+       return xxx_messageInfo_Route.Unmarshal(m, b)
+}
+func (m *Route) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+       return xxx_messageInfo_Route.Marshal(b, m, deterministic)
+}
+func (m *Route) XXX_Merge(src proto.Message) {
+       xxx_messageInfo_Route.Merge(m, src)
+}
+func (m *Route) XXX_Size() int {
+       return xxx_messageInfo_Route.Size(m)
+}
+func (m *Route) XXX_DiscardUnknown() {
+       xxx_messageInfo_Route.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Route proto.InternalMessageInfo
+
+func (m *Route) GetName() string {
+       if m != nil {
+               return m.Name
+       }
+       return ""
+}
+
+func (m *Route) GetPath() string {
+       if m != nil {
+               return m.Path
+       }
+       return ""
+}
+
+type Request struct {
+       Id                   string   `protobuf:"bytes,1,opt,name=id,proto3" 
json:"id,omitempty"`
+       Route                *Route   `protobuf:"bytes,2,opt,name=route,proto3" 
json:"route,omitempty"`
+       XXX_NoUnkeyedLiteral struct{} `json:"-"`
+       XXX_unrecognized     []byte   `json:"-"`
+       XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *Request) Reset()         { *m = Request{} }
+func (m *Request) String() string { return proto.CompactTextString(m) }
+func (*Request) ProtoMessage()    {}
+func (*Request) Descriptor() ([]byte, []int) {
+       return fileDescriptor_078f480fb67d0ab3, []int{2}
+}
+
+func (m *Request) XXX_Unmarshal(b []byte) error {
+       return xxx_messageInfo_Request.Unmarshal(m, b)
+}
+func (m *Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+       return xxx_messageInfo_Request.Marshal(b, m, deterministic)
+}
+func (m *Request) XXX_Merge(src proto.Message) {
+       xxx_messageInfo_Request.Merge(m, src)
+}
+func (m *Request) XXX_Size() int {
+       return xxx_messageInfo_Request.Size(m)
+}
+func (m *Request) XXX_DiscardUnknown() {
+       xxx_messageInfo_Request.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Request proto.InternalMessageInfo
+
+func (m *Request) GetId() string {
+       if m != nil {
+               return m.Id
+       }
+       return ""
+}
+
+func (m *Request) GetRoute() *Route {
+       if m != nil {
+               return m.Route
+       }
+       return nil
+}
+
+type Response struct {
+       Status               bool              
`protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"`
+       Route                *Route            
`protobuf:"bytes,2,opt,name=route,proto3" json:"route,omitempty"`
+       Routes               map[string]*Route 
`protobuf:"bytes,3,rep,name=routes,proto3" json:"routes,omitempty" 
protobuf_key:"bytes,1,opt,name=key,proto3" 
protobuf_val:"bytes,2,opt,name=value,proto3"`
+       XXX_NoUnkeyedLiteral struct{}          `json:"-"`
+       XXX_unrecognized     []byte            `json:"-"`
+       XXX_sizecache        int32             `json:"-"`
+}
+
+func (m *Response) Reset()         { *m = Response{} }
+func (m *Response) String() string { return proto.CompactTextString(m) }
+func (*Response) ProtoMessage()    {}
+func (*Response) Descriptor() ([]byte, []int) {
+       return fileDescriptor_078f480fb67d0ab3, []int{3}
+}
+
+func (m *Response) XXX_Unmarshal(b []byte) error {
+       return xxx_messageInfo_Response.Unmarshal(m, b)
+}
+func (m *Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+       return xxx_messageInfo_Response.Marshal(b, m, deterministic)
+}
+func (m *Response) XXX_Merge(src proto.Message) {
+       xxx_messageInfo_Response.Merge(m, src)
+}
+func (m *Response) XXX_Size() int {
+       return xxx_messageInfo_Response.Size(m)
+}
+func (m *Response) XXX_DiscardUnknown() {
+       xxx_messageInfo_Response.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Response proto.InternalMessageInfo
+
+func (m *Response) GetStatus() bool {
+       if m != nil {
+               return m.Status
+       }
+       return false
+}
+
+func (m *Response) GetRoute() *Route {
+       if m != nil {
+               return m.Route
+       }
+       return nil
+}
+
+func (m *Response) GetRoutes() map[string]*Route {
+       if m != nil {
+               return m.Routes
+       }
+       return nil
+}
+
+func init() {
+       proto.RegisterType((*Empty)(nil), "a6.Empty")
+       proto.RegisterType((*Route)(nil), "a6.Route")
+       proto.RegisterType((*Request)(nil), "a6.Request")
+       proto.RegisterType((*Response)(nil), "a6.Response")
+       proto.RegisterMapType((map[string]*Route)(nil), 
"a6.Response.RoutesEntry")
+}
+
+func init() { proto.RegisterFile("routes.proto", 
fileDescriptor_078f480fb67d0ab3) }
+
+var fileDescriptor_078f480fb67d0ab3 = []byte{
+       // 307 bytes of a gzipped FileDescriptorProto
+       0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x92, 
0x4f, 0x4b, 0xf3, 0x40,
+       0x10, 0x87, 0xdf, 0x24, 0x6f, 0xd2, 0x76, 0x52, 0x44, 0xe6, 0x20, 0xa1, 
0x17, 0xcb, 0x4a, 0xa1,
+       0xa7, 0x54, 0x2a, 0x14, 0xa9, 0x27, 0xc5, 0x5a, 0xbc, 0xae, 0x78, 0xf1, 
0xb6, 0x9a, 0x81, 0x06,
+       0xf3, 0xcf, 0xec, 0x26, 0x90, 0xcf, 0xe6, 0xc7, 0xf2, 0x0b, 0x48, 0x26, 
0x39, 0x14, 0x14, 0x73,
+       0x9b, 0x3c, 0x79, 0xe6, 0x37, 0x93, 0x21, 0x30, 0x2d, 0xf3, 0xca, 0x90, 
0x0e, 0x8b, 0x32, 0x37,
+       0x39, 0xda, 0x6a, 0x23, 0x46, 0xe0, 0xee, 0xd2, 0xc2, 0x34, 0x62, 0x05, 
0xae, 0x6c, 0x5f, 0x22,
+       0xc2, 0xff, 0x4c, 0xa5, 0x14, 0x58, 0x73, 0x6b, 0x39, 0x91, 0x5c, 0xb7, 
0xac, 0x50, 0xe6, 0x10,
+       0xd8, 0x1d, 0x6b, 0x6b, 0xb1, 0x85, 0x91, 0xa4, 0x8f, 0x8a, 0xb4, 0xc1, 
0x13, 0xb0, 0xe3, 0xa8,
+       0x6f, 0xb0, 0xe3, 0x08, 0xcf, 0xc1, 0xe5, 0x41, 0xec, 0xfb, 0xeb, 0x49, 
0xa8, 0x36, 0x21, 0x87,
+       0xcb, 0x8e, 0x8b, 0x4f, 0x0b, 0xc6, 0x92, 0x74, 0x91, 0x67, 0x9a, 0xf0, 
0x0c, 0x3c, 0x6d, 0x94,
+       0xa9, 0x34, 0x27, 0x8c, 0x65, 0xff, 0x34, 0x98, 0x82, 0x97, 0xe0, 0x75, 
0xdf, 0x13, 0x38, 0x73,
+       0x67, 0xe9, 0xaf, 0x03, 0x36, 0xfa, 0xd8, 0x4e, 0xd5, 0xbb, 0xcc, 0x94, 
0x8d, 0xec, 0xbd, 0xd9,
+       0x3d, 0xf8, 0x47, 0x18, 0x4f, 0xc1, 0x79, 0xa7, 0xa6, 0x5f, 0xbc, 0x2d, 
0xdb, 0x99, 0xb5, 0x4a,
+       0xaa, 0xdf, 0x66, 0x32, 0xdf, 0xda, 0xd7, 0xd6, 0xfa, 0xcb, 0x82, 0x29, 
0xc3, 0x27, 0x2a, 0xeb,
+       0xf8, 0x8d, 0x70, 0x01, 0xe3, 0x87, 0xa4, 0xd2, 0x87, 0xdb, 0x24, 0x41, 
0x6e, 0xe1, 0x93, 0xce,
+       0xa6, 0xc7, 0xfb, 0x88, 0x7f, 0x78, 0x01, 0xde, 0x9e, 0xcc, 0x80, 0x24, 
0xc0, 0xd9, 0x93, 0x41,
+       0xbf, 0xc3, 0x7c, 0xdf, 0x1f, 0xce, 0x02, 0xbc, 0xc7, 0x4c, 0x53, 0x39, 
0xac, 0x3d, 0x17, 0x91,
+       0x32, 0x34, 0xa8, 0x49, 0x4a, 0xf3, 0xfa, 0x6f, 0xed, 0x6e, 0xf4, 0xe2, 
0x86, 0xab, 0x1b, 0xb5,
+       0x79, 0xf5, 0xf8, 0xef, 0xb9, 0xfa, 0x0e, 0x00, 0x00, 0xff, 0xff, 0xba, 
0xd4, 0xb5, 0x13, 0x4d,
+       0x02, 0x00, 0x00,
+}
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ context.Context
+var _ grpc.ClientConn
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+const _ = grpc.SupportPackageIsVersion4
+
+// RouteServiceClient is the client API for RouteService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please 
refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
+type RouteServiceClient interface {
+       FlushAll(ctx context.Context, in *Empty, opts ...grpc.CallOption) 
(*Response, error)
+       GetAll(ctx context.Context, in *Empty, opts ...grpc.CallOption) 
(*Response, error)
+       Get(ctx context.Context, in *Request, opts ...grpc.CallOption) 
(*Response, error)
+       Insert(ctx context.Context, in *Request, opts ...grpc.CallOption) 
(*Response, error)
+       Update(ctx context.Context, in *Request, opts ...grpc.CallOption) 
(*Response, error)
+       Remove(ctx context.Context, in *Request, opts ...grpc.CallOption) 
(*Response, error)
+}
+
+type routeServiceClient struct {
+       cc *grpc.ClientConn
+}
+
+func NewRouteServiceClient(cc *grpc.ClientConn) RouteServiceClient {
+       return &routeServiceClient{cc}
+}
+
+func (c *routeServiceClient) FlushAll(ctx context.Context, in *Empty, opts 
...grpc.CallOption) (*Response, error) {
+       out := new(Response)
+       err := c.cc.Invoke(ctx, "/a6.RouteService/FlushAll", in, out, opts...)
+       if err != nil {
+               return nil, err
+       }
+       return out, nil
+}
+
+func (c *routeServiceClient) GetAll(ctx context.Context, in *Empty, opts 
...grpc.CallOption) (*Response, error) {
+       out := new(Response)
+       err := c.cc.Invoke(ctx, "/a6.RouteService/GetAll", in, out, opts...)
+       if err != nil {
+               return nil, err
+       }
+       return out, nil
+}
+
+func (c *routeServiceClient) Get(ctx context.Context, in *Request, opts 
...grpc.CallOption) (*Response, error) {
+       out := new(Response)
+       err := c.cc.Invoke(ctx, "/a6.RouteService/Get", in, out, opts...)
+       if err != nil {
+               return nil, err
+       }
+       return out, nil
+}
+
+func (c *routeServiceClient) Insert(ctx context.Context, in *Request, opts 
...grpc.CallOption) (*Response, error) {
+       out := new(Response)
+       err := c.cc.Invoke(ctx, "/a6.RouteService/Insert", in, out, opts...)
+       if err != nil {
+               return nil, err
+       }
+       return out, nil
+}
+
+func (c *routeServiceClient) Update(ctx context.Context, in *Request, opts 
...grpc.CallOption) (*Response, error) {
+       out := new(Response)
+       err := c.cc.Invoke(ctx, "/a6.RouteService/Update", in, out, opts...)
+       if err != nil {
+               return nil, err
+       }
+       return out, nil
+}
+
+func (c *routeServiceClient) Remove(ctx context.Context, in *Request, opts 
...grpc.CallOption) (*Response, error) {
+       out := new(Response)
+       err := c.cc.Invoke(ctx, "/a6.RouteService/Remove", in, out, opts...)
+       if err != nil {
+               return nil, err
+       }
+       return out, nil
+}
+
+// RouteServiceServer is the server API for RouteService service.
+type RouteServiceServer interface {
+       FlushAll(context.Context, *Empty) (*Response, error)
+       GetAll(context.Context, *Empty) (*Response, error)
+       Get(context.Context, *Request) (*Response, error)
+       Insert(context.Context, *Request) (*Response, error)
+       Update(context.Context, *Request) (*Response, error)
+       Remove(context.Context, *Request) (*Response, error)
+}
+
+// UnimplementedRouteServiceServer can be embedded to have forward compatible 
implementations.
+type UnimplementedRouteServiceServer struct {
+}
+
+func (*UnimplementedRouteServiceServer) FlushAll(ctx context.Context, req 
*Empty) (*Response, error) {
+       return nil, status.Errorf(codes.Unimplemented, "method FlushAll not 
implemented")
+}
+func (*UnimplementedRouteServiceServer) GetAll(ctx context.Context, req 
*Empty) (*Response, error) {
+       return nil, status.Errorf(codes.Unimplemented, "method GetAll not 
implemented")
+}
+func (*UnimplementedRouteServiceServer) Get(ctx context.Context, req *Request) 
(*Response, error) {
+       return nil, status.Errorf(codes.Unimplemented, "method Get not 
implemented")
+}
+func (*UnimplementedRouteServiceServer) Insert(ctx context.Context, req 
*Request) (*Response, error) {
+       return nil, status.Errorf(codes.Unimplemented, "method Insert not 
implemented")
+}
+func (*UnimplementedRouteServiceServer) Update(ctx context.Context, req 
*Request) (*Response, error) {
+       return nil, status.Errorf(codes.Unimplemented, "method Update not 
implemented")
+}
+func (*UnimplementedRouteServiceServer) Remove(ctx context.Context, req 
*Request) (*Response, error) {
+       return nil, status.Errorf(codes.Unimplemented, "method Remove not 
implemented")
+}
+
+func RegisterRouteServiceServer(s *grpc.Server, srv RouteServiceServer) {
+       s.RegisterService(&_RouteService_serviceDesc, srv)
+}
+
+func _RouteService_FlushAll_Handler(srv interface{}, ctx context.Context, dec 
func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, 
error) {
+       in := new(Empty)
+       if err := dec(in); err != nil {
+               return nil, err
+       }
+       if interceptor == nil {
+               return srv.(RouteServiceServer).FlushAll(ctx, in)
+       }
+       info := &grpc.UnaryServerInfo{
+               Server:     srv,
+               FullMethod: "/a6.RouteService/FlushAll",
+       }
+       handler := func(ctx context.Context, req interface{}) (interface{}, 
error) {
+               return srv.(RouteServiceServer).FlushAll(ctx, req.(*Empty))
+       }
+       return interceptor(ctx, in, info, handler)
+}
+
+func _RouteService_GetAll_Handler(srv interface{}, ctx context.Context, dec 
func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, 
error) {
+       in := new(Empty)
+       if err := dec(in); err != nil {
+               return nil, err
+       }
+       if interceptor == nil {
+               return srv.(RouteServiceServer).GetAll(ctx, in)
+       }
+       info := &grpc.UnaryServerInfo{
+               Server:     srv,
+               FullMethod: "/a6.RouteService/GetAll",
+       }
+       handler := func(ctx context.Context, req interface{}) (interface{}, 
error) {
+               return srv.(RouteServiceServer).GetAll(ctx, req.(*Empty))
+       }
+       return interceptor(ctx, in, info, handler)
+}
+
+func _RouteService_Get_Handler(srv interface{}, ctx context.Context, dec 
func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, 
error) {
+       in := new(Request)
+       if err := dec(in); err != nil {
+               return nil, err
+       }
+       if interceptor == nil {
+               return srv.(RouteServiceServer).Get(ctx, in)
+       }
+       info := &grpc.UnaryServerInfo{
+               Server:     srv,
+               FullMethod: "/a6.RouteService/Get",
+       }
+       handler := func(ctx context.Context, req interface{}) (interface{}, 
error) {
+               return srv.(RouteServiceServer).Get(ctx, req.(*Request))
+       }
+       return interceptor(ctx, in, info, handler)
+}
+
+func _RouteService_Insert_Handler(srv interface{}, ctx context.Context, dec 
func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, 
error) {
+       in := new(Request)
+       if err := dec(in); err != nil {
+               return nil, err
+       }
+       if interceptor == nil {
+               return srv.(RouteServiceServer).Insert(ctx, in)
+       }
+       info := &grpc.UnaryServerInfo{
+               Server:     srv,
+               FullMethod: "/a6.RouteService/Insert",
+       }
+       handler := func(ctx context.Context, req interface{}) (interface{}, 
error) {
+               return srv.(RouteServiceServer).Insert(ctx, req.(*Request))
+       }
+       return interceptor(ctx, in, info, handler)
+}
+
+func _RouteService_Update_Handler(srv interface{}, ctx context.Context, dec 
func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, 
error) {
+       in := new(Request)
+       if err := dec(in); err != nil {
+               return nil, err
+       }
+       if interceptor == nil {
+               return srv.(RouteServiceServer).Update(ctx, in)
+       }
+       info := &grpc.UnaryServerInfo{
+               Server:     srv,
+               FullMethod: "/a6.RouteService/Update",
+       }
+       handler := func(ctx context.Context, req interface{}) (interface{}, 
error) {
+               return srv.(RouteServiceServer).Update(ctx, req.(*Request))
+       }
+       return interceptor(ctx, in, info, handler)
+}
+
+func _RouteService_Remove_Handler(srv interface{}, ctx context.Context, dec 
func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, 
error) {
+       in := new(Request)
+       if err := dec(in); err != nil {
+               return nil, err
+       }
+       if interceptor == nil {
+               return srv.(RouteServiceServer).Remove(ctx, in)
+       }
+       info := &grpc.UnaryServerInfo{
+               Server:     srv,
+               FullMethod: "/a6.RouteService/Remove",
+       }
+       handler := func(ctx context.Context, req interface{}) (interface{}, 
error) {
+               return srv.(RouteServiceServer).Remove(ctx, req.(*Request))
+       }
+       return interceptor(ctx, in, info, handler)
+}
+
+var _RouteService_serviceDesc = grpc.ServiceDesc{
+       ServiceName: "a6.RouteService",
+       HandlerType: (*RouteServiceServer)(nil),
+       Methods: []grpc.MethodDesc{
+               {
+                       MethodName: "FlushAll",
+                       Handler:    _RouteService_FlushAll_Handler,
+               },
+               {
+                       MethodName: "GetAll",
+                       Handler:    _RouteService_GetAll_Handler,
+               },
+               {
+                       MethodName: "Get",
+                       Handler:    _RouteService_Get_Handler,
+               },
+               {
+                       MethodName: "Insert",
+                       Handler:    _RouteService_Insert_Handler,
+               },
+               {
+                       MethodName: "Update",
+                       Handler:    _RouteService_Update_Handler,
+               },
+               {
+                       MethodName: "Remove",
+                       Handler:    _RouteService_Remove_Handler,
+               },
+       },
+       Streams:  []grpc.StreamDesc{},
+       Metadata: "routes.proto",
+}
diff --git a/t/plugin/grpc-web/a6/routes.proto 
b/t/plugin/grpc-web/a6/routes.proto
new file mode 100644
index 0000000..d8946c6
--- /dev/null
+++ b/t/plugin/grpc-web/a6/routes.proto
@@ -0,0 +1,49 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+syntax = "proto3";
+
+package a6;
+
+option go_package = "./;a6";
+
+service RouteService {
+  rpc FlushAll (Empty) returns (Response) {}
+  rpc GetAll (Empty) returns (Response) {}
+  rpc Get (Request) returns (Response) {}
+  rpc Insert (Request) returns (Response) {}
+  rpc Update (Request) returns (Response) {}
+  rpc Remove (Request) returns (Response) {}
+}
+
+message Empty {}
+
+message Route {
+  string name = 1;
+  string path = 2;
+}
+
+message Request {
+  string id = 1;
+  Route route = 2;
+}
+
+message Response {
+  bool status = 1;
+  Route route = 2;
+  map <string, Route> routes = 3;
+}
diff --git a/t/plugin/grpc-web/a6/routes_grpc.pb.go 
b/t/plugin/grpc-web/a6/routes_grpc.pb.go
new file mode 100644
index 0000000..e2bdab4
--- /dev/null
+++ b/t/plugin/grpc-web/a6/routes_grpc.pb.go
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package a6
+
+import (
+       "errors"
+       "golang.org/x/net/context"
+
+       uuid "github.com/satori/go.uuid"
+)
+
+type RouteServer struct {
+       Routes map[string]*Route
+}
+
+func (rs *RouteServer) init() {
+       if rs.Routes == nil {
+               rs.Routes = make(map[string]*Route)
+       }
+}
+
+func (rs *RouteServer) FlushAll(ctx context.Context, req *Empty) (*Response, 
error) {
+       if rs.Routes != nil {
+               rs.Routes = make(map[string]*Route)
+       }
+       return &Response{Routes: rs.Routes, Status: true}, nil
+}
+
+func (rs *RouteServer) GetAll(ctx context.Context, req *Empty) (*Response, 
error) {
+       rs.init()
+       return &Response{Routes: rs.Routes, Status: true}, nil
+}
+
+func (rs *RouteServer) Get(ctx context.Context, req *Request) (*Response, 
error) {
+       rs.init()
+       if len(req.Id) == 0 {
+               return &Response{Status: false}, errors.New("route ID 
undefined")
+       }
+
+       if route, ok := rs.Routes[req.Id]; ok {
+               return &Response{Status: true, Route: route}, nil
+       }
+
+       return &Response{Status: false}, errors.New("route not found")
+}
+
+func (rs *RouteServer) Insert(ctx context.Context, req *Request) (*Response, 
error) {
+       rs.init()
+       if len(req.Id) <= 0 {
+               req.Id = uuid.NewV4().String()
+       }
+       rs.Routes[req.Id] = req.Route
+       return &Response{Status: true, Routes: rs.Routes}, nil
+}
+
+func (rs *RouteServer) Update(ctx context.Context, req *Request) (*Response, 
error) {
+       rs.init()
+       if len(req.Id) == 0 {
+               return &Response{Status: false}, errors.New("route ID 
undefined")
+       }
+
+       if _, ok := rs.Routes[req.Id]; ok {
+               rs.Routes[req.Id] = req.Route
+               return &Response{Status: true, Routes: rs.Routes}, nil
+       }
+
+       return &Response{Status: false}, errors.New("route not found")
+}
+
+func (rs *RouteServer) Remove(ctx context.Context, req *Request) (*Response, 
error) {
+       rs.init()
+       if len(req.Id) == 0 {
+               return &Response{Status: false}, errors.New("route ID 
undefined")
+       }
+
+       if _, ok := rs.Routes[req.Id]; ok {
+               delete(rs.Routes, req.Id)
+               return &Response{Status: true, Routes: rs.Routes}, nil
+       }
+
+       return &Response{Status: false}, errors.New("route not found")
+}
diff --git a/t/plugin/grpc-web/a6/routes_grpc_web_pb.js 
b/t/plugin/grpc-web/a6/routes_grpc_web_pb.js
new file mode 100644
index 0000000..fff570e
--- /dev/null
+++ b/t/plugin/grpc-web/a6/routes_grpc_web_pb.js
@@ -0,0 +1,443 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const grpc = {};
+grpc.web = require('grpc-web');
+
+const proto = {};
+proto.a6 = require('./routes_pb.js');
+
+/**
+ * @param {string} hostname
+ * @param {?Object} credentials
+ * @param {?grpc.web.ClientOptions} options
+ * @constructor
+ * @struct
+ * @final
+ */
+proto.a6.RouteServiceClient =
+    function(hostname, credentials, options) {
+  if (!options) options = {};
+  options.format = 'binary';
+
+  /**
+   * @private @const {!grpc.web.GrpcWebClientBase} The client
+   */
+  this.client_ = new grpc.web.GrpcWebClientBase(options);
+
+  /**
+   * @private @const {string} The hostname
+   */
+  this.hostname_ = hostname;
+
+};
+
+
+/**
+ * @param {string} hostname
+ * @param {?Object} credentials
+ * @param {?grpc.web.ClientOptions} options
+ * @constructor
+ * @struct
+ * @final
+ */
+proto.a6.RouteServicePromiseClient =
+    function(hostname, credentials, options) {
+  if (!options) options = {};
+  options.format = 'binary';
+
+  /**
+   * @private @const {!grpc.web.GrpcWebClientBase} The client
+   */
+  this.client_ = new grpc.web.GrpcWebClientBase(options);
+
+  /**
+   * @private @const {string} The hostname
+   */
+  this.hostname_ = hostname;
+
+};
+
+
+/**
+ * @const
+ * @type {!grpc.web.MethodDescriptor<
+ *   !proto.a6.Empty,
+ *   !proto.a6.Response>}
+ */
+const methodDescriptor_RouteService_FlushAll = new grpc.web.MethodDescriptor(
+  '/a6.RouteService/FlushAll',
+  grpc.web.MethodType.UNARY,
+  proto.a6.Empty,
+  proto.a6.Response,
+  /**
+   * @param {!proto.a6.Empty} request
+   * @return {!Uint8Array}
+   */
+  function(request) {
+    return request.serializeBinary();
+  },
+  proto.a6.Response.deserializeBinary
+);
+
+
+/**
+ * @param {!proto.a6.Empty} request The
+ *     request proto
+ * @param {?Object<string, string>} metadata User defined
+ *     call metadata
+ * @param {function(?grpc.web.RpcError, ?proto.a6.Response)}
+ *     callback The callback function(error, response)
+ * @return {!grpc.web.ClientReadableStream<!proto.a6.Response>|undefined}
+ *     The XHR Node Readable Stream
+ */
+proto.a6.RouteServiceClient.prototype.flushAll =
+    function(request, metadata, callback) {
+  return this.client_.rpcCall(this.hostname_ +
+      '/a6.RouteService/FlushAll',
+      request,
+      metadata || {},
+      methodDescriptor_RouteService_FlushAll,
+      callback);
+};
+
+
+/**
+ * @param {!proto.a6.Empty} request The
+ *     request proto
+ * @param {?Object<string, string>=} metadata User defined
+ *     call metadata
+ * @return {!Promise<!proto.a6.Response>}
+ *     Promise that resolves to the response
+ */
+proto.a6.RouteServicePromiseClient.prototype.flushAll =
+    function(request, metadata) {
+  return this.client_.unaryCall(this.hostname_ +
+      '/a6.RouteService/FlushAll',
+      request,
+      metadata || {},
+      methodDescriptor_RouteService_FlushAll);
+};
+
+
+/**
+ * @const
+ * @type {!grpc.web.MethodDescriptor<
+ *   !proto.a6.Empty,
+ *   !proto.a6.Response>}
+ */
+const methodDescriptor_RouteService_GetAll = new grpc.web.MethodDescriptor(
+  '/a6.RouteService/GetAll',
+  grpc.web.MethodType.UNARY,
+  proto.a6.Empty,
+  proto.a6.Response,
+  /**
+   * @param {!proto.a6.Empty} request
+   * @return {!Uint8Array}
+   */
+  function(request) {
+    return request.serializeBinary();
+  },
+  proto.a6.Response.deserializeBinary
+);
+
+
+/**
+ * @param {!proto.a6.Empty} request The
+ *     request proto
+ * @param {?Object<string, string>} metadata User defined
+ *     call metadata
+ * @param {function(?grpc.web.RpcError, ?proto.a6.Response)}
+ *     callback The callback function(error, response)
+ * @return {!grpc.web.ClientReadableStream<!proto.a6.Response>|undefined}
+ *     The XHR Node Readable Stream
+ */
+proto.a6.RouteServiceClient.prototype.getAll =
+    function(request, metadata, callback) {
+  return this.client_.rpcCall(this.hostname_ +
+      '/a6.RouteService/GetAll',
+      request,
+      metadata || {},
+      methodDescriptor_RouteService_GetAll,
+      callback);
+};
+
+
+/**
+ * @param {!proto.a6.Empty} request The
+ *     request proto
+ * @param {?Object<string, string>=} metadata User defined
+ *     call metadata
+ * @return {!Promise<!proto.a6.Response>}
+ *     Promise that resolves to the response
+ */
+proto.a6.RouteServicePromiseClient.prototype.getAll =
+    function(request, metadata) {
+  return this.client_.unaryCall(this.hostname_ +
+      '/a6.RouteService/GetAll',
+      request,
+      metadata || {},
+      methodDescriptor_RouteService_GetAll);
+};
+
+
+/**
+ * @const
+ * @type {!grpc.web.MethodDescriptor<
+ *   !proto.a6.Request,
+ *   !proto.a6.Response>}
+ */
+const methodDescriptor_RouteService_Get = new grpc.web.MethodDescriptor(
+  '/a6.RouteService/Get',
+  grpc.web.MethodType.UNARY,
+  proto.a6.Request,
+  proto.a6.Response,
+  /**
+   * @param {!proto.a6.Request} request
+   * @return {!Uint8Array}
+   */
+  function(request) {
+    return request.serializeBinary();
+  },
+  proto.a6.Response.deserializeBinary
+);
+
+
+/**
+ * @param {!proto.a6.Request} request The
+ *     request proto
+ * @param {?Object<string, string>} metadata User defined
+ *     call metadata
+ * @param {function(?grpc.web.RpcError, ?proto.a6.Response)}
+ *     callback The callback function(error, response)
+ * @return {!grpc.web.ClientReadableStream<!proto.a6.Response>|undefined}
+ *     The XHR Node Readable Stream
+ */
+proto.a6.RouteServiceClient.prototype.get =
+    function(request, metadata, callback) {
+  return this.client_.rpcCall(this.hostname_ +
+      '/a6.RouteService/Get',
+      request,
+      metadata || {},
+      methodDescriptor_RouteService_Get,
+      callback);
+};
+
+
+/**
+ * @param {!proto.a6.Request} request The
+ *     request proto
+ * @param {?Object<string, string>=} metadata User defined
+ *     call metadata
+ * @return {!Promise<!proto.a6.Response>}
+ *     Promise that resolves to the response
+ */
+proto.a6.RouteServicePromiseClient.prototype.get =
+    function(request, metadata) {
+  return this.client_.unaryCall(this.hostname_ +
+      '/a6.RouteService/Get',
+      request,
+      metadata || {},
+      methodDescriptor_RouteService_Get);
+};
+
+
+/**
+ * @const
+ * @type {!grpc.web.MethodDescriptor<
+ *   !proto.a6.Request,
+ *   !proto.a6.Response>}
+ */
+const methodDescriptor_RouteService_Insert = new grpc.web.MethodDescriptor(
+  '/a6.RouteService/Insert',
+  grpc.web.MethodType.UNARY,
+  proto.a6.Request,
+  proto.a6.Response,
+  /**
+   * @param {!proto.a6.Request} request
+   * @return {!Uint8Array}
+   */
+  function(request) {
+    return request.serializeBinary();
+  },
+  proto.a6.Response.deserializeBinary
+);
+
+
+/**
+ * @param {!proto.a6.Request} request The
+ *     request proto
+ * @param {?Object<string, string>} metadata User defined
+ *     call metadata
+ * @param {function(?grpc.web.RpcError, ?proto.a6.Response)}
+ *     callback The callback function(error, response)
+ * @return {!grpc.web.ClientReadableStream<!proto.a6.Response>|undefined}
+ *     The XHR Node Readable Stream
+ */
+proto.a6.RouteServiceClient.prototype.insert =
+    function(request, metadata, callback) {
+  return this.client_.rpcCall(this.hostname_ +
+      '/a6.RouteService/Insert',
+      request,
+      metadata || {},
+      methodDescriptor_RouteService_Insert,
+      callback);
+};
+
+
+/**
+ * @param {!proto.a6.Request} request The
+ *     request proto
+ * @param {?Object<string, string>=} metadata User defined
+ *     call metadata
+ * @return {!Promise<!proto.a6.Response>}
+ *     Promise that resolves to the response
+ */
+proto.a6.RouteServicePromiseClient.prototype.insert =
+    function(request, metadata) {
+  return this.client_.unaryCall(this.hostname_ +
+      '/a6.RouteService/Insert',
+      request,
+      metadata || {},
+      methodDescriptor_RouteService_Insert);
+};
+
+
+/**
+ * @const
+ * @type {!grpc.web.MethodDescriptor<
+ *   !proto.a6.Request,
+ *   !proto.a6.Response>}
+ */
+const methodDescriptor_RouteService_Update = new grpc.web.MethodDescriptor(
+  '/a6.RouteService/Update',
+  grpc.web.MethodType.UNARY,
+  proto.a6.Request,
+  proto.a6.Response,
+  /**
+   * @param {!proto.a6.Request} request
+   * @return {!Uint8Array}
+   */
+  function(request) {
+    return request.serializeBinary();
+  },
+  proto.a6.Response.deserializeBinary
+);
+
+
+/**
+ * @param {!proto.a6.Request} request The
+ *     request proto
+ * @param {?Object<string, string>} metadata User defined
+ *     call metadata
+ * @param {function(?grpc.web.RpcError, ?proto.a6.Response)}
+ *     callback The callback function(error, response)
+ * @return {!grpc.web.ClientReadableStream<!proto.a6.Response>|undefined}
+ *     The XHR Node Readable Stream
+ */
+proto.a6.RouteServiceClient.prototype.update =
+    function(request, metadata, callback) {
+  return this.client_.rpcCall(this.hostname_ +
+      '/a6.RouteService/Update',
+      request,
+      metadata || {},
+      methodDescriptor_RouteService_Update,
+      callback);
+};
+
+
+/**
+ * @param {!proto.a6.Request} request The
+ *     request proto
+ * @param {?Object<string, string>=} metadata User defined
+ *     call metadata
+ * @return {!Promise<!proto.a6.Response>}
+ *     Promise that resolves to the response
+ */
+proto.a6.RouteServicePromiseClient.prototype.update =
+    function(request, metadata) {
+  return this.client_.unaryCall(this.hostname_ +
+      '/a6.RouteService/Update',
+      request,
+      metadata || {},
+      methodDescriptor_RouteService_Update);
+};
+
+
+/**
+ * @const
+ * @type {!grpc.web.MethodDescriptor<
+ *   !proto.a6.Request,
+ *   !proto.a6.Response>}
+ */
+const methodDescriptor_RouteService_Remove = new grpc.web.MethodDescriptor(
+  '/a6.RouteService/Remove',
+  grpc.web.MethodType.UNARY,
+  proto.a6.Request,
+  proto.a6.Response,
+  /**
+   * @param {!proto.a6.Request} request
+   * @return {!Uint8Array}
+   */
+  function(request) {
+    return request.serializeBinary();
+  },
+  proto.a6.Response.deserializeBinary
+);
+
+
+/**
+ * @param {!proto.a6.Request} request The
+ *     request proto
+ * @param {?Object<string, string>} metadata User defined
+ *     call metadata
+ * @param {function(?grpc.web.RpcError, ?proto.a6.Response)}
+ *     callback The callback function(error, response)
+ * @return {!grpc.web.ClientReadableStream<!proto.a6.Response>|undefined}
+ *     The XHR Node Readable Stream
+ */
+proto.a6.RouteServiceClient.prototype.remove =
+    function(request, metadata, callback) {
+  return this.client_.rpcCall(this.hostname_ +
+      '/a6.RouteService/Remove',
+      request,
+      metadata || {},
+      methodDescriptor_RouteService_Remove,
+      callback);
+};
+
+
+/**
+ * @param {!proto.a6.Request} request The
+ *     request proto
+ * @param {?Object<string, string>=} metadata User defined
+ *     call metadata
+ * @return {!Promise<!proto.a6.Response>}
+ *     Promise that resolves to the response
+ */
+proto.a6.RouteServicePromiseClient.prototype.remove =
+    function(request, metadata) {
+  return this.client_.unaryCall(this.hostname_ +
+      '/a6.RouteService/Remove',
+      request,
+      metadata || {},
+      methodDescriptor_RouteService_Remove);
+};
+
+
+module.exports = proto.a6;
+
diff --git a/t/plugin/grpc-web/a6/routes_pb.js 
b/t/plugin/grpc-web/a6/routes_pb.js
new file mode 100644
index 0000000..90612d3
--- /dev/null
+++ b/t/plugin/grpc-web/a6/routes_pb.js
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var jspb = require('google-protobuf');
+var goog = jspb;
+var global = Function('return this')();
+
+goog.exportSymbol('proto.a6.Empty', null, global);
+goog.exportSymbol('proto.a6.Request', null, global);
+goog.exportSymbol('proto.a6.Response', null, global);
+goog.exportSymbol('proto.a6.Route', null, global);
+/**
+ * Generated by JsPbCodeGenerator.
+ * @param {Array=} opt_data Optional initial data array, typically from a
+ * server response, or constructed directly in Javascript. The array is used
+ * in place and becomes part of the constructed object. It is not cloned.
+ * If no data is provided, the constructed object will be empty, but still
+ * valid.
+ * @extends {jspb.Message}
+ * @constructor
+ */
+proto.a6.Empty = function(opt_data) {
+  jspb.Message.initialize(this, opt_data, 0, -1, null, null);
+};
+goog.inherits(proto.a6.Empty, jspb.Message);
+if (goog.DEBUG && !COMPILED) {
+  /**
+   * @public
+   * @override
+   */
+  proto.a6.Empty.displayName = 'proto.a6.Empty';
+}
+/**
+ * Generated by JsPbCodeGenerator.
+ * @param {Array=} opt_data Optional initial data array, typically from a
+ * server response, or constructed directly in Javascript. The array is used
+ * in place and becomes part of the constructed object. It is not cloned.
+ * If no data is provided, the constructed object will be empty, but still
+ * valid.
+ * @extends {jspb.Message}
+ * @constructor
+ */
+proto.a6.Route = function(opt_data) {
+  jspb.Message.initialize(this, opt_data, 0, -1, null, null);
+};
+goog.inherits(proto.a6.Route, jspb.Message);
+if (goog.DEBUG && !COMPILED) {
+  /**
+   * @public
+   * @override
+   */
+  proto.a6.Route.displayName = 'proto.a6.Route';
+}
+/**
+ * Generated by JsPbCodeGenerator.
+ * @param {Array=} opt_data Optional initial data array, typically from a
+ * server response, or constructed directly in Javascript. The array is used
+ * in place and becomes part of the constructed object. It is not cloned.
+ * If no data is provided, the constructed object will be empty, but still
+ * valid.
+ * @extends {jspb.Message}
+ * @constructor
+ */
+proto.a6.Request = function(opt_data) {
+  jspb.Message.initialize(this, opt_data, 0, -1, null, null);
+};
+goog.inherits(proto.a6.Request, jspb.Message);
+if (goog.DEBUG && !COMPILED) {
+  /**
+   * @public
+   * @override
+   */
+  proto.a6.Request.displayName = 'proto.a6.Request';
+}
+/**
+ * Generated by JsPbCodeGenerator.
+ * @param {Array=} opt_data Optional initial data array, typically from a
+ * server response, or constructed directly in Javascript. The array is used
+ * in place and becomes part of the constructed object. It is not cloned.
+ * If no data is provided, the constructed object will be empty, but still
+ * valid.
+ * @extends {jspb.Message}
+ * @constructor
+ */
+proto.a6.Response = function(opt_data) {
+  jspb.Message.initialize(this, opt_data, 0, -1, null, null);
+};
+goog.inherits(proto.a6.Response, jspb.Message);
+if (goog.DEBUG && !COMPILED) {
+  /**
+   * @public
+   * @override
+   */
+  proto.a6.Response.displayName = 'proto.a6.Response';
+}
+
+
+
+if (jspb.Message.GENERATE_TO_OBJECT) {
+/**
+ * Creates an object representation of this proto.
+ * Field names that are reserved in JavaScript and will be renamed to pb_name.
+ * Optional fields that are not set will be set to undefined.
+ * To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
+ * For the list of reserved names please see:
+ *     net/proto2/compiler/js/internal/generator.cc#kKeyword.
+ * @param {boolean=} opt_includeInstance Deprecated. whether to include the
+ *     JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @return {!Object}
+ */
+proto.a6.Empty.prototype.toObject = function(opt_includeInstance) {
+  return proto.a6.Empty.toObject(opt_includeInstance, this);
+};
+
+
+/**
+ * Static version of the {@see toObject} method.
+ * @param {boolean|undefined} includeInstance Deprecated. Whether to include
+ *     the JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @param {!proto.a6.Empty} msg The msg instance to transform.
+ * @return {!Object}
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.a6.Empty.toObject = function(includeInstance, msg) {
+  var f, obj = {
+
+  };
+
+  if (includeInstance) {
+    obj.$jspbMessageInstance = msg;
+  }
+  return obj;
+};
+}
+
+
+/**
+ * Deserializes binary data (in protobuf wire format).
+ * @param {jspb.ByteSource} bytes The bytes to deserialize.
+ * @return {!proto.a6.Empty}
+ */
+proto.a6.Empty.deserializeBinary = function(bytes) {
+  var reader = new jspb.BinaryReader(bytes);
+  var msg = new proto.a6.Empty;
+  return proto.a6.Empty.deserializeBinaryFromReader(msg, reader);
+};
+
+
+/**
+ * Deserializes binary data (in protobuf wire format) from the
+ * given reader into the given message object.
+ * @param {!proto.a6.Empty} msg The message object to deserialize into.
+ * @param {!jspb.BinaryReader} reader The BinaryReader to use.
+ * @return {!proto.a6.Empty}
+ */
+proto.a6.Empty.deserializeBinaryFromReader = function(msg, reader) {
+  while (reader.nextField()) {
+    if (reader.isEndGroup()) {
+      break;
+    }
+    var field = reader.getFieldNumber();
+    switch (field) {
+    default:
+      reader.skipField();
+      break;
+    }
+  }
+  return msg;
+};
+
+
+/**
+ * Serializes the message to binary data (in protobuf wire format).
+ * @return {!Uint8Array}
+ */
+proto.a6.Empty.prototype.serializeBinary = function() {
+  var writer = new jspb.BinaryWriter();
+  proto.a6.Empty.serializeBinaryToWriter(this, writer);
+  return writer.getResultBuffer();
+};
+
+
+/**
+ * Serializes the given message to binary data (in protobuf wire
+ * format), writing to the given BinaryWriter.
+ * @param {!proto.a6.Empty} message
+ * @param {!jspb.BinaryWriter} writer
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.a6.Empty.serializeBinaryToWriter = function(message, writer) {
+  var f = undefined;
+};
+
+
+
+
+
+if (jspb.Message.GENERATE_TO_OBJECT) {
+/**
+ * Creates an object representation of this proto.
+ * Field names that are reserved in JavaScript and will be renamed to pb_name.
+ * Optional fields that are not set will be set to undefined.
+ * To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
+ * For the list of reserved names please see:
+ *     net/proto2/compiler/js/internal/generator.cc#kKeyword.
+ * @param {boolean=} opt_includeInstance Deprecated. whether to include the
+ *     JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @return {!Object}
+ */
+proto.a6.Route.prototype.toObject = function(opt_includeInstance) {
+  return proto.a6.Route.toObject(opt_includeInstance, this);
+};
+
+
+/**
+ * Static version of the {@see toObject} method.
+ * @param {boolean|undefined} includeInstance Deprecated. Whether to include
+ *     the JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @param {!proto.a6.Route} msg The msg instance to transform.
+ * @return {!Object}
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.a6.Route.toObject = function(includeInstance, msg) {
+  var f, obj = {
+    name: jspb.Message.getFieldWithDefault(msg, 1, ""),
+    path: jspb.Message.getFieldWithDefault(msg, 2, "")
+  };
+
+  if (includeInstance) {
+    obj.$jspbMessageInstance = msg;
+  }
+  return obj;
+};
+}
+
+
+/**
+ * Deserializes binary data (in protobuf wire format).
+ * @param {jspb.ByteSource} bytes The bytes to deserialize.
+ * @return {!proto.a6.Route}
+ */
+proto.a6.Route.deserializeBinary = function(bytes) {
+  var reader = new jspb.BinaryReader(bytes);
+  var msg = new proto.a6.Route;
+  return proto.a6.Route.deserializeBinaryFromReader(msg, reader);
+};
+
+
+/**
+ * Deserializes binary data (in protobuf wire format) from the
+ * given reader into the given message object.
+ * @param {!proto.a6.Route} msg The message object to deserialize into.
+ * @param {!jspb.BinaryReader} reader The BinaryReader to use.
+ * @return {!proto.a6.Route}
+ */
+proto.a6.Route.deserializeBinaryFromReader = function(msg, reader) {
+  while (reader.nextField()) {
+    if (reader.isEndGroup()) {
+      break;
+    }
+    var field = reader.getFieldNumber();
+    switch (field) {
+    case 1:
+      var value = /** @type {string} */ (reader.readString());
+      msg.setName(value);
+      break;
+    case 2:
+      var value = /** @type {string} */ (reader.readString());
+      msg.setPath(value);
+      break;
+    default:
+      reader.skipField();
+      break;
+    }
+  }
+  return msg;
+};
+
+
+/**
+ * Serializes the message to binary data (in protobuf wire format).
+ * @return {!Uint8Array}
+ */
+proto.a6.Route.prototype.serializeBinary = function() {
+  var writer = new jspb.BinaryWriter();
+  proto.a6.Route.serializeBinaryToWriter(this, writer);
+  return writer.getResultBuffer();
+};
+
+
+/**
+ * Serializes the given message to binary data (in protobuf wire
+ * format), writing to the given BinaryWriter.
+ * @param {!proto.a6.Route} message
+ * @param {!jspb.BinaryWriter} writer
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.a6.Route.serializeBinaryToWriter = function(message, writer) {
+  var f = undefined;
+  f = message.getName();
+  if (f.length > 0) {
+    writer.writeString(
+      1,
+      f
+    );
+  }
+  f = message.getPath();
+  if (f.length > 0) {
+    writer.writeString(
+      2,
+      f
+    );
+  }
+};
+
+
+/**
+ * optional string name = 1;
+ * @return {string}
+ */
+proto.a6.Route.prototype.getName = function() {
+  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
+};
+
+
+/**
+ * @param {string} value
+ * @return {!proto.a6.Route} returns this
+ */
+proto.a6.Route.prototype.setName = function(value) {
+  return jspb.Message.setProto3StringField(this, 1, value);
+};
+
+
+/**
+ * optional string path = 2;
+ * @return {string}
+ */
+proto.a6.Route.prototype.getPath = function() {
+  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, ""));
+};
+
+
+/**
+ * @param {string} value
+ * @return {!proto.a6.Route} returns this
+ */
+proto.a6.Route.prototype.setPath = function(value) {
+  return jspb.Message.setProto3StringField(this, 2, value);
+};
+
+
+
+
+
+if (jspb.Message.GENERATE_TO_OBJECT) {
+/**
+ * Creates an object representation of this proto.
+ * Field names that are reserved in JavaScript and will be renamed to pb_name.
+ * Optional fields that are not set will be set to undefined.
+ * To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
+ * For the list of reserved names please see:
+ *     net/proto2/compiler/js/internal/generator.cc#kKeyword.
+ * @param {boolean=} opt_includeInstance Deprecated. whether to include the
+ *     JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @return {!Object}
+ */
+proto.a6.Request.prototype.toObject = function(opt_includeInstance) {
+  return proto.a6.Request.toObject(opt_includeInstance, this);
+};
+
+
+/**
+ * Static version of the {@see toObject} method.
+ * @param {boolean|undefined} includeInstance Deprecated. Whether to include
+ *     the JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @param {!proto.a6.Request} msg The msg instance to transform.
+ * @return {!Object}
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.a6.Request.toObject = function(includeInstance, msg) {
+  var f, obj = {
+    id: jspb.Message.getFieldWithDefault(msg, 1, ""),
+    route: (f = msg.getRoute()) && proto.a6.Route.toObject(includeInstance, f)
+  };
+
+  if (includeInstance) {
+    obj.$jspbMessageInstance = msg;
+  }
+  return obj;
+};
+}
+
+
+/**
+ * Deserializes binary data (in protobuf wire format).
+ * @param {jspb.ByteSource} bytes The bytes to deserialize.
+ * @return {!proto.a6.Request}
+ */
+proto.a6.Request.deserializeBinary = function(bytes) {
+  var reader = new jspb.BinaryReader(bytes);
+  var msg = new proto.a6.Request;
+  return proto.a6.Request.deserializeBinaryFromReader(msg, reader);
+};
+
+
+/**
+ * Deserializes binary data (in protobuf wire format) from the
+ * given reader into the given message object.
+ * @param {!proto.a6.Request} msg The message object to deserialize into.
+ * @param {!jspb.BinaryReader} reader The BinaryReader to use.
+ * @return {!proto.a6.Request}
+ */
+proto.a6.Request.deserializeBinaryFromReader = function(msg, reader) {
+  while (reader.nextField()) {
+    if (reader.isEndGroup()) {
+      break;
+    }
+    var field = reader.getFieldNumber();
+    switch (field) {
+    case 1:
+      var value = /** @type {string} */ (reader.readString());
+      msg.setId(value);
+      break;
+    case 2:
+      var value = new proto.a6.Route;
+      reader.readMessage(value,proto.a6.Route.deserializeBinaryFromReader);
+      msg.setRoute(value);
+      break;
+    default:
+      reader.skipField();
+      break;
+    }
+  }
+  return msg;
+};
+
+
+/**
+ * Serializes the message to binary data (in protobuf wire format).
+ * @return {!Uint8Array}
+ */
+proto.a6.Request.prototype.serializeBinary = function() {
+  var writer = new jspb.BinaryWriter();
+  proto.a6.Request.serializeBinaryToWriter(this, writer);
+  return writer.getResultBuffer();
+};
+
+
+/**
+ * Serializes the given message to binary data (in protobuf wire
+ * format), writing to the given BinaryWriter.
+ * @param {!proto.a6.Request} message
+ * @param {!jspb.BinaryWriter} writer
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.a6.Request.serializeBinaryToWriter = function(message, writer) {
+  var f = undefined;
+  f = message.getId();
+  if (f.length > 0) {
+    writer.writeString(
+      1,
+      f
+    );
+  }
+  f = message.getRoute();
+  if (f != null) {
+    writer.writeMessage(
+      2,
+      f,
+      proto.a6.Route.serializeBinaryToWriter
+    );
+  }
+};
+
+
+/**
+ * optional string id = 1;
+ * @return {string}
+ */
+proto.a6.Request.prototype.getId = function() {
+  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
+};
+
+
+/**
+ * @param {string} value
+ * @return {!proto.a6.Request} returns this
+ */
+proto.a6.Request.prototype.setId = function(value) {
+  return jspb.Message.setProto3StringField(this, 1, value);
+};
+
+
+/**
+ * optional Route route = 2;
+ * @return {?proto.a6.Route}
+ */
+proto.a6.Request.prototype.getRoute = function() {
+  return /** @type{?proto.a6.Route} */ (
+    jspb.Message.getWrapperField(this, proto.a6.Route, 2));
+};
+
+
+/**
+ * @param {?proto.a6.Route|undefined} value
+ * @return {!proto.a6.Request} returns this
+*/
+proto.a6.Request.prototype.setRoute = function(value) {
+  return jspb.Message.setWrapperField(this, 2, value);
+};
+
+
+/**
+ * Clears the message field making it undefined.
+ * @return {!proto.a6.Request} returns this
+ */
+proto.a6.Request.prototype.clearRoute = function() {
+  return this.setRoute(undefined);
+};
+
+
+/**
+ * Returns whether this field is set.
+ * @return {boolean}
+ */
+proto.a6.Request.prototype.hasRoute = function() {
+  return jspb.Message.getField(this, 2) != null;
+};
+
+
+
+
+
+if (jspb.Message.GENERATE_TO_OBJECT) {
+/**
+ * Creates an object representation of this proto.
+ * Field names that are reserved in JavaScript and will be renamed to pb_name.
+ * Optional fields that are not set will be set to undefined.
+ * To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
+ * For the list of reserved names please see:
+ *     net/proto2/compiler/js/internal/generator.cc#kKeyword.
+ * @param {boolean=} opt_includeInstance Deprecated. whether to include the
+ *     JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @return {!Object}
+ */
+proto.a6.Response.prototype.toObject = function(opt_includeInstance) {
+  return proto.a6.Response.toObject(opt_includeInstance, this);
+};
+
+
+/**
+ * Static version of the {@see toObject} method.
+ * @param {boolean|undefined} includeInstance Deprecated. Whether to include
+ *     the JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @param {!proto.a6.Response} msg The msg instance to transform.
+ * @return {!Object}
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.a6.Response.toObject = function(includeInstance, msg) {
+  var f, obj = {
+    status: jspb.Message.getBooleanFieldWithDefault(msg, 1, false),
+    route: (f = msg.getRoute()) && proto.a6.Route.toObject(includeInstance, f),
+    routesMap: (f = msg.getRoutesMap()) ? f.toObject(includeInstance, 
proto.a6.Route.toObject) : []
+  };
+
+  if (includeInstance) {
+    obj.$jspbMessageInstance = msg;
+  }
+  return obj;
+};
+}
+
+
+/**
+ * Deserializes binary data (in protobuf wire format).
+ * @param {jspb.ByteSource} bytes The bytes to deserialize.
+ * @return {!proto.a6.Response}
+ */
+proto.a6.Response.deserializeBinary = function(bytes) {
+  var reader = new jspb.BinaryReader(bytes);
+  var msg = new proto.a6.Response;
+  return proto.a6.Response.deserializeBinaryFromReader(msg, reader);
+};
+
+
+/**
+ * Deserializes binary data (in protobuf wire format) from the
+ * given reader into the given message object.
+ * @param {!proto.a6.Response} msg The message object to deserialize into.
+ * @param {!jspb.BinaryReader} reader The BinaryReader to use.
+ * @return {!proto.a6.Response}
+ */
+proto.a6.Response.deserializeBinaryFromReader = function(msg, reader) {
+  while (reader.nextField()) {
+    if (reader.isEndGroup()) {
+      break;
+    }
+    var field = reader.getFieldNumber();
+    switch (field) {
+    case 1:
+      var value = /** @type {boolean} */ (reader.readBool());
+      msg.setStatus(value);
+      break;
+    case 2:
+      var value = new proto.a6.Route;
+      reader.readMessage(value,proto.a6.Route.deserializeBinaryFromReader);
+      msg.setRoute(value);
+      break;
+    case 3:
+      var value = msg.getRoutesMap();
+      reader.readMessage(value, function(message, reader) {
+        jspb.Map.deserializeBinary(message, reader, 
jspb.BinaryReader.prototype.readString, 
jspb.BinaryReader.prototype.readMessage, 
proto.a6.Route.deserializeBinaryFromReader, "", new proto.a6.Route());
+         });
+      break;
+    default:
+      reader.skipField();
+      break;
+    }
+  }
+  return msg;
+};
+
+
+/**
+ * Serializes the message to binary data (in protobuf wire format).
+ * @return {!Uint8Array}
+ */
+proto.a6.Response.prototype.serializeBinary = function() {
+  var writer = new jspb.BinaryWriter();
+  proto.a6.Response.serializeBinaryToWriter(this, writer);
+  return writer.getResultBuffer();
+};
+
+
+/**
+ * Serializes the given message to binary data (in protobuf wire
+ * format), writing to the given BinaryWriter.
+ * @param {!proto.a6.Response} message
+ * @param {!jspb.BinaryWriter} writer
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.a6.Response.serializeBinaryToWriter = function(message, writer) {
+  var f = undefined;
+  f = message.getStatus();
+  if (f) {
+    writer.writeBool(
+      1,
+      f
+    );
+  }
+  f = message.getRoute();
+  if (f != null) {
+    writer.writeMessage(
+      2,
+      f,
+      proto.a6.Route.serializeBinaryToWriter
+    );
+  }
+  f = message.getRoutesMap(true);
+  if (f && f.getLength() > 0) {
+    f.serializeBinary(3, writer, jspb.BinaryWriter.prototype.writeString, 
jspb.BinaryWriter.prototype.writeMessage, 
proto.a6.Route.serializeBinaryToWriter);
+  }
+};
+
+
+/**
+ * optional bool status = 1;
+ * @return {boolean}
+ */
+proto.a6.Response.prototype.getStatus = function() {
+  return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 
1, false));
+};
+
+
+/**
+ * @param {boolean} value
+ * @return {!proto.a6.Response} returns this
+ */
+proto.a6.Response.prototype.setStatus = function(value) {
+  return jspb.Message.setProto3BooleanField(this, 1, value);
+};
+
+
+/**
+ * optional Route route = 2;
+ * @return {?proto.a6.Route}
+ */
+proto.a6.Response.prototype.getRoute = function() {
+  return /** @type{?proto.a6.Route} */ (
+    jspb.Message.getWrapperField(this, proto.a6.Route, 2));
+};
+
+
+/**
+ * @param {?proto.a6.Route|undefined} value
+ * @return {!proto.a6.Response} returns this
+*/
+proto.a6.Response.prototype.setRoute = function(value) {
+  return jspb.Message.setWrapperField(this, 2, value);
+};
+
+
+/**
+ * Clears the message field making it undefined.
+ * @return {!proto.a6.Response} returns this
+ */
+proto.a6.Response.prototype.clearRoute = function() {
+  return this.setRoute(undefined);
+};
+
+
+/**
+ * Returns whether this field is set.
+ * @return {boolean}
+ */
+proto.a6.Response.prototype.hasRoute = function() {
+  return jspb.Message.getField(this, 2) != null;
+};
+
+
+/**
+ * map<string, Route> routes = 3;
+ * @param {boolean=} opt_noLazyCreate Do not create the map if
+ * empty, instead returning `undefined`
+ * @return {!jspb.Map<string,!proto.a6.Route>}
+ */
+proto.a6.Response.prototype.getRoutesMap = function(opt_noLazyCreate) {
+  return /** @type {!jspb.Map<string,!proto.a6.Route>} */ (
+      jspb.Message.getMapField(this, 3, opt_noLazyCreate,
+      proto.a6.Route));
+};
+
+
+/**
+ * Clears values from the map. The map will be non-null.
+ * @return {!proto.a6.Response} returns this
+ */
+proto.a6.Response.prototype.clearRoutesMap = function() {
+  this.getRoutesMap().clear();
+  return this;};
+
+
+goog.object.extend(exports, proto.a6);
diff --git a/t/plugin/grpc-web/client.js b/t/plugin/grpc-web/client.js
new file mode 100644
index 0000000..7f37ff0
--- /dev/null
+++ b/t/plugin/grpc-web/client.js
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+global.XMLHttpRequest = require('xhr2')
+
+const {Empty, Request, Route} = require('./a6/routes_pb')
+const {RouteServiceClient} = require('./a6/routes_grpc_web_pb')
+
+const FUNCTION_ALL = "ALL"
+const FUNCTION_GET = "GET"
+const FUNCTION_POST = "POST"
+const FUNCTION_PUT = "PUT"
+const FUNCTION_DEL = "DEL"
+const FUNCTION_FLUSH = "FLUSH"
+
+const functions = [FUNCTION_ALL, FUNCTION_GET, FUNCTION_POST, FUNCTION_PUT, 
FUNCTION_DEL, FUNCTION_FLUSH]
+
+class gRPCWebClient {
+    constructor() {
+        this.client = new RouteServiceClient("http://127.0.0.1:1984/grpc";, 
null, null)
+    };
+
+    flush() {
+        let request = new Empty()
+        this.client.flushAll(request, {}, function (error, response) {
+            if (error) {
+                console.log(error)
+                return
+            }
+            console.log(JSON.stringify(response.toObject().routesMap))
+        });
+    }
+
+    all() {
+        let request = new Empty()
+        this.client.getAll(request, {}, function (error, response) {
+            if (error) {
+                console.log(error)
+                return
+            }
+            console.log(JSON.stringify(response.toObject().routesMap))
+        });
+    }
+
+    get(params) {
+        if (params[0] === null) {
+            console.log("route ID invalid")
+            return
+        }
+        let request = new Request()
+        request.setId(params[0])
+        this.client.get(request, {}, function (error, response) {
+            if (error) {
+                console.log(error)
+                return
+            }
+            console.log(JSON.stringify(response.toObject().route))
+        });
+    }
+
+    post(params) {
+        if (params[0] === null) {
+            console.log("route ID invalid")
+            return
+        }
+        if (params[1] === null) {
+            console.log("route Name invalid")
+            return
+        }
+        if (params[2] === null) {
+            console.log("route Path invalid")
+            return
+        }
+        let request = new Request()
+        let route = new Route()
+        request.setId(params[0])
+        route.setName(params[1])
+        route.setPath(params[2])
+        request.setRoute(route)
+        this.client.insert(request, {}, function (error, response) {
+            if (error) {
+                console.log(error)
+                return
+            }
+            console.log(JSON.stringify(response.toObject().routesMap))
+        });
+    }
+
+    put(params) {
+        if (params[0] === null) {
+            console.log("route ID invalid")
+            return
+        }
+        if (params[1] === null) {
+            console.log("route Name invalid")
+            return
+        }
+        if (params[2] === null) {
+            console.log("route Path invalid")
+            return
+        }
+        let request = new Request()
+        let route = new Route()
+        request.setId(params[0])
+        route.setName(params[1])
+        route.setPath(params[2])
+        request.setRoute(route)
+        this.client.update(request, {}, function (error, response) {
+            if (error) {
+                console.log(error)
+                return
+            }
+            console.log(JSON.stringify(response.toObject().routesMap))
+        })
+    }
+
+    del() {
+        if (params[0] === null) {
+            console.log("route ID invalid")
+            return
+        }
+        let request = new Request()
+        request.setId(params[0])
+        this.client.remove(request, {}, function (error, response) {
+            if (error) {
+                console.log(error)
+                return
+            }
+            console.log(JSON.stringify(response.toObject().routesMap))
+        })
+    }
+}
+
+
+const arguments = process.argv.splice(2)
+
+if (arguments.length === 0) {
+    console.log("please input dispatch function, e.g: node client.js insert 
arg_id arg_name arg_path")
+    return
+}
+
+const func = arguments[0].toUpperCase()
+if (!functions.includes(func)) {
+    console.log("dispatch function not found")
+    return
+}
+
+const params = arguments.splice(1)
+
+let grpc = new gRPCWebClient();
+
+if (func === FUNCTION_GET) {
+    grpc.get(params)
+} else if (func === FUNCTION_POST) {
+    grpc.post(params)
+} else if (func === FUNCTION_PUT) {
+    grpc.put(params)
+} else if (func === FUNCTION_DEL) {
+    grpc.del(params)
+} else if (func === FUNCTION_FLUSH) {
+    grpc.flush()
+} else {
+    grpc.all()
+}
diff --git a/t/plugin/grpc-web/go.mod b/t/plugin/grpc-web/go.mod
new file mode 100644
index 0000000..cece5f1
--- /dev/null
+++ b/t/plugin/grpc-web/go.mod
@@ -0,0 +1,13 @@
+module apisix.apache.org/plugin/grpc-web
+
+go 1.16
+
+require (
+       github.com/golang/protobuf v1.4.3
+       github.com/satori/go.uuid v1.2.0 // indirect
+       golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
+       golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect
+       golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
+       golang.org/x/text v0.3.6 // indirect
+       google.golang.org/grpc v1.43.0
+)
diff --git a/t/plugin/grpc-web/go.sum b/t/plugin/grpc-web/go.sum
new file mode 100644
index 0000000..626dadf
--- /dev/null
+++ b/t/plugin/grpc-web/go.sum
@@ -0,0 +1,125 @@
+cloud.google.com/go v0.26.0/go.mod 
h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod 
h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod 
h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/antihax/optional v1.0.0/go.mod 
h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod 
h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod 
h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/client9/misspell v0.3.4/go.mod 
h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod 
h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod 
h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod 
h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
+github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod 
h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod 
h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod 
h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/davecgh/go-spew v1.1.0/go.mod 
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod 
h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane 
v0.9.1-0.20191026205805-5f8ba28d4473/go.mod 
h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod 
h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane 
v0.9.9-0.20201210154907-fd9021fe5dad/go.mod 
h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane 
v0.9.10-0.20210907150352-cf90f659a021/go.mod 
h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod 
h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/ghodss/yaml v1.0.0/go.mod 
h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod 
h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod 
h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod 
h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod 
h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod 
h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod 
h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod 
h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod 
h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod 
h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod 
h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod 
h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod 
h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3 
h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
+github.com/golang/protobuf v1.4.3/go.mod 
h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/google/go-cmp v0.2.0/go.mod 
h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod 
h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod 
h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod 
h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
+github.com/google/go-cmp v0.5.0/go.mod 
h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/uuid v1.1.2/go.mod 
h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod 
h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/pmezard/go-difflib v1.0.0/go.mod 
h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod 
h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod 
h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/satori/go.uuid v1.2.0 
h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
+github.com/satori/go.uuid v1.2.0/go.mod 
h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/stretchr/objx v0.1.0/go.mod 
h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.5.1/go.mod 
h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0/go.mod 
h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod 
h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod 
h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod 
h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod 
h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod 
h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod 
h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod 
h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod 
h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod 
h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod 
h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod 
h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod 
h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod 
h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20200822124328-c89045814202 
h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod 
h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20211216030914-fe4d6282115f 
h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM=
+golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod 
h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod 
h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod 
h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod 
h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod 
h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd 
h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod 
h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod 
h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod 
h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod 
h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod 
h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod 
h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod 
h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 
h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod 
h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod 
h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod 
h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod 
h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod 
h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod 
h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 
h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod 
h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/grpc v1.19.0/go.mod 
h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod 
h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod 
h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod 
h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.1/go.mod 
h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.36.0/go.mod 
h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.43.0 h1:Eeu7bZtDZ2DpRCsLhUlcrLnvYaMK1Gz86a+hMVvELmM=
+google.golang.org/grpc v1.43.0/go.mod 
h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod 
h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod 
h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod 
h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod 
h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod 
h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod 
h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod 
h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod 
h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0 
h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
+google.golang.org/protobuf v1.25.0/go.mod 
h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod 
h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod 
h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod 
h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod 
h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/t/plugin/grpc-web/package-lock.json 
b/t/plugin/grpc-web/package-lock.json
new file mode 100644
index 0000000..dade617
--- /dev/null
+++ b/t/plugin/grpc-web/package-lock.json
@@ -0,0 +1,52 @@
+{
+  "name": "apisix-grpc-web",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "apisix.apache.org/plugin/grpc-web",
+      "dependencies": {
+        "google-protobuf": "^3.19.1",
+        "grpc-web": "^1.3.0",
+        "xhr2": "^0.2.1"
+      }
+    },
+    "node_modules/google-protobuf": {
+      "version": "3.19.1",
+      "resolved": 
"https://registry.npmmirror.com/google-protobuf/download/google-protobuf-3.19.1.tgz?cache=0&sync_timestamp=1635869461201&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fgoogle-protobuf%2Fdownload%2Fgoogle-protobuf-3.19.1.tgz";,
+      "integrity": "sha1-WvU5DoIGxEbY9J/rr/1Lf0rCj0E=",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/grpc-web": {
+      "version": "1.3.0",
+      "resolved": 
"https://registry.npmmirror.com/grpc-web/download/grpc-web-1.3.0.tgz";,
+      "integrity": "sha1-TDbZfnp7YQKn30Y+eCLNhtT2Xtg=",
+      "license": "Apache-2.0"
+    },
+    "node_modules/xhr2": {
+      "version": "0.2.1",
+      "resolved": 
"https://registry.npm.taobao.org/xhr2/download/xhr2-0.2.1.tgz";,
+      "integrity": "sha1-TnOtxPnP7Jy9IVf3Pv3OOl8QipM=",
+      "engines": {
+        "node": ">= 6"
+      }
+    }
+  },
+  "dependencies": {
+    "google-protobuf": {
+      "version": "3.19.1",
+      "resolved": 
"https://registry.npmmirror.com/google-protobuf/download/google-protobuf-3.19.1.tgz?cache=0&sync_timestamp=1635869461201&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fgoogle-protobuf%2Fdownload%2Fgoogle-protobuf-3.19.1.tgz";,
+      "integrity": "sha1-WvU5DoIGxEbY9J/rr/1Lf0rCj0E="
+    },
+    "grpc-web": {
+      "version": "1.3.0",
+      "resolved": 
"https://registry.npmmirror.com/grpc-web/download/grpc-web-1.3.0.tgz";,
+      "integrity": "sha1-TDbZfnp7YQKn30Y+eCLNhtT2Xtg="
+    },
+    "xhr2": {
+      "version": "0.2.1",
+      "resolved": 
"https://registry.npm.taobao.org/xhr2/download/xhr2-0.2.1.tgz";,
+      "integrity": "sha1-TnOtxPnP7Jy9IVf3Pv3OOl8QipM="
+    }
+  }
+}
diff --git a/t/plugin/grpc-web/package.json b/t/plugin/grpc-web/package.json
new file mode 100644
index 0000000..29b035c
--- /dev/null
+++ b/t/plugin/grpc-web/package.json
@@ -0,0 +1,8 @@
+{
+  "name": "apisix-grpc-web",
+  "dependencies": {
+    "google-protobuf": "^3.19.1",
+    "grpc-web": "^1.3.0",
+    "xhr2": "^0.2.1"
+  }
+}
diff --git a/t/plugin/grpc-web/server.go b/t/plugin/grpc-web/server.go
new file mode 100644
index 0000000..100b957
--- /dev/null
+++ b/t/plugin/grpc-web/server.go
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+import (
+       "flag"
+       "log"
+       "net"
+
+       "apisix.apache.org/plugin/grpc-web/a6"
+       "google.golang.org/grpc"
+       "google.golang.org/grpc/reflection"
+)
+
+var grpcListenAddress string
+
+func init() {
+       flag.StringVar(&grpcListenAddress, "listen", ":50001", "address for 
grpc")
+}
+
+func main() {
+       flag.Parse()
+       listen, err := net.Listen("tcp", grpcListenAddress)
+       if err != nil {
+               log.Fatalf("failed to listen gRPC-Web Test Server: %v", err)
+       } else {
+               log.Printf("successful to listen gRPC-Web Test Server, address 
%s", grpcListenAddress)
+       }
+
+       s := a6.RouteServer{}
+       grpcServer := grpc.NewServer()
+       reflection.Register(grpcServer)
+       a6.RegisterRouteServiceServer(grpcServer, &s)
+
+       if err = grpcServer.Serve(listen); err != nil {
+               log.Fatalf("failed to serve: %v", err)
+       }
+}
diff --git a/t/plugin/grpc-web/setup.sh b/t/plugin/grpc-web/setup.sh
new file mode 100755
index 0000000..4305ee4
--- /dev/null
+++ b/t/plugin/grpc-web/setup.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+set -ex
+
+npm install
+
+CGO_ENABLED=0 go build -o grpc-web-server server.go
+
+./grpc-web-server > grpc-web-server.log 2>&1 || (cat grpc-web-server.log && 
exit 1)&

Reply via email to