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"}

Reply via email to