From 2ac953523b4db80c5cace229e42e93b59737cf73 Mon Sep 17 00:00:00 2001 From: shreemaan-abhishek Date: Fri, 26 Jun 2026 18:28:19 +0545 Subject: [PATCH] fix(workflow): skip the action plugin in the chain to avoid double execution When a plugin such as limit-count or limit-conn runs as a workflow action and the same plugin is also configured in the normal plugin chain, the plugin runs twice per request (e.g. a request is counted twice against a rate limit). Mark the action plugin as skipped for the rest of the request so it does not run again in the chain. Signed-off-by: shreemaan-abhishek --- apisix/plugin.lua | 18 ++++ apisix/plugins/workflow.lua | 3 + docs/en/latest/plugins/workflow.md | 6 ++ t/plugin/workflow-without-case.t | 137 +++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+) diff --git a/apisix/plugin.lua b/apisix/plugin.lua index da587e8e76ca..ef0488daf40c 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 @@ -1329,6 +1339,9 @@ function _M.run_plugin(phase, plugins, api_ctx) run_meta_pre_function(conf, api_ctx, plugins[i]["name"]) plugin_run = true api_ctx._plugin_name = plugins[i]["name"] + if api_ctx._skip_plugins and api_ctx._skip_plugins[api_ctx._plugin_name] then + goto CONTINUE + end local code, body = phase_func(conf, api_ctx) api_ctx._plugin_name = nil if code or body then @@ -1367,12 +1380,17 @@ function _M.run_plugin(phase, plugins, api_ctx) plugin_run = true run_meta_pre_function(conf, api_ctx, plugins[i]["name"]) api_ctx._plugin_name = plugins[i]["name"] + if api_ctx._skip_plugins and api_ctx._skip_plugins[api_ctx._plugin_name] then + goto CONTINUE + end local span = tracer.start(api_ctx.ngx_ctx, "apisix.phase." .. phase .. ".plugins." .. api_ctx._plugin_name) phase_func(conf, 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 c3aa78e0ab6b..aa5143a43b10 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 2b695d95f385..2e750dbb4ee7 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 2ce469a7f0f4..c06b272f20e8 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]