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]

Reply via email to