diff --git a/README.md b/README.md index d29a57d144b7..a0102b429f02 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 c09db06bff76..d3e79098cbce 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 000000000000..34d784a89f97 --- /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 ee1d69853bd8..3b16de12fded 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 561d3a023df5..238e1983cb1f 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 bf3141400251..1fc08c5abccd 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 000000000000..dc06e862bdef --- /dev/null +++ b/doc/plugins/batch-requests-cn.md @@ -0,0 +1,135 @@ + + +# [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 000000000000..081c5904ddac --- /dev/null +++ b/doc/plugins/batch-requests.md @@ -0,0 +1,135 @@ + + +# [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 20ee9ed88f7c..11939872ff4c 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 0acdb8244347..dcd66e50d4ad 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 000000000000..9c784a6bd481 --- /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]