dhivakar020 opened a new issue, #14127:
URL: https://github.com/apache/apisix/issues/14127

   ### Description
   
   ### Description
   
   Allow users to set `run_policy: "prefer_route"` in a plugin's `_meta` 
configuration, so that when the same plugin is configured in both a Global Rule 
and a Route, the global rule's instance is skipped and only the route's 
instance runs.
   
   ### Background
   
   Today, when the same plugin is configured in both a Global Rule and a Route, 
both instances execute sequentially across phases (documented in 
`docs/en/latest/terminology/plugin.md`, "Plugins merging precedence").
   
   Core already has a mechanism to avoid this. In `apisix/plugin.lua`, 
`_M.filter` skips the global rule instance when `plugin_obj.run_policy == 
"prefer_route"` and the route configures the same plugin (not disabled). This 
was historically added to resolve conflicts for specific plugins (e.g. #4589 
for `skywalking`, #6579 for `prometheus`, and #14016 for `request-decompress`).
   
   However, `run_policy` can currently only be set statically in a plugin's Lua 
source code (`local _M = { run_policy = "prefer_route" }`). Users cannot set it 
dynamically through the Admin API or declarative config. For any built-in 
plugin lacking this hardcoded flag (e.g. `cors`, `http-logger`, `limit-count`) 
or any custom plugin, duplicate execution across phases cannot be avoided at 
the configuration level.
   
   **Reproduction:**
   
   1. Create a Global Rule with `cors` enabled (e.g., standard corporate 
default `allow_origins: "*"`).
   2. Create a Route with `cors` enabled (e.g., restricted `allow_origins: 
"https://example.com"`).
   3. Send a request matching the route. Both plugin instances execute, 
producing duplicate headers / conflicting behavior.
   
   Attempting to set `run_policy` through `_meta`:
   
   ```json
   "plugins": {
     "cors": {
       "_meta": {
         "run_policy": "prefer_route"
       },
       "allow_origins": "https://example.com";
     }
   }
   ```
   
   fails schema validation with `HTTP/1.1 400 Bad Request`:
   
   ```json
   {
     "error_msg": "failed to check the configuration of plugin cors err: 
property \"_meta\" validation failed: additional properties forbidden, found 
run_policy"
   }
   ```
   
   Current workarounds, such as writing negative-regex `_meta.filter` 
expressions on the global rule, are fragile and error-prone, especially when 
combined with plugins that alter the request URI state (such as 
`proxy-rewrite`).
   
   **Version:** APISIX 3.19.0 (commit 
[5300993](https://github.com/apache/apisix/commit/5300993973a455d5f11ab360bcd3d5af6714a70f))
   
   ### Why `_meta`
   
   `apisix/plugin.lua` explicitly documents that new injected plugin fields 
belong under `_meta` ([lines 253-257 at 
5300993](https://github.com/apache/apisix/blob/5300993973a455d5f11ab360bcd3d5af6714a70f/apisix/plugin.lua#L253-L257)):
   
   > -- new injected fields should be added under `_meta`
   > -- 1. so we won't break user's code when adding any new injected fields
   > -- 2. the semantics is clear, especially in the doc and in the caller side
   
   `run_policy = "prefer_route"` is already implemented in core 
([apisix/plugin.lua#L599-L605](https://github.com/apache/apisix/blob/5300993973a455d5f11ab360bcd3d5af6714a70f/apisix/plugin.lua#L599-L605)).
 This proposal exposes the existing mechanism through `_meta` without adding 
complex new execution logic.
   
   ### Proposed Change
   
   **1. Schema (`apisix/schema_def.lua`)**: add `run_policy` to 
`_M.plugin_injected_schema._meta.properties`:
   
   ```lua
   run_policy = {
       description = "when set to prefer_route, a global rule instance of this 
plugin "
                     .. "is skipped if the matched route also enables the same 
plugin",
       type = "string",
       enum = {"prefer_route"},
   },
   ```
   
   **2. Behavior (`apisix/plugin.lua`, `_M.filter`)**: derive the effective 
`run_policy` from the plugin's `_meta` (from the route or global rule) as well 
as the plugin definition:
   
   ```lua
   local plugin_conf_in_route = route_plugin_conf and route_plugin_conf[name]
   local run_policy = (plugin_conf_in_route and plugin_conf_in_route._meta
                       and plugin_conf_in_route._meta.run_policy)
                      or (plugin_conf._meta and plugin_conf._meta.run_policy)
                      or plugin_obj.run_policy
   
   if run_policy == "prefer_route" and route_plugin_conf ~= nil then
       local disable_in_route = check_disable(plugin_conf_in_route)
       if plugin_conf_in_route and not disable_in_route then
           goto continue
       end
   end
   ```
   
   **3. Docs**: add `run_policy` to the `_meta` common configuration table in 
`docs/en/latest/terminology/plugin.md` (and the zh counterpart), with an 
example.
   
   ### Semantics
   
   - `run_policy` is meaningful when configured on a plugin in a Route or in a 
Global Rule. On other objects (Service, Consumer, Plugin Config) it passes 
validation but follows standard merging precedence.
   - The policy takes effect when set on the route's plugin, on the global 
rule's plugin, or in the plugin's Lua definition. Since `prefer_route` is 
currently the only supported value, no conflict is possible. If more values are 
added in the future, the route-level configuration will take precedence.
   
   | Global Rule | Route | Result |
   |---|---|---|
   | plugin | none | global instance runs |
   | none | plugin | route instance runs |
   | plugin | plugin (policy unset) | both run sequentially (unchanged default) 
|
   | plugin | plugin (`prefer_route` set on either) | only route instance runs |
   | plugin | plugin with `_meta.disable: true`, `prefer_route` set | global 
instance runs |
   | plugin | plugin with `_meta.filter` evaluating to false, `prefer_route` 
set | neither runs (matches existing hardcoded behavior for skywalking / 
prometheus) |
   
   Note on the last row: this matches the existing core behavior. When a route 
configures a plugin under `prefer_route`, it explicitly takes ownership of that 
plugin from the global rule. If its runtime filter evaluates to false, the 
route instance skips execution and the global rule remains suppressed.
   
   ### Cache Invalidation
   
   No new caching is introduced. In `apisix/plugin.lua:1561-1574`, 
`api_ctx.global_plugins` is cached per request (on `api_ctx`) and is only 
reused when `api_ctx.global_plugins_route == route` and 
`api_ctx.global_plugins_version == dummy_global_rule.modifiedIndex`. Any route 
or global rule update in etcd therefore takes effect on subsequent requests 
without a gateway restart.
   
   ### Backward Compatibility
   
   Fully backward compatible. The field is optional under `_meta`, defaults to 
current behavior, and existing configs as well as plugins with hardcoded 
`run_policy` continue to work unchanged.
   
   ### Alternatives Considered
   
   - **Hardcoding `run_policy` per plugin:** does not help custom plugins or 
standard plugins where users require route-level overrides without modifying 
upstream Lua files.
   - **`_meta.filter` negative regex workarounds:** fragile, maintenance-heavy, 
and breaks when upstream rewrite plugins mutate the URI.
   - **Global switch:** too coarse, as users often want route override for only 
specific plugins.
   
   ### Willingness to Contribute
   
   I have implemented and tested this locally against master and am willing to 
submit a Pull Request with the schema change, behavior update, Test::Nginx 
integration tests covering the semantics matrix, and documentation updates.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

  • [I] feat: As a user, I want to ... dhivakar020 (via GitHub) via notifications

Reply via email to