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]