This is an automated email from the ASF dual-hosted git repository.
AlinsRan 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 4d4e0d477 feat: add exit-transformer plugin (#13343)
4d4e0d477 is described below
commit 4d4e0d4774385b9a244e4cafb64c1e8d34d20838
Author: AlinsRan <[email protected]>
AuthorDate: Tue May 12 11:50:16 2026 +0800
feat: add exit-transformer plugin (#13343)
---
apisix/core/response.lua | 76 +++++
apisix/plugins/exit-transformer.lua | 88 ++++++
conf/config.yaml.example | 1 +
docs/en/latest/config.json | 3 +-
docs/en/latest/plugins/exit-transformer.md | 142 +++++++++
docs/zh/latest/config.json | 3 +-
docs/zh/latest/plugins/exit-transformer.md | 142 +++++++++
t/plugin/exit-transformer.t | 482 +++++++++++++++++++++++++++++
8 files changed, 935 insertions(+), 2 deletions(-)
diff --git a/apisix/core/response.lua b/apisix/core/response.lua
index 55135fd5b..f1c22db8c 100644
--- a/apisix/core/response.lua
+++ b/apisix/core/response.lua
@@ -41,10 +41,27 @@ local tonumber = tonumber
local clear_tab = require("table.clear")
local pairs = pairs
local ngx_var = ngx.var
+local table = require("apisix.core.table")
local _M = {version = 0.1}
+--- Register a callback to intercept and transform exit responses.
+-- Callbacks are stored per-request in ngx.ctx and invoked by resp_exit in
+-- registration order. Each callback receives (code, body, headers, conf) and
+-- must return (new_code, new_body, new_headers).
+--
+-- @function core.response.exit_insert_callback
+-- @tparam function func Callback with signature (code, body, headers, conf).
+-- @tparam any conf Opaque value forwarded to the callback as its last
arg.
+function _M.exit_insert_callback(func, conf)
+ local ngx_ctx = ngx.ctx
+ local exit_callback_funcs = ngx_ctx.apisix_exit_callback_funcs or {}
+ table.insert_tail(exit_callback_funcs, func, conf)
+ ngx_ctx.apisix_exit_callback_funcs = exit_callback_funcs
+end
+
+
local resp_exit
do
local t = {}
@@ -60,6 +77,65 @@ function resp_exit(code, ...)
code = nil
end
+ -- When exit callbacks are registered, pass the body in its original form
+ -- (table or string) so callbacks can inspect and modify it directly.
+ local exit_callback_funcs = ngx.ctx.apisix_exit_callback_funcs
+ if exit_callback_funcs then
+ -- Extract primary body from varargs, preserving the original type.
+ local body
+ local nargs = select('#', ...)
+ for i = 1, nargs do
+ local v = select(i, ...)
+ if v ~= nil then
+ body = v
+ break
+ end
+ end
+ -- Include non-numeric first arg prepended before varargs, if any.
+ if body == nil and idx > 0 then
+ body = t[1]
+ end
+
+ local headers = {}
+
+ for i = 1, #exit_callback_funcs, 2 do
+ local callback_func = exit_callback_funcs[i]
+ local callback_conf = exit_callback_funcs[i + 1]
+ code, body, headers = callback_func(code, body, headers,
callback_conf)
+ end
+
+ if code then
+ ngx.status = code
+ end
+ if headers and table.nkeys(headers) > 0 then
+ for k, v in pairs(headers) do
+ ngx_header[k] = v
+ end
+ end
+ if body ~= nil then
+ if type(body) == "table" then
+ local encoded, err = encode_json(body)
+ if err then
+ error("failed to encode data: " .. err, -2)
+ end
+ ngx_print(encoded, "\n")
+ else
+ ngx_print(body)
+ end
+ end
+ if code then
+ local ctx = ngx.ctx.api_ctx
+ if ctx and not ctx._resp_source then
+ ctx._resp_source = "apisix"
+ end
+ if code >= 400 then
+ tracer.finish_all(ngx.ctx, tracer.status.ERROR, "response code
" .. code)
+ end
+ return ngx_exit(code)
+ end
+ return
+ end
+
if code then
ngx.status = code
end
diff --git a/apisix/plugins/exit-transformer.lua
b/apisix/plugins/exit-transformer.lua
new file mode 100644
index 000000000..f9797a1d9
--- /dev/null
+++ b/apisix/plugins/exit-transformer.lua
@@ -0,0 +1,88 @@
+--
+-- 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 lua_load = load
+local ipairs = ipairs
+local pcall = pcall
+
+local core = require("apisix.core")
+
+local lrucache = core.lrucache.new({
+ ttl = 300, count = 512
+})
+
+local schema = {
+ type = "object",
+ properties = {
+ functions = {
+ type = "array",
+ items = {
+ type = "string",
+ },
+ },
+ },
+ required = {"functions"},
+}
+
+local _M = {
+ version = 0.1,
+ priority = 22950,
+ name = "exit-transformer",
+ schema = schema
+}
+
+function _M.check_schema(conf)
+ local data_valid, err = core.schema.check(schema, conf)
+ if not data_valid then
+ return false, err
+ end
+ for _, lua_code_func in ipairs(conf.functions) do
+ local _ , err = lua_load(lua_code_func)
+ if err then
+ return false, err
+ end
+ end
+ return true
+end
+
+
+local function exit_callback(resp_code, resp_body, resp_header, lua_code_func)
+ local safe_loaded_func, err = lrucache(lua_code_func, nil, lua_load,
lua_code_func)
+ if err then
+ core.log.error("failed to load lua code: ", err)
+ return resp_code, resp_body, resp_header
+ end
+
+ local ok, err_or_new_resp_code, new_resp_body, new_resp_header
+ = pcall(safe_loaded_func, resp_code, resp_body, resp_header)
+ if not ok then
+ core.log.error("failed to run lua code: ", err_or_new_resp_code)
+ return resp_code, resp_body, resp_header
+ end
+
+ return err_or_new_resp_code, new_resp_body, new_resp_header
+end
+
+
+function _M.rewrite(conf)
+ for _, lua_code_func in ipairs(conf.functions) do
+ core.response.exit_insert_callback(exit_callback, lua_code_func)
+ end
+end
+
+
+return _M
diff --git a/conf/config.yaml.example b/conf/config.yaml.example
index 2e900bb18..9922d3c81 100644
--- a/conf/config.yaml.example
+++ b/conf/config.yaml.example
@@ -472,6 +472,7 @@ graphql:
plugins: # plugin list (sorted by priority)
- real-ip # priority: 23000
- ai # priority: 22900
+ #- exit-transformer # priority: 22950, disabled by default
- client-control # priority: 22000
- proxy-control # priority: 21990
- request-id # priority: 12015
diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json
index 836e607d2..6de6e1d92 100644
--- a/docs/en/latest/config.json
+++ b/docs/en/latest/config.json
@@ -113,7 +113,8 @@
"plugins/mocking",
"plugins/degraphql",
"plugins/body-transformer",
- "plugins/attach-consumer-label"
+ "plugins/attach-consumer-label",
+ "plugins/exit-transformer"
]
},
{
diff --git a/docs/en/latest/plugins/exit-transformer.md
b/docs/en/latest/plugins/exit-transformer.md
new file mode 100644
index 000000000..b36417ac1
--- /dev/null
+++ b/docs/en/latest/plugins/exit-transformer.md
@@ -0,0 +1,142 @@
+---
+title: exit-transformer
+keywords:
+ - Apache APISIX
+ - API Gateway
+ - Plugin
+ - exit-transformer
+ - error response transformation
+description: The exit-transformer Plugin intercepts APISIX-generated error
responses and transforms them using user-defined Lua functions before sending
to clients.
+---
+
+<!--
+#
+# 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.
+#
+-->
+
+<head>
+ <link rel="canonical" href="https://docs.api7.ai/hub/exit-transformer" />
+</head>
+
+## Description
+
+The `exit-transformer` Plugin intercepts responses generated by APISIX itself
— such as authentication failures, rate-limit rejections, or upstream errors —
and transforms them using user-defined Lua functions before sending to the
client.
+
+The Plugin registers callbacks that are invoked when `core.response.exit()` is
called, receiving the response `(status_code, body, headers)` as arguments and
returning the (possibly modified) values. Multiple functions can be chained,
and each function's output becomes the next function's input.
+
+:::note
+
+This Plugin only transforms responses generated by APISIX's own
`core.response.exit()` mechanism. It does **not** transform responses that
originate from upstream services.
+
+:::
+
+## Attributes
+
+| Name | Type | Required | Default | Valid values | Description |
+|------|------|----------|---------|--------------|-------------|
+| functions | array[string] | True | | | An array of Lua function source
strings. Each string must be a complete Lua chunk that returns a function. The
function receives `(status_code, body, headers)` and must return `status_code,
body, headers` (modified or unchanged). If a function throws an error, it is
logged and the original values are passed to the next function. |
+
+Each Lua function string must be a chunk that evaluates to a function with the
following signature:
+
+```lua
+return (function(code, body, header)
+ -- modify code, body, or header as needed
+ return code, body, header
+end)(...)
+```
+
+## Examples
+
+The examples below demonstrate how you can configure `exit-transformer` in
different scenarios.
+
+:::note
+
+```bash
+admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed
's/"//g')
+```
+
+:::
+
+### Remap Status Codes
+
+The following example demonstrates how to remap a `404 Not Found` response to
`405 Method Not Allowed`.
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "id": "exit-transformer-route",
+ "uri": "/anything",
+ "plugins": {
+ "key-auth": {},
+ "exit-transformer": {
+ "functions": [
+ "return (function(code, body, header) if code == 401 then return
403, body, header end return code, body, header end)(...)"
+ ]
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {"httpbin.org:80": 1}
+ }
+ }'
+```
+
+Send a request without an API key:
+
+```shell
+curl -i "http://127.0.0.1:9080/anything"
+```
+
+You should receive a `403 Forbidden` response instead of the default `401
Unauthorized`.
+
+### Normalize Error Response Format
+
+The following example demonstrates how to rewrite any error response body to a
consistent JSON format and add a custom header.
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "id": "exit-transformer-route",
+ "uri": "/anything",
+ "plugins": {
+ "key-auth": {},
+ "exit-transformer": {
+ "functions": [
+ "return (function(code, body, header) if code and code >= 400 then
header = header or {} header[\"X-Error-Code\"] = tostring(code) body = {error =
true, status = code, message = (type(body) == \"table\" and body.message) or
\"request failed\"} end return code, body, header end)(...)"
+ ]
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {"httpbin.org:80": 1}
+ }
+ }'
+```
+
+Send a request without an API key:
+
+```shell
+curl -i "http://127.0.0.1:9080/anything"
+```
+
+You should receive a `401` response with a normalized JSON body and the
`X-Error-Code: 401` header:
+
+```json
+{"error":true,"status":401,"message":"Missing API key in request"}
+```
diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json
index 1a66321fa..074c571eb 100644
--- a/docs/zh/latest/config.json
+++ b/docs/zh/latest/config.json
@@ -103,7 +103,8 @@
"plugins/mocking",
"plugins/degraphql",
"plugins/body-transformer",
- "plugins/attach-consumer-label"
+ "plugins/attach-consumer-label",
+ "plugins/exit-transformer"
]
},
{
diff --git a/docs/zh/latest/plugins/exit-transformer.md
b/docs/zh/latest/plugins/exit-transformer.md
new file mode 100644
index 000000000..90e755e5c
--- /dev/null
+++ b/docs/zh/latest/plugins/exit-transformer.md
@@ -0,0 +1,142 @@
+---
+title: exit-transformer
+keywords:
+ - Apache APISIX
+ - API 网关
+ - Plugin
+ - exit-transformer
+ - 错误响应转换
+description: exit-transformer 插件拦截 APISIX 生成的错误响应,并通过用户自定义的 Lua
函数对其进行转换后再发送给客户端。
+---
+
+<!--
+#
+# 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.
+#
+-->
+
+<head>
+ <link rel="canonical" href="https://docs.api7.ai/hub/exit-transformer" />
+</head>
+
+## 描述
+
+`exit-transformer` 插件拦截由 APISIX 自身生成的响应——例如认证失败、限流拒绝或上游错误——并在发送给客户端之前,通过用户自定义的
Lua 函数对其进行转换。
+
+该插件通过注册回调函数的方式工作:当 `core.response.exit()` 被调用时,回调函数依次执行,接收响应的 `(状态码, 响应体,
响应头)` 作为参数,并返回(可能已修改的)值。多个函数可以链式执行,前一个函数的输出作为下一个函数的输入。
+
+:::note
+
+该插件仅转换由 APISIX 自身 `core.response.exit()` 机制产生的响应,**不会**转换来自上游服务的响应。
+
+:::
+
+## 属性
+
+| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 |
+|------|------|--------|--------|--------|------|
+| functions | array[string] | 是 | | | Lua 函数源码字符串数组。每个字符串必须是一个完整的 Lua
代码块,该代码块须返回一个函数。函数接收 `(status_code, body, headers)` 三个参数,并必须返回 `status_code,
body, headers`(修改后的或原始值)。若函数抛出异常,错误将被记录到日志,原始值将被传递给下一个函数。 |
+
+每个 Lua 函数字符串必须是一个可求值为函数的代码块,函数签名如下:
+
+```lua
+return (function(code, body, header)
+ -- 按需修改 code、body 或 header
+ return code, body, header
+end)(...)
+```
+
+## 示例
+
+以下示例演示了如何在不同场景中使用 `exit-transformer` 插件。
+
+:::note
+
+```bash
+admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed
's/"//g')
+```
+
+:::
+
+### 重映射状态码
+
+以下示例演示如何将 `401 Unauthorized` 响应重映射为 `403 Forbidden`。
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "id": "exit-transformer-route",
+ "uri": "/anything",
+ "plugins": {
+ "key-auth": {},
+ "exit-transformer": {
+ "functions": [
+ "return (function(code, body, header) if code == 401 then return
403, body, header end return code, body, header end)(...)"
+ ]
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {"httpbin.org:80": 1}
+ }
+ }'
+```
+
+发送一个不带 API Key 的请求:
+
+```shell
+curl -i "http://127.0.0.1:9080/anything"
+```
+
+将收到 `403 Forbidden` 响应,而非默认的 `401 Unauthorized`。
+
+### 统一错误响应格式
+
+以下示例演示如何将所有错误响应体重写为统一的 JSON 格式,并添加自定义响应头。
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "id": "exit-transformer-route",
+ "uri": "/anything",
+ "plugins": {
+ "key-auth": {},
+ "exit-transformer": {
+ "functions": [
+ "return (function(code, body, header) if code and code >= 400 then
header = header or {} header[\"X-Error-Code\"] = tostring(code) body = {error =
true, status = code, message = (type(body) == \"table\" and body.message) or
\"request failed\"} end return code, body, header end)(...)"
+ ]
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {"httpbin.org:80": 1}
+ }
+ }'
+```
+
+发送一个不带 API Key 的请求:
+
+```shell
+curl -i "http://127.0.0.1:9080/anything"
+```
+
+将收到带有统一 JSON 格式响应体和 `X-Error-Code: 401` 响应头的 `401` 响应:
+
+```json
+{"error":true,"status":401,"message":"Missing API key in request"}
+```
diff --git a/t/plugin/exit-transformer.t b/t/plugin/exit-transformer.t
new file mode 100644
index 000000000..d9d8f6307
--- /dev/null
+++ b/t/plugin/exit-transformer.t
@@ -0,0 +1,482 @@
+#
+# 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();
+no_shuffle();
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ my $extra_yaml_config = <<_EOC_;
+plugins:
+ - exit-transformer
+ - key-auth
+ - limit-count
+_EOC_
+
+ if (!$block->extra_yaml_config) {
+ $block->set_value("extra_yaml_config", $extra_yaml_config);
+ }
+
+ if (!$block->request) {
+ $block->set_value("request", "GET /t");
+ }
+});
+
+run_tests();
+
+
+__DATA__
+
+=== TEST 1: failed schema check with invalid lua code
+--- config
+ location /t {
+ content_by_lua_block {
+ local plugin = require("apisix.plugins.exit-transformer")
+ local ok, err = plugin.check_schema({
+ functions = {
+ "return (function(code, body, header) if code == then
return 405 end return code, body, header end)(...)",
+ }
+ })
+
+ if not ok then
+ ngx.say(err)
+ else
+ ngx.say("passed")
+ end
+ }
+ }
+--- response_body eval
+qr/unexpected symbol/
+
+
+
+=== TEST 2: set plugin to convert 404 to 405
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/global_rules/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "exit-transformer": {
+ "functions": ["return (function(code, body,
header) if code == 404 then return 405 end return code, body, header end)(...)"]
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 3: hit route
+--- error_code: 405
+--- request
+GET /hello
+
+
+
+=== TEST 4: set plugin to convert 401 to 402 for auth plugins
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/global_rules/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "exit-transformer": {
+ "functions": ["return (function(code, body,
header) if code == 401 then return 402, body, header end return code, body,
header end)(...)"]
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 5: add consumer with username and plugins
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/consumers',
+ ngx.HTTP_PUT,
+ [[{
+ "username": "jack",
+ "plugins": {
+ "key-auth": {
+ "key": "auth-one"
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 6: add key auth plugin using admin api
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "key-auth": {}
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 7: valid consumer
+--- request
+GET /hello
+--- more_headers
+apikey: auth-one
+--- response_body
+hello world
+
+
+
+=== TEST 8: invalid consumer
+--- request
+GET /hello
+--- more_headers
+apikey: 123
+--- error_code: 402
+--- response_body
+{"message":"Invalid API key in request"}
+
+
+
+=== TEST 9: set plugin to convert 503 to 502 for auth plugins
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/global_rules/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "exit-transformer": {
+ "functions": ["return (function(code, body,
header) if code == 503 then return 502, \"Modified 503 to 502\", header end
return code, body, header end)(...)"]
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 10: set limit count plugin on route
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "methods": ["GET"],
+ "plugins": {
+ "limit-count": {
+ "count": 2,
+ "time_window": 60,
+ "rejected_code": 503,
+ "key": "remote_addr"
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 11: up the limit
+--- pipelined_requests eval
+["GET /hello", "GET /hello", "GET /hello", "GET /hello"]
+--- error_code eval
+[200, 200, 502, 502]
+--- response_body eval
+["hello world\n", "hello world\n", "Modified 503 to 502", "Modified 503 to
502"]
+
+
+
+=== TEST 12: set plugin with invalid code inside function
+# attempt to call code as a function)
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/global_rules/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "exit-transformer": {
+ "functions": ["return (function(code, body,
header) if code == 404 then return code() end return code, body, header
end)(...)"]
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 13: hit a non existent route and expect 404 status code
+# exit transformer will catch the invalid code inside func and print an error
log gracefully
+--- error_code: 404
+--- request
+GET /nohello
+--- error_log
+attempt to call local 'code' (a number value)
+
+
+
+=== TEST 14: set plugin with judgement based on request content-type
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/global_rules/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "exit-transformer": {
+ "functions": [
+ "return
+ (function(code, body, header)
+ local core = require(\"apisix.core\")
+ local ct =
core.request.headers()[\"Content-Type\"]
+
+ core.log.warn(\"exit transformer
running outside if check\")
+
+ if ct == \"application/json\" and code
== 404 then
+ core.log.warn(\"exit transformer
running inside if check\")
+ return 405
+ end
+ return code, body, header
+ end)
+ (...)"
+ ]
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 15: hit a request with non `application/json` content-type
+--- request
+GET /nohello
+--- more_headers
+Content-Type: text/html
+--- error_code: 404
+--- error_log
+exit transformer running outside if check
+
+
+
+=== TEST 16: hit a request with `application/json` content-type
+--- request
+GET /nohello
+--- more_headers
+Content-Type: application/json
+--- error_code: 405
+--- error_log
+exit transformer running outside if check
+exit transformer running inside if check
+
+
+
+=== TEST 17: treat body as a table
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "key-auth": {},
+ "exit-transformer": {
+ "functions": [
+ "return
+ (function(code, body, header)
+ if code == 401 and body.message ==
\"Missing API key in request\" then
+ return 400, {message =
\"authentication Failed\"}, {[\"content-type\"] = \"application/json\"}
+ end
+ return code, body, header
+ end)
+ (...)"
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 18: valid consumer
+--- request
+GET /hello
+--- more_headers
+apikey: auth-one
+--- response_headers
+content-type: text/plain
+--- response_body
+hello world
+
+
+
+=== TEST 19: missing api key
+--- request
+GET /hello
+--- error_code: 400
+--- response_headers
+content-type: application/json
+--- response_body
+{"message":"authentication Failed"}
+
+
+
+=== TEST 20: invalid consumer
+--- request
+GET /hello
+--- more_headers
+apikey: 123
+--- error_code: 401
+--- response_headers
+content-type: text/plain
+--- response_body
+{"message":"Invalid API key in request"}