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

membphis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new 3a9e0fc  feature: add batch request plugin. (#1388)
3a9e0fc is described below

commit 3a9e0fc675df14a771f8d6b79cb4a8f1106906e5
Author: Vinci Xu <277040...@qq.com>
AuthorDate: Wed Apr 29 21:40:45 2020 +0800

    feature: add batch request plugin. (#1388)
---
 README.md                         |   1 +
 README_CN.md                      |   1 +
 apisix/plugins/batch-requests.lua | 241 +++++++++++++
 conf/config.yaml                  |   1 +
 doc/README.md                     |   1 +
 doc/README_CN.md                  |   1 +
 doc/plugins/batch-requests-cn.md  | 135 ++++++++
 doc/plugins/batch-requests.md     | 135 ++++++++
 t/admin/plugins.t                 |   2 +-
 t/debug/debug-mode.t              |   1 +
 t/plugin/batch-requests.t         | 686 ++++++++++++++++++++++++++++++++++++++
 11 files changed, 1204 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index d29a57d..a0102b4 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,7 @@ A/B testing, canary release, blue-green deployment, limit 
rate, defense against
     - IPv6: Use IPv6 to match route.
     - Support [TTL](doc/admin-api-cn.md#route)
     - [Support priority](doc/router-radixtree.md#3-match-priority)
+    - [Support Batch Http Requests](doc/plugins/batch-requests.md)
 
 - **Security**
     - Authentications: [key-auth](doc/plugins/key-auth.md), 
[JWT](doc/plugins/jwt-auth.md), [basic-auth](doc/plugins/basic-auth.md), 
[wolf-rbac](doc/plugins/wolf-rbac.md)
diff --git a/README_CN.md b/README_CN.md
index c09db06..d3e7909 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -81,6 +81,7 @@ A/B 测试、金丝雀发布(灰度发布)、蓝绿部署、限流限速、抵
     - IPv6:支持使用 IPv6 格式匹配路由
     - 支持路由的[自动过期(TTL)](doc/admin-api-cn.md#route)
     - [支持路由的优先级](doc/router-radixtree.md#3-match-priority)
+    - [支持批量 Http 请求](doc/plugins/batch-requests-cn.md)
 
 - **安全防护**
     - 多种身份认证方式: [key-auth](doc/plugins/key-auth-cn.md), 
[JWT](doc/plugins/jwt-auth-cn.md), [basic-auth](doc/plugins/basic-auth-cn.md), 
[wolf-rbac](doc/plugins/wolf-rbac-cn.md)。
diff --git a/apisix/plugins/batch-requests.lua 
b/apisix/plugins/batch-requests.lua
new file mode 100644
index 0000000..34d784a
--- /dev/null
+++ b/apisix/plugins/batch-requests.lua
@@ -0,0 +1,241 @@
+--
+-- 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 core    = require("apisix.core")
+local http    = require("resty.http")
+local ngx     = ngx
+local io_open = io.open
+local ipairs  = ipairs
+local pairs   = pairs
+
+local plugin_name = "batch-requests"
+
+local schema = {
+    type = "object",
+    additionalProperties = false,
+}
+
+local req_schema = {
+    type = "object",
+    properties = {
+        query = {
+            description = "pipeline query string",
+            type = "object"
+        },
+        headers = {
+            description = "pipeline header",
+            type = "object"
+        },
+        timeout = {
+            description = "pipeline timeout(ms)",
+            type = "integer",
+            default = 30000,
+        },
+        pipeline = {
+            type = "array",
+            minItems = 1,
+            items = {
+                type = "object",
+                properties = {
+                    version = {
+                        description = "HTTP version",
+                        type = "number",
+                        enum = {1.0, 1.1},
+                        default = 1.1,
+                    },
+                    method = {
+                        description = "HTTP method",
+                        type = "string",
+                        enum = {"GET", "POST", "PUT", "DELETE", "PATCH", 
"HEAD",
+                                "OPTIONS", "CONNECT", "TRACE"},
+                        default = "GET"
+                    },
+                    path = {
+                        type = "string",
+                        minLength = 1,
+                    },
+                    query = {
+                        description = "request header",
+                        type = "object",
+                    },
+                    headers = {
+                        description = "request query string",
+                        type = "object",
+                    },
+                    ssl_verify = {
+                        type = "boolean",
+                        default = false
+                    },
+                }
+            }
+        }
+    },
+    anyOf = {
+        {required = {"pipeline"}},
+    },
+}
+
+local _M = {
+    version = 0.1,
+    priority = 4010,
+    name = plugin_name,
+    schema = schema
+}
+
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+    if not ok then
+        return false, err
+    end
+    return true
+end
+
+
+local function check_input(data)
+    local ok, err = core.schema.check(req_schema, data)
+    if not ok then
+        return 400, {error_msg = "bad request body: " .. err}
+    end
+end
+
+
+local function set_common_header(data)
+    if not data.headers then
+        return
+    end
+
+    for i,req in ipairs(data.pipeline) do
+        if not req.headers then
+            req.headers = data.headers
+        else
+            for k, v in pairs(data.headers) do
+                if not req.headers[k] then
+                    req.headers[k] = v
+                end
+            end
+        end
+    end
+end
+
+
+local function set_common_query(data)
+    if not data.query then
+        return
+    end
+
+    for i,req in ipairs(data.pipeline) do
+        if not req.query then
+            req.query = data.query
+        else
+            for k, v in pairs(data.query) do
+                if not req.query[k] then
+                    req.query[k] = v
+                end
+            end
+        end
+    end
+end
+
+
+local function get_file(file_name)
+    local f = io_open(file_name, 'r')
+    if f then
+        local req_body = f:read("*all")
+        f:close()
+        return req_body
+    end
+
+    return
+end
+
+
+local function batch_requests()
+    ngx.req.read_body()
+    local req_body = ngx.req.get_body_data()
+    if not req_body then
+        local file_name = ngx.req.get_body_file()
+        if file_name then
+            req_body = get_file(file_name)
+        end
+
+        if not req_body then
+            core.response.exit(400, {
+                error_msg = "no request body, you should give at least one 
pipeline setting"
+            })
+        end
+    end
+
+    local data, err = core.json.decode(req_body)
+    if not data then
+        core.response.exit(400, {
+            error_msg = "invalid request body: " .. req_body .. ", err: " .. 
err
+        })
+    end
+
+    local code, body = check_input(data)
+    if code then
+        core.response.exit(code, body)
+    end
+
+    local httpc = http.new()
+    httpc:set_timeout(data.timeout)
+    local ok, err = httpc:connect("127.0.0.1", ngx.var.server_port)
+    if not ok then
+        core.response.exit(500, {error_msg = "connect to apisix failed: " .. 
err})
+    end
+
+    set_common_header(data)
+    set_common_query(data)
+    local responses, err = httpc:request_pipeline(data.pipeline)
+    if not responses then
+        core.response.exit(400, {error_msg = "request failed: " .. err})
+    end
+
+    local aggregated_resp = {}
+    for _, resp in ipairs(responses) do
+        if not resp.status then
+            core.table.insert(aggregated_resp, {
+                status = 504,
+                reason = "upstream timeout"
+            })
+        end
+        local sub_resp = {
+            status  = resp.status,
+            reason  = resp.reason,
+            headers = resp.headers,
+        }
+        if resp.has_body then
+            sub_resp.body = resp:read_body()
+        end
+        core.table.insert(aggregated_resp, sub_resp)
+    end
+    core.response.exit(200, aggregated_resp)
+end
+
+
+function _M.api()
+    return {
+        {
+            methods = {"POST"},
+            uri = "/apisix/batch-requests",
+            handler = batch_requests,
+        }
+    }
+end
+
+
+return _M
diff --git a/conf/config.yaml b/conf/config.yaml
index ee1d698..3b16de1 100644
--- a/conf/config.yaml
+++ b/conf/config.yaml
@@ -145,5 +145,6 @@ plugins:                          # plugin list
   - proxy-mirror
   - kafka-logger
   - cors
+  - batch-requests
 stream_plugins:
   - mqtt-proxy
diff --git a/doc/README.md b/doc/README.md
index 561d3a0..238e198 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -64,6 +64,7 @@ Plugins
 * [proxy-mirror](plugins/proxy-mirror.md): Provides the ability to mirror 
client requests.
 * [kafka-logger](plugins/kafka-logger.md): Log requests to External Kafka 
servers.
 * [cors](plugins/cors.md): Enable CORS(Cross-origin resource sharing) for your 
API.
+* [batch-requests](plugins/batch-requests.md): Allow you send mutiple http api 
via **http pipeline**.
 
 Deploy to the Cloud
 =======
diff --git a/doc/README_CN.md b/doc/README_CN.md
index bf31414..1fc08c5 100644
--- a/doc/README_CN.md
+++ b/doc/README_CN.md
@@ -65,3 +65,4 @@ Reference document
 * [tcp-logger](plugins/tcp-logger.md): 将请求记录到TCP服务器
 * [kafka-logger](plugins/kafka-logger-cn.md): 将请求记录到外部Kafka服务器。
 * [cors](plugins/cors-cn.md): 为你的API启用CORS.
+* [batch-requests](plugins/batch-requests-cn.md): 以 **http pipeline** 
的方式在网关一次性发起多个 `http` 请求。
diff --git a/doc/plugins/batch-requests-cn.md b/doc/plugins/batch-requests-cn.md
new file mode 100644
index 0000000..dc06e86
--- /dev/null
+++ b/doc/plugins/batch-requests-cn.md
@@ -0,0 +1,135 @@
+<!--
+#
+# 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.
+#
+-->
+
+# [English](batch-requests.md)
+
+# 目录
+
+- [**简介**](#简介)
+- [**属性**](#属性)
+- [**如何启用**](#如何启用)
+- [**批量接口请求/响应**](#批量接口请求/响应)
+- [**测试插件**](#测试插件)
+- [**禁用插件**](#禁用插件)
+
+## 简介
+
+`batch-requests` 插件可以一次接受多个请求并以 [http 
pipeline](https://en.wikipedia.org/wiki/HTTP_pipelining) 
的方式在网关发起多个http请求,合并结果后再返回客户端,这在客户端需要访问多个接口时可以显著地提升请求性能。
+
+## 属性
+
+无
+
+## 如何启用
+
+本插件默认启用。
+
+## 批量接口请求/响应
+插件会为 `apisix` 创建一个 `/apisix/batch-requests` 的接口来处理你的批量请求。
+
+### 接口请求参数:
+
+| 参数名 | 类型 | 可选 | 默认值 | 描述 |
+| --- | --- | --- | --- | --- |
+| query | Object | Yes | | 给所有请求都携带的 `QueryString` |
+| headers | Object | Yes | | 给所有请求都携带的 `Header` |
+| timeout | Number | Yes | 3000 | 聚合请求的超时时间,单位为 `ms` |
+| pipeline | [HttpRequest](#Request) | No | | Http 请求的详细信息 |
+
+#### HttpRequest
+| 参数名 | 类型 | 可选 | 默认值 | 描述 |
+| --- | --- | --- | --- | --- |
+| version | Enum | Yes | 1.1 | 请求用的 `http` 协议版本,可以使用 `1.0` or `1.1` |
+| method | Enum | Yes | GET | 请求使用的 `http` 方法,例如:`GET`. |
+| query | Object | Yes | | 独立请求所携带的 `QueryString`, 如果 `Key` 和全局的有冲突,以此设置为主。 |
+| headers | Object | Yes | | 独立请求所携带的 `Header`, 如果 `Key` 和全局的有冲突,以此设置为主。 |
+| path | String | No | | 请求路径 |
+| body | String | Yes | | 请求体 |
+
+### 接口响应参数:
+返回值为一个 [HttpResponse](#HttpResponse) 的 `数组`。
+
+#### HttpResponse
+| 参数名 | 类型 | 描述 |
+| --- | --- | --- | --- | --- |
+| status | Integer | Http 请求的状态码 |
+| reason | String | Http 请求的返回信息 |
+| body | String | Http 请求的响应体 |
+| headers | Object | Http 请求的响应头 |
+
+## 测试插件
+
+你可以将要访问的请求信息传到网关的批量请求接口( `/apisix/batch-requests` ),网关会以 [http 
pipeline](https://en.wikipedia.org/wiki/HTTP_pipelining) 的方式自动帮你完成请求。
+```shell
+curl --location --request POST 'http://127.0.0.1:9080/apisix/batch-requests' \
+--header 'Content-Type: application/json' \
+--d '{
+    "headers": {
+        "Content-Type": "application/json",
+        "admin-jwt":"xxxx"
+    },
+    "timeout": 500,
+    "pipeline": [
+        {
+            "method": "POST",
+            "path": "/community.GiftSrv/GetGifts",
+            "body": "test"
+        },
+        {
+            "method": "POST",
+            "path": "/community.GiftSrv/GetGifts",
+            "body": "test2"
+        }
+    ]
+}'
+```
+
+返回如下:
+```json
+[
+    {
+        "status": 200,
+        "reason": "OK",
+        "body": 
"{\"ret\":500,\"msg\":\"error\",\"game_info\":null,\"gift\":[],\"to_gets\":0,\"get_all_msg\":\"\"}",
+        "headers": {
+            "Connection": "keep-alive",
+            "Date": "Sat, 11 Apr 2020 17:53:20 GMT",
+            "Content-Type": "application/json",
+            "Content-Length": "81",
+            "Server": "APISIX web server"
+        }
+    },
+    {
+        "status": 200,
+        "reason": "OK",
+        "body": 
"{\"ret\":500,\"msg\":\"error\",\"game_info\":null,\"gift\":[],\"to_gets\":0,\"get_all_msg\":\"\"}",
+        "headers": {
+            "Connection": "keep-alive",
+            "Date": "Sat, 11 Apr 2020 17:53:20 GMT",
+            "Content-Type": "application/json",
+            "Content-Length": "81",
+            "Server": "APISIX web server"
+        }
+    }
+]
+```
+
+## 禁用插件
+
+正常来说你不需要禁用本插件,如果有特殊情况,请从 `/conf/config.yaml` 的 `plugins` 节点中移除即可。
diff --git a/doc/plugins/batch-requests.md b/doc/plugins/batch-requests.md
new file mode 100644
index 0000000..081c590
--- /dev/null
+++ b/doc/plugins/batch-requests.md
@@ -0,0 +1,135 @@
+<!--
+#
+# 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.
+#
+-->
+
+# [Chinese](batch-requests-cn.md)
+
+# Summary
+
+- [**Description**](#Description)
+- [**Attributes**](#Attributes)
+- [**How To Enable**](#how-to-Enable)
+- [**Batch Api Request/Response**](#batch-api-request/response)
+- [**Test Plugin**](#test-plugin)
+- [**Disable Plugin**](#disable-plugin)
+
+## Description
+
+`batch-requests` can accept mutiple request and send them from `apisix` via 
[http pipeline](https://en.wikipedia.org/wiki/HTTP_pipelining),and return a 
aggregated response to client,this can significantly improve performance when 
the client needs to access multiple APIs.
+
+## Attributes
+
+None
+
+## How To Enable
+
+Default enbaled
+
+## Batch Api Request/Response
+The plugin will create a api in `apisix` to handle your aggregation request.
+
+### Batch Api Request:
+
+| ParameterName | Type | Optional | Default | Description |
+| --- | --- | --- | --- | --- |
+| query | Object | Yes | | Specify `QueryString` for all request |
+| headers | Object | Yes | | Specify `Header` for all request |
+| timeout | Number | Yes | 3000 | Aggregate Api timeout in `ms` |
+| pipeline | [HttpRequest](#Request) | No | | Request's detail |
+
+#### HttpRequest
+| ParameterName | Type | Optional | Default | Description |
+| --- | --- | --- | --- | --- |
+| version | Enum | Yes | 1.1 | http version: `1.0` or `1.1` |
+| method | Enum | Yes | GET | http method, such as:`GET`. |
+| query | Object | Yes | | request's `QueryString`, if `Key` is conflicted 
with global `query`, this setting's value will be setted.|
+| headers | Object | Yes | | request's `Header`, if `Key` is conflicted with 
global `headers`, this setting's value will be setted.|
+| path | String | No | | http request's path |
+| body | String | Yes | | http request's body |
+
+### Batch Api Response:
+Response is `Array` of [HttpResponse](#HttpResponse).
+
+#### HttpResponse
+| ParameterName | Type | Description |
+| --- | --- | --- | --- | --- |
+| status | Integer | http status code |
+| reason | String | http reason phrase |
+| body | String | http response body |
+| headers | Object | http response headers |
+
+## Test Plugin
+
+You can pass your request detail to batch api( `/apisix/batch-requests` ), 
`apisix` can automatically complete requests via [http 
pipeline](https://en.wikipedia.org/wiki/HTTP_pipelining). Such as:
+```shell
+curl --location --request POST 'http://127.0.0.1:9080/apisix/batch-requests' \
+--header 'Content-Type: application/json' \
+--d '{
+    "headers": {
+        "Content-Type": "application/json",
+        "admin-jwt":"xxxx"
+    },
+    "timeout": 500,
+    "pipeline": [
+        {
+            "method": "POST",
+            "path": "/community.GiftSrv/GetGifts",
+            "body": "test"
+        },
+        {
+            "method": "POST",
+            "path": "/community.GiftSrv/GetGifts",
+            "body": "test2"
+        }
+    ]
+}'
+```
+
+response as below:
+```json
+[
+    {
+        "status": 200,
+        "reason": "OK",
+        "body": 
"{\"ret\":500,\"msg\":\"error\",\"game_info\":null,\"gift\":[],\"to_gets\":0,\"get_all_msg\":\"\"}",
+        "headers": {
+            "Connection": "keep-alive",
+            "Date": "Sat, 11 Apr 2020 17:53:20 GMT",
+            "Content-Type": "application/json",
+            "Content-Length": "81",
+            "Server": "APISIX web server"
+        }
+    },
+    {
+        "status": 200,
+        "reason": "OK",
+        "body": 
"{\"ret\":500,\"msg\":\"error\",\"game_info\":null,\"gift\":[],\"to_gets\":0,\"get_all_msg\":\"\"}",
+        "headers": {
+            "Connection": "keep-alive",
+            "Date": "Sat, 11 Apr 2020 17:53:20 GMT",
+            "Content-Type": "application/json",
+            "Content-Length": "81",
+            "Server": "APISIX web server"
+        }
+    }
+]
+```
+
+## Disable Plugin
+
+Normally, you don't need to disable this plugin.If you does need please remove 
it from the `plugins` section of`/conf/config.yaml`.
diff --git a/t/admin/plugins.t b/t/admin/plugins.t
index 20ee9ed..1193987 100644
--- a/t/admin/plugins.t
+++ b/t/admin/plugins.t
@@ -30,7 +30,7 @@ __DATA__
 --- request
 GET /apisix/admin/plugins/list
 --- response_body_like eval
-qr/\["limit-req","limit-count","limit-conn","key-auth","basic-auth","prometheus","node-status","jwt-auth","zipkin","ip-restriction","grpc-transcode","serverless-pre-function","serverless-post-function","openid-connect","proxy-rewrite","redirect","response-rewrite","fault-injection","udp-logger","wolf-rbac","proxy-cache","tcp-logger","proxy-mirror","kafka-logger","cors"\]/
+qr/\["limit-req","limit-count","limit-conn","key-auth","basic-auth","prometheus","node-status","jwt-auth","zipkin","ip-restriction","grpc-transcode","serverless-pre-function","serverless-post-function","openid-connect","proxy-rewrite","redirect","response-rewrite","fault-injection","udp-logger","wolf-rbac","proxy-cache","tcp-logger","proxy-mirror","kafka-logger","cors","batch-requests"\]/
 --- no_error_log
 [error]
 
diff --git a/t/debug/debug-mode.t b/t/debug/debug-mode.t
index 0acdb82..dcd66e5 100644
--- a/t/debug/debug-mode.t
+++ b/t/debug/debug-mode.t
@@ -57,6 +57,7 @@ qr/loaded plugin and sort by priority: [-\d]+ name: [\w-]+/
 --- grep_error_log_out
 loaded plugin and sort by priority: 11000 name: fault-injection
 loaded plugin and sort by priority: 10000 name: serverless-pre-function
+loaded plugin and sort by priority: 4010 name: batch-requests
 loaded plugin and sort by priority: 4000 name: cors
 loaded plugin and sort by priority: 3000 name: ip-restriction
 loaded plugin and sort by priority: 2599 name: openid-connect
diff --git a/t/plugin/batch-requests.t b/t/plugin/batch-requests.t
new file mode 100644
index 0000000..9c784a6
--- /dev/null
+++ b/t/plugin/batch-requests.t
@@ -0,0 +1,686 @@
+#
+# 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';
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+log_level("info");
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: sanity
+--- config
+    location = /aggregate {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/batch-requests',
+                 ngx.HTTP_POST,
+                 [=[{
+                    "query": {
+                        "base": "base_query",
+                        "conflict": "query_value"
+                    },
+                    "headers": {
+                        "Base-Header": "base",
+                        "Conflict-Header": "header_value"
+                    },
+                    "pipeline":[
+                    {
+                        "path": "/b",
+                        "headers": {
+                            "Header1": "hello",
+                            "Header2": "world",
+                            "Conflict-Header": "b-header-value"
+                        }
+                    },{
+                        "path": "/c",
+                        "method": "PUT"
+                    },{
+                        "path": "/d",
+                        "query": {
+                            "one": "thing",
+                            "conflict": "d_value"
+                        }
+                    }]
+                }]=],
+                [=[[
+                {
+                    "status": 200,
+                    "body":"B",
+                    "headers": {
+                        "Base-Header": "base",
+                        "Base-Query": "base_query",
+                        "X-Res": "B",
+                        "X-Header1": "hello",
+                        "X-Header2": "world",
+                        "X-Conflict-Header": "b-header-value"
+                    }
+                },
+                {
+                    "status": 201,
+                    "body":"C",
+                    "headers": {
+                        "Base-Header": "base",
+                        "Base-Query": "base_query",
+                        "X-Res": "C",
+                        "X-Method": "PUT"
+                    }
+                },
+                {
+                    "status": 202,
+                    "body":"D",
+                    "headers": {
+                        "Base-Header": "base",
+                        "Base-Query": "base_query",
+                        "X-Res": "D",
+                        "X-Query-One": "thing",
+                        "X-Query-Conflict": "d_value"
+                    }
+                }
+                ]]=]
+                )
+
+            ngx.status = code
+            ngx.say(body)
+        }
+    }
+
+    location = /b {
+        content_by_lua_block {
+            ngx.status = 200
+            ngx.header["Base-Header"] = ngx.req.get_headers()["Base-Header"]
+            ngx.header["Base-Query"] = ngx.var.arg_base
+            ngx.header["X-Header1"] = ngx.req.get_headers()["Header1"]
+            ngx.header["X-Header2"] = ngx.req.get_headers()["Header2"]
+            ngx.header["X-Conflict-Header"] = 
ngx.req.get_headers()["Conflict-Header"]
+            ngx.header["X-Res"] = "B"
+            ngx.print("B")
+        }
+    }
+    location = /c {
+        content_by_lua_block {
+            ngx.status = 201
+            ngx.header["Base-Header"] = ngx.req.get_headers()["Base-Header"]
+            ngx.header["Base-Query"] = ngx.var.arg_base
+            ngx.header["X-Res"] = "C"
+            ngx.header["X-Method"] = ngx.req.get_method()
+            ngx.print("C")
+        }
+    }
+    location = /d {
+        content_by_lua_block {
+            ngx.status = 202
+            ngx.header["Base-Header"] = ngx.req.get_headers()["Base-Header"]
+            ngx.header["Base-Query"] = ngx.var.arg_base
+            ngx.header["X-Query-One"] = ngx.var.arg_one
+            ngx.header["X-Query-Conflict"] = ngx.var.arg_conflict
+            ngx.header["X-Res"] = "D"
+            ngx.print("D")
+        }
+    }
+--- request
+GET /aggregate
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 2: missing pipeling
+--- config
+    location = /aggregate {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/batch-requests',
+                ngx.HTTP_POST,
+                [=[{
+                    "pipeline1":[
+                    {
+                        "path": "/b",
+                        "headers": {
+                            "Header1": "hello",
+                            "Header2": "world"
+                        }
+                    },{
+                        "path": "/c",
+                        "method": "PUT"
+                    },{
+                        "path": "/d"
+                    }]
+                }]=]
+                )
+
+            ngx.status = code
+            ngx.print(body)
+        }
+    }
+--- request
+GET /aggregate
+--- error_code: 400
+--- response_body
+{"error_msg":"bad request body: object matches none of the requireds: 
[\"pipeline\"]"}
+--- no_error_log
+[error]
+
+
+
+=== TEST 3: timeout is not number
+--- config
+    location = /aggregate {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/batch-requests',
+                ngx.HTTP_POST,
+                [=[{
+                    "timeout": "200",
+                    "pipeline":[
+                    {
+                        "path": "/b",
+                        "headers": {
+                            "Header1": "hello",
+                            "Header2": "world"
+                        }
+                    },{
+                        "path": "/c",
+                        "method": "PUT"
+                    },{
+                        "path": "/d"
+                    }]
+                }]=]
+                )
+
+            ngx.status = code
+            ngx.print(body)
+        }
+    }
+--- request
+GET /aggregate
+--- error_code: 400
+--- response_body
+{"error_msg":"bad request body: property \"timeout\" validation failed: wrong 
type: expected integer, got string"}
+--- no_error_log
+[error]
+
+
+
+=== TEST 4: different response time
+--- config
+    location = /aggregate {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/batch-requests',
+                ngx.HTTP_POST,
+                [=[{
+                    "timeout": 2000,
+                    "pipeline":[
+                    {
+                        "path": "/b",
+                        "headers": {
+                            "Header1": "hello",
+                            "Header2": "world"
+                        }
+                    },{
+                        "path": "/c",
+                        "method": "PUT"
+                    },{
+                        "path": "/d"
+                    }]
+                }]=],
+                [=[[
+                {
+                    "status": 200
+                },
+                {
+                    "status": 201
+                },
+                {
+                    "status": 202
+                }
+                ]]=]
+                )
+
+            ngx.status = code
+            ngx.say(body)
+        }
+    }
+
+    location = /b {
+        content_by_lua_block {
+            ngx.sleep(0.02)
+            ngx.status = 200
+        }
+    }
+    location = /c {
+        content_by_lua_block {
+            ngx.sleep(0.05)
+            ngx.status = 201
+        }
+    }
+    location = /d {
+        content_by_lua_block {
+            ngx.sleep(1)
+            ngx.status = 202
+        }
+    }
+--- request
+GET /aggregate
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 5: last request timeout
+--- config
+    location = /aggregate {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/batch-requests',
+                ngx.HTTP_POST,
+                [=[{
+                    "timeout": 100,
+                    "pipeline":[
+                    {
+                        "path": "/b",
+                        "headers": {
+                            "Header1": "hello",
+                            "Header2": "world"
+                        }
+                    },{
+                        "path": "/c",
+                        "method": "PUT"
+                    },{
+                        "path": "/d"
+                    }]
+                }]=],
+                [=[[
+                {
+                    "status": 200
+                },
+                {
+                    "status": 201
+                },
+                {
+                    "status": 504,
+                    "reason": "upstream timeout"
+                }
+                ]]=]
+                )
+
+            ngx.status = code
+            ngx.say(body)
+        }
+    }
+
+    location = /b {
+        content_by_lua_block {
+            ngx.status = 200
+        }
+    }
+    location = /c {
+        content_by_lua_block {
+            ngx.status = 201
+        }
+    }
+    location = /d {
+        content_by_lua_block {
+            ngx.sleep(1)
+            ngx.status = 202
+        }
+    }
+--- request
+GET /aggregate
+--- response_body
+passed
+--- error_log
+timeout
+
+
+
+=== TEST 6: first request timeout
+--- config
+    location = /aggregate {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/batch-requests',
+                ngx.HTTP_POST,
+                [=[{
+                    "timeout": 100,
+                    "pipeline":[
+                    {
+                        "path": "/b",
+                        "headers": {
+                            "Header1": "hello",
+                            "Header2": "world"
+                        }
+                    },{
+                        "path": "/c",
+                        "method": "PUT"
+                    },{
+                        "path": "/d"
+                    }]
+                }]=],
+                [=[[
+                {
+                    "status": 504,
+                    "reason": "upstream timeout"
+                }
+                ]]=]
+                )
+
+            ngx.status = code
+            ngx.say(body)
+        }
+    }
+
+    location = /b {
+        content_by_lua_block {
+            ngx.sleep(1)
+            ngx.status = 200
+        }
+    }
+    location = /c {
+        content_by_lua_block {
+            ngx.status = 201
+        }
+    }
+    location = /d {
+        content_by_lua_block {
+            ngx.status = 202
+        }
+    }
+--- request
+GET /aggregate
+--- response_body
+passed
+--- error_log
+timeout
+
+
+
+=== TEST 7: no body in request
+--- config
+    location = /aggregate {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/batch-requests',
+                ngx.HTTP_POST,
+                nil,
+                nil
+                )
+
+            ngx.status = code
+            ngx.print(body)
+        }
+    }
+--- request
+GET /aggregate
+--- error_code: 400
+--- response_body
+{"error_msg":"no request body, you should give at least one pipeline setting"}
+--- no_error_log
+[error]
+
+
+
+=== TEST 8: invalid body
+--- config
+    location = /aggregate {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/batch-requests',
+                ngx.HTTP_POST,
+                "invaild json string"
+                )
+
+            ngx.status = code
+            ngx.print(body)
+        }
+    }
+--- request
+GET /aggregate
+--- error_code: 400
+--- response_body
+{"error_msg":"invalid request body: invaild json string, err: Expected value 
but found invalid token at character 1"}
+--- no_error_log
+[error]
+
+
+
+=== TEST 9: invalid pipeline's path
+--- config
+    location = /aggregate {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/batch-requests',
+                ngx.HTTP_POST,
+                [=[{
+                    "pipeline":[
+                    {
+                        "path": ""
+                    }]
+                }]=]
+                )
+
+            ngx.status = code
+            ngx.print(body)
+        }
+    }
+--- request
+GET /aggregate
+--- error_code: 400
+--- response_body
+{"error_msg":"bad request body: property \"pipeline\" validation failed: 
failed to validate item 1: property \"path\" validation failed: string too 
short, expected at least 1, got 0"}
+--- no_error_log
+[error]
+
+
+
+=== TEST 10: invalid pipeline's method
+--- config
+    location = /aggregate {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/batch-requests',
+                ngx.HTTP_POST,
+                [=[{
+                    "pipeline":[{
+                        "path": "/c",
+                        "method": "put"
+                    }]
+                }]=]
+                )
+
+            ngx.status = code
+            ngx.print(body)
+        }
+    }
+--- request
+GET /aggregate
+--- error_code: 400
+--- response_body
+{"error_msg":"bad request body: property \"pipeline\" validation failed: 
failed to validate item 1: property \"method\" validation failed: matches non 
of the enum values"}
+--- no_error_log
+[error]
+
+
+
+=== TEST 11: invalid pipeline's version
+--- config
+    location = /aggregate {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/batch-requests',
+                ngx.HTTP_POST,
+                [=[{
+                    "pipeline":[{
+                        "path": "/d",
+                        "version":1.2
+                    }]
+                }]=]
+                )
+
+            ngx.status = code
+            ngx.print(body)
+        }
+    }
+--- request
+GET /aggregate
+--- error_code: 400
+--- response_body
+{"error_msg":"bad request body: property \"pipeline\" validation failed: 
failed to validate item 1: property \"version\" validation failed: matches non 
of the enum values"}
+--- no_error_log
+[error]
+
+
+
+=== TEST 12: invalid pipeline's ssl
+--- config
+    location = /aggregate {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/batch-requests',
+                ngx.HTTP_POST,
+                [=[{
+                    "pipeline":[{
+                        "path": "/d",
+                        "ssl_verify":1.2
+                    }]
+                }]=]
+                )
+
+            ngx.status = code
+            ngx.print(body)
+        }
+    }
+--- request
+GET /aggregate
+--- error_code: 400
+--- response_body
+{"error_msg":"bad request body: property \"pipeline\" validation failed: 
failed to validate item 1: property \"ssl_verify\" validation failed: wrong 
type: expected boolean, got number"}
+--- no_error_log
+[error]
+
+
+
+=== TEST 13: invalid pipeline's number
+--- config
+    location = /aggregate {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/batch-requests',
+                ngx.HTTP_POST,
+                [=[{
+                    "pipeline":[]
+                }]=]
+                )
+            ngx.status = code
+            ngx.print(body)
+        }
+    }
+--- request
+GET /aggregate
+--- error_code: 400
+--- response_body
+{"error_msg":"bad request body: property \"pipeline\" validation failed: 
expect array to have at least 1 items"}
+--- no_error_log
+[error]
+
+
+
+=== TEST 14: when client body has been wrote to temp file
+--- config
+    client_body_in_file_only on;
+    location = /aggregate {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/batch-requests',
+                ngx.HTTP_POST,
+                [=[{
+                    "timeout": 100,
+                    "pipeline":[
+                    {
+                        "path": "/b",
+                        "headers": {
+                            "Header1": "hello",
+                            "Header2": "world"
+                        }
+                    },{
+                        "path": "/c",
+                        "method": "PUT"
+                    },{
+                        "path": "/d"
+                    }]
+                }]=],
+                [=[[
+                    {
+                        "status": 200
+                    },
+                    {
+                        "status": 201
+                    },
+                    {
+                        "status": 202
+                    }
+                ]]=]
+                )
+
+            ngx.status = code
+            ngx.say(body)
+        }
+    }
+
+    location = /b {
+        content_by_lua_block {
+            ngx.status = 200
+        }
+    }
+    location = /c {
+        content_by_lua_block {
+            ngx.status = 201
+        }
+    }
+    location = /d {
+        content_by_lua_block {
+            ngx.status = 202
+        }
+    }
+--- request
+GET /aggregate
+--- response_body
+passed
+--- no_error_log
+[error]

Reply via email to