This is an automated email from the ASF dual-hosted git repository.
shreemaan-abhishek 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 9f694b27c fix(workflow): skip the action plugin in the chain to avoid
double execution (#13617)
9f694b27c is described below
commit 9f694b27c1f7d58fed0a18dffe0a955fe43eecd3
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Tue Jun 30 14:26:50 2026 +0800
fix(workflow): skip the action plugin in the chain to avoid double
execution (#13617)
---
apisix/plugin.lua | 21 ++++++
apisix/plugins/workflow.lua | 3 +
docs/en/latest/plugins/workflow.md | 6 ++
t/plugin/workflow-without-case.t | 137 +++++++++++++++++++++++++++++++++++++
4 files changed, 167 insertions(+)
diff --git a/apisix/plugin.lua b/apisix/plugin.lua
index da587e8e7..84123d8a2 100644
--- a/apisix/plugin.lua
+++ b/apisix/plugin.lua
@@ -1295,6 +1295,16 @@ local function run_meta_pre_function(conf, api_ctx, name)
end
end
+-- mark a plugin to be skipped for the rest of the request, so a plugin run as
+-- a workflow action does not run again in the normal plugin chain
+function _M.skip_plugin(ctx, plugin_name)
+ if not ctx._skip_plugins then
+ ctx._skip_plugins = {}
+ end
+ ctx._skip_plugins[plugin_name] = true
+end
+
+
function _M.run_plugin(phase, plugins, api_ctx)
local plugin_run = false
api_ctx = api_ctx or ngx.ctx.api_ctx
@@ -1326,6 +1336,11 @@ function _M.run_plugin(phase, plugins, api_ctx)
goto CONTINUE
end
+ -- skip a plugin already run as a workflow action, before any
meta hooks
+ if api_ctx._skip_plugins and
api_ctx._skip_plugins[plugins[i]["name"]] then
+ goto CONTINUE
+ end
+
run_meta_pre_function(conf, api_ctx, plugins[i]["name"])
plugin_run = true
api_ctx._plugin_name = plugins[i]["name"]
@@ -1364,6 +1379,10 @@ function _M.run_plugin(phase, plugins, api_ctx)
local phase_func = plugins[i][phase]
local conf = plugins[i + 1]
if phase_func and meta_filter(api_ctx, plugins[i]["name"], conf) then
+ -- skip a plugin already run as a workflow action, before any meta
hooks
+ if api_ctx._skip_plugins and
api_ctx._skip_plugins[plugins[i]["name"]] then
+ goto CONTINUE
+ end
plugin_run = true
run_meta_pre_function(conf, api_ctx, plugins[i]["name"])
api_ctx._plugin_name = plugins[i]["name"]
@@ -1373,6 +1392,8 @@ function _M.run_plugin(phase, plugins, api_ctx)
span:finish(api_ctx.ngx_ctx)
api_ctx._plugin_name = nil
end
+
+ ::CONTINUE::
end
return api_ctx, plugin_run
diff --git a/apisix/plugins/workflow.lua b/apisix/plugins/workflow.lua
index c3aa78e0a..aa5143a43 100644
--- a/apisix/plugins/workflow.lua
+++ b/apisix/plugins/workflow.lua
@@ -15,6 +15,7 @@
-- limitations under the License.
--
local core = require("apisix.core")
+local plugin = require("apisix.plugin")
local expr = require("resty.expr.v1")
local ipairs = ipairs
local setmetatable = setmetatable
@@ -167,6 +168,8 @@ function _M.access(conf, ctx)
if match_result then
-- only one action is currently supported
local action = rule.actions[1]
+ -- skip the action plugin in the chain so it does not run twice
+ plugin.skip_plugin(ctx, action[1])
local action_name = action[1]
local action_conf = action[2]
diff --git a/docs/en/latest/plugins/workflow.md
b/docs/en/latest/plugins/workflow.md
index 2b695d95f..2e750dbb4 100644
--- a/docs/en/latest/plugins/workflow.md
+++ b/docs/en/latest/plugins/workflow.md
@@ -39,6 +39,12 @@ import TabItem from '@theme/TabItem';
The `workflow` Plugin supports the conditional execution of user-defined
actions to client traffic based on a given set of rules, defined using
[lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list). This
provides a granular approach to traffic management.
+:::note
+
+When a Plugin such as `limit-count` or `limit-conn` is used as a `workflow`
action, the same Plugin is automatically skipped in the normal Plugin chain for
that request. This avoids running it twice (for example, counting a request
twice against a rate limit) when the Plugin is also configured directly on the
same Route, Service, Consumer, or Global Rule.
+
+:::
+
## Attributes
| Name | Type | Required | Default | Valid values | Description |
diff --git a/t/plugin/workflow-without-case.t b/t/plugin/workflow-without-case.t
index 2ce469a7f..c06b272f2 100644
--- a/t/plugin/workflow-without-case.t
+++ b/t/plugin/workflow-without-case.t
@@ -83,3 +83,140 @@ passed
--- request
GET /hello
--- error_code: 403
+
+
+
+=== TEST 3: create a route with key-auth & limit-count plugin
+--- 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": {},
+ "limit-count": {
+ "count": 3,
+ "time_window": 10,
+ "rejected_code": 503
+ }
+ },
+ "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 4: create a consumer rose
+--- 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": "rose",
+ "plugins": {
+ "key-auth": {
+ "key": "rose"
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 5: create a consumer jack with workflow plugin
+--- 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": "jack"
+ },
+ "workflow": {
+ "rules": [
+ {
+ "case": [
+ ["route_id", "==", "1"]
+ ],
+ "actions": [
+ [
+ "limit-count",
+ {
+ "count": 5,
+ "time_window": 10,
+ "rejected_code": 429
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 6: send request with rose consumer, only the chain limit-count applies
+--- pipelined_requests eval
+["GET /hello", "GET /hello", "GET /hello", "GET /hello"]
+--- more_headers
+apikey: rose
+--- error_code eval
+[200, 200, 200, 503]
+
+
+
+=== TEST 7: send request with jack consumer, the chain limit-count is skipped
so only the workflow action counts
+--- pipelined_requests eval
+["GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET
/hello"]
+--- more_headers
+apikey: jack
+--- error_code eval
+[200, 200, 200, 200, 200, 429]