This is an automated email from the ASF dual-hosted git repository.
shreemaanabhishek 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 f37c19013 feat: support configuring variables in limit-conn,
limit-count and ai-rate-limiting (#12967)
f37c19013 is described below
commit f37c19013afe4d515c950dc28d0b388d29f32d85
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Thu Feb 5 13:51:47 2026 +0545
feat: support configuring variables in limit-conn, limit-count and
ai-rate-limiting (#12967)
---
apisix/plugins/ai-rate-limiting.lua | 28 ++-
apisix/plugins/limit-conn.lua | 14 +-
apisix/plugins/limit-conn/init.lua | 44 ++++-
apisix/plugins/limit-count/init.lua | 80 +++++----
apisix/plugins/limit-count/limit-count-local.lua | 8 +-
t/plugin/ai-rate-limiting.t | 206 +++++++++++++++++++++++
t/plugin/limit-conn-variable.t | 178 ++++++++++++++++++++
t/plugin/limit-conn.t | 6 +-
t/plugin/limit-count-redis-cluster.t | 3 -
t/plugin/limit-count-redis.t | 3 -
t/plugin/limit-count-variable.t | 145 ++++++++++++++++
t/plugin/limit-count.t | 12 +-
12 files changed, 665 insertions(+), 62 deletions(-)
diff --git a/apisix/plugins/ai-rate-limiting.lua
b/apisix/plugins/ai-rate-limiting.lua
index c674bbef8..2bbfa8aca 100644
--- a/apisix/plugins/ai-rate-limiting.lua
+++ b/apisix/plugins/ai-rate-limiting.lua
@@ -27,8 +27,18 @@ local instance_limit_schema = {
type = "object",
properties = {
name = {type = "string"},
- limit = {type = "integer", minimum = 1},
- time_window = {type = "integer", minimum = 1}
+ limit = {
+ oneOf = {
+ {type = "integer", minimum = 1},
+ {type = "string"},
+ },
+ },
+ time_window = {
+ oneOf = {
+ {type = "integer", minimum = 1},
+ {type = "string"},
+ },
+ }
},
required = {"name", "limit", "time_window"}
}
@@ -36,8 +46,18 @@ local instance_limit_schema = {
local schema = {
type = "object",
properties = {
- limit = {type = "integer", exclusiveMinimum = 0},
- time_window = {type = "integer", exclusiveMinimum = 0},
+ limit = {
+ oneOf = {
+ {type = "integer", exclusiveMinimum = 0},
+ {type = "string"},
+ },
+ },
+ time_window = {
+ oneOf = {
+ {type = "integer", exclusiveMinimum = 0},
+ {type = "string"},
+ },
+ },
show_limit_quota_header = {type = "boolean", default = true},
limit_strategy = {
type = "string",
diff --git a/apisix/plugins/limit-conn.lua b/apisix/plugins/limit-conn.lua
index 6fdefd1f4..dd88162af 100644
--- a/apisix/plugins/limit-conn.lua
+++ b/apisix/plugins/limit-conn.lua
@@ -24,8 +24,18 @@ local workflow =
require("apisix.plugins.workflow")
local schema = {
type = "object",
properties = {
- conn = {type = "integer", exclusiveMinimum = 0}, --
limit.conn max
- burst = {type = "integer", minimum = 0},
+ conn = {
+ oneOf = {
+ {type = "integer", exclusiveMinimum = 0},
+ {type = "string"},
+ },
+ },
+ burst = {
+ oneOf = {
+ {type = "integer", minimum = 0},
+ {type = "string"},
+ },
+ },
default_conn_delay = {type = "number", exclusiveMinimum = 0},
only_use_default_delay = {type = "boolean", default = false},
key = {type = "string"},
diff --git a/apisix/plugins/limit-conn/init.lua
b/apisix/plugins/limit-conn/init.lua
index 22ce44a12..6c5c823e1 100644
--- a/apisix/plugins/limit-conn/init.lua
+++ b/apisix/plugins/limit-conn/init.lua
@@ -18,6 +18,9 @@ local limit_conn_new = require("resty.limit.conn").new
local core = require("apisix.core")
local is_http = ngx.config.subsystem == "http"
local sleep = core.sleep
+local tonumber = tonumber
+local type = type
+local tostring = tostring
local shdict_name = "plugin-limit-conn"
if ngx.config.subsystem == "stream" then
shdict_name = shdict_name .. "-stream"
@@ -34,30 +37,55 @@ do
end
-local lrucache = core.lrucache.new({
- type = "plugin",
-})
local _M = {}
-local function create_limit_obj(conf)
+local function create_limit_obj(ctx, conf)
core.log.info("create new limit-conn plugin instance")
+ local conn = conf.conn
+ if type(conn) == "string" then
+ local err, _
+ conn, err, _ = core.utils.resolve_var(conn, ctx.var)
+ if err then
+ return nil, "could not resolve vars in conn: " .. err
+ end
+ conn = tonumber(conn)
+ if not conn then
+ return nil, "resolved conn is not a number: " .. tostring(conn)
+ end
+ end
+
+ local burst = conf.burst
+ if type(burst) == "string" then
+ local err, _
+ burst, err, _ = core.utils.resolve_var(burst, ctx.var)
+ if err then
+ return nil, "could not resolve vars in burst: " .. err
+ end
+ burst = tonumber(burst)
+ if not burst then
+ return nil, "resolved burst is not a number: " .. tostring(burst)
+ end
+ end
+
+ core.log.info("limit conn: ", conn, ", burst: ", burst)
+
if conf.policy == "redis" then
core.log.info("create new limit-conn redis plugin instance")
- return redis_single_new("plugin-limit-conn", conf, conf.conn,
conf.burst,
+ return redis_single_new("plugin-limit-conn", conf, conn, burst,
conf.default_conn_delay)
elseif conf.policy == "redis-cluster" then
core.log.info("create new limit-conn redis-cluster plugin instance")
- return redis_cluster_new("plugin-limit-conn", conf, conf.conn,
conf.burst,
+ return redis_cluster_new("plugin-limit-conn", conf, conn, burst,
conf.default_conn_delay)
else
core.log.info("create new limit-conn plugin instance")
- return limit_conn_new(shdict_name, conf.conn, conf.burst,
+ return limit_conn_new(shdict_name, conn, burst,
conf.default_conn_delay)
end
end
@@ -65,7 +93,7 @@ end
function _M.increase(conf, ctx)
core.log.info("ver: ", ctx.conf_version)
- local lim, err = lrucache(conf, nil, create_limit_obj, conf)
+ local lim, err = create_limit_obj(ctx, conf)
if not lim then
core.log.error("failed to instantiate a resty.limit.conn object: ",
err)
if conf.allow_degradation then
diff --git a/apisix/plugins/limit-count/init.lua
b/apisix/plugins/limit-count/init.lua
index c7b8579d0..7d5fe7ca9 100644
--- a/apisix/plugins/limit-count/init.lua
+++ b/apisix/plugins/limit-count/init.lua
@@ -22,6 +22,9 @@ local pairs = pairs
local redis_schema = require("apisix.utils.redis-schema")
local policy_to_additional_properties = redis_schema.schema
local get_phase = ngx.get_phase
+local tonumber = tonumber
+local type = type
+local tostring = tostring
local limit_redis_cluster_new
local limit_redis_new
@@ -36,9 +39,6 @@ do
local cluster_src = "apisix.plugins.limit-count.limit-count-redis-cluster"
limit_redis_cluster_new = require(cluster_src).new
end
-local lrucache = core.lrucache.new({
- type = 'plugin', serial_creating = true,
-})
local group_conf_lru = core.lrucache.new({
type = 'plugin',
})
@@ -70,8 +70,18 @@ local metadata_schema = {
local schema = {
type = "object",
properties = {
- count = {type = "integer", exclusiveMinimum = 0},
- time_window = {type = "integer", exclusiveMinimum = 0},
+ count = {
+ oneOf = {
+ {type = "integer", exclusiveMinimum = 0},
+ {type = "string"},
+ },
+ },
+ time_window = {
+ oneOf = {
+ {type = "integer", exclusiveMinimum = 0},
+ {type = "string"},
+ },
+ },
group = {type = "string"},
key = {type = "string", default = "remote_addr"},
key_type = {type = "string",
@@ -174,22 +184,47 @@ function _M.check_schema(conf, schema_type)
end
-local function create_limit_obj(conf, plugin_name)
+local function create_limit_obj(conf, ctx, plugin_name)
core.log.info("create new " .. plugin_name .. " plugin instance")
+ local count = conf.count
+ if type(count) == "string" then
+ local err, _
+ count, err, _ = core.utils.resolve_var(count, ctx.var)
+ if err then
+ return nil, "could not resolve vars in count: " .. err
+ end
+ count = tonumber(count)
+ if not count then
+ return nil, "resolved count is not a number: " .. tostring(count)
+ end
+ end
+
+ local time_window = conf.time_window
+ if type(time_window) == "string" then
+ local err, _
+ time_window, err, _ = core.utils.resolve_var(time_window, ctx.var)
+ if err then
+ return nil, "could not resolve vars in time_window: " .. err
+ end
+ time_window = tonumber(time_window)
+ if not time_window then
+ return nil, "resolved time_window is not a number: " ..
tostring(time_window)
+ end
+ end
+
+ core.log.info("limit count: ", count, ", time_window: ", time_window)
+
if not conf.policy or conf.policy == "local" then
- return limit_local_new("plugin-" .. plugin_name, conf.count,
- conf.time_window)
+ return limit_local_new("plugin-" .. plugin_name, count, time_window)
end
if conf.policy == "redis" then
- return limit_redis_new("plugin-" .. plugin_name,
- conf.count, conf.time_window, conf)
+ return limit_redis_new("plugin-" .. plugin_name, count, time_window,
conf)
end
if conf.policy == "redis-cluster" then
- return limit_redis_cluster_new("plugin-" .. plugin_name, conf.count,
- conf.time_window, conf)
+ return limit_redis_cluster_new("plugin-" .. plugin_name, count,
time_window, conf)
end
return nil
@@ -223,26 +258,11 @@ local function gen_limit_key(conf, ctx, key)
end
-local function gen_limit_obj(conf, ctx, plugin_name)
- if conf.group then
- return lrucache(conf.group, "", create_limit_obj, conf, plugin_name)
- end
-
- local extra_key
- if conf._vid then
- extra_key = conf.policy .. '#' .. conf._vid
- else
- extra_key = conf.policy
- end
-
- return core.lrucache.plugin_ctx(lrucache, ctx, extra_key,
create_limit_obj, conf, plugin_name)
-end
-
function _M.rate_limit(conf, ctx, name, cost, dry_run)
core.log.info("ver: ", ctx.conf_version)
core.log.info("conf: ", core.json.delay_encode(conf, true))
- local lim, err = gen_limit_obj(conf, ctx, name)
+ local lim, err = create_limit_obj(conf, ctx, name)
if not lim then
core.log.error("failed to fetch limit.count object: ", err)
@@ -307,7 +327,7 @@ function _M.rate_limit(conf, ctx, name, cost, dry_run)
if err == "rejected" then
-- show count limit header when rejected
if conf.show_limit_quota_header and set_header then
- core.response.set_header(set_limit_headers.limit_header,
conf.count,
+ core.response.set_header(set_limit_headers.limit_header,
lim.limit,
set_limit_headers.remaining_header, 0,
set_limit_headers.reset_header, reset)
end
@@ -326,7 +346,7 @@ function _M.rate_limit(conf, ctx, name, cost, dry_run)
end
if conf.show_limit_quota_header and set_header then
- core.response.set_header(set_limit_headers.limit_header, conf.count,
+ core.response.set_header(set_limit_headers.limit_header, lim.limit,
set_limit_headers.remaining_header, remaining,
set_limit_headers.reset_header, reset)
end
diff --git a/apisix/plugins/limit-count/limit-count-local.lua
b/apisix/plugins/limit-count/limit-count-local.lua
index b6f319ae0..9ad33d89c 100644
--- a/apisix/plugins/limit-count/limit-count-local.lua
+++ b/apisix/plugins/limit-count/limit-count-local.lua
@@ -57,7 +57,9 @@ function _M.new(plugin_name, limit, window)
local self = {
limit_count = limit_count.new(plugin_name, limit, window),
- dict = ngx.shared[plugin_name .. "-reset-header"]
+ dict = ngx.shared[plugin_name .. "-reset-header"],
+ limit = limit,
+ window = window,
}
return setmetatable(self, mt)
@@ -67,8 +69,8 @@ function _M.incoming(self, key, commit, conf, cost)
local delay, remaining = self.limit_count:incoming(key, commit, cost)
local reset
- if remaining == conf.count - cost then
- reset = set_endtime(self, key, conf.time_window)
+ if remaining == self.limit - cost then
+ reset = set_endtime(self, key, self.window)
else
reset = read_reset(self, key)
end
diff --git a/t/plugin/ai-rate-limiting.t b/t/plugin/ai-rate-limiting.t
index 66aa0b07f..c6112b9ab 100644
--- a/t/plugin/ai-rate-limiting.t
+++ b/t/plugin/ai-rate-limiting.t
@@ -984,3 +984,209 @@ passed
Authorization: Bearer token
--- error_code eval
[200, 200, 200, 200, 200, 200, 200, 503, 503]
+
+
+
+=== TEST 21: use variable in count and time_window with default value
+--- config
+ location /t {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local data = {
+ uri = "/ai",
+ plugins = {
+ ["ai-proxy-multi"] = {
+ fallback_strategy =
"instance_health_and_rate_limiting",
+ instances = {
+ {
+ name = "deepseek",
+ provider = "openai",
+ weight = 1,
+ priority = 1,
+ auth = {
+ header = {
+ Authorization = "Bearer token"
+ }
+ },
+ override = {
+ endpoint = "http://localhost:16724"
+ }
+ },
+ {
+ name = "openai",
+ provider = "openai",
+ weight = 1,
+ priority = 0,
+ auth = {
+ header = {
+ Authorization = "Bearer token"
+ }
+ },
+ override = {
+ endpoint = "http://localhost:16724"
+ }
+ }
+ },
+ ssl_verify = false
+ },
+ ["ai-rate-limiting"] = {
+ limit = "${http_count ?? 10}",
+ time_window = "${http_time_window ?? 60}",
+ instances = {
+ {
+ name = "openai",
+ limit = "${http_openai_count ?? 20}",
+ time_window = "${http_time_window ?? 60}"
+ }
+ }
+ }
+ },
+ upstream = {
+ type = "roundrobin",
+ nodes = {
+ ["canbeanything.com"] = 1
+ }
+ }
+ }
+
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ core.json.encode(data)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 22: request with default variable values
+--- config
+ location /t {
+ content_by_lua_block {
+ local http = require("resty.http")
+
+ local test_cases = {
+ { code = 200 },
+ { code = 200 },
+ { code = 200 },
+ { code = 503 },
+ }
+
+ local httpc = http.new()
+ for i, case in ipairs(test_cases) do
+ local res = httpc:request_uri(
+ "http://127.0.0.1:" .. ngx.var.server_port .. "/ai",
+ {
+ method = "POST",
+ body = [[{
+ "messages": [
+ { "role": "system", "content": "You are a
mathematician" },
+ { "role": "user", "content": "What is 1+1?" }
+ ]
+ }]],
+ headers = {
+ ["Content-Type"] = "application/json",
+ }
+ }
+ )
+ if res.status ~= case.code then
+ ngx.say( i .. "th request should return " .. case.code ..
", but got " .. res.status)
+ return
+ end
+ end
+
+ ngx.say("passed")
+ }
+ }
+--- request
+GET /t
+--- timeout: 10
+--- response_body
+passed
+--- grep_error_log eval
+qr/picked instance: [^,]+/
+--- grep_error_log_out
+picked instance: deepseek
+picked instance: openai
+picked instance: openai
+picked instance: nil
+
+
+
+=== TEST 23: request with custom count/time_window headers
+--- config
+ location /t {
+ content_by_lua_block {
+ local http = require("resty.http")
+
+ local test_cases = {
+ { count = 20, openai_count = 30, time_window = 2, code = 200 },
+ { count = 20, openai_count = 30, time_window = 2, code = 200 },
+ { count = 20, openai_count = 30, time_window = 2, code = 200 },
+ { count = 20, openai_count = 30, time_window = 2, code = 200 },
+ { count = 20, openai_count = 30, time_window = 2, code = 200 },
+ { count = 20, openai_count = 30, time_window = 2, code = 503 },
+ }
+
+ local run_tests = function()
+ local httpc = http.new()
+ for i, case in ipairs(test_cases) do
+ local res = httpc:request_uri(
+ "http://127.0.0.1:" .. ngx.var.server_port .. "/ai",
+ {
+ method = "POST",
+ body = [[{
+ "messages": [
+ { "role": "system", "content": "You are a
mathematician" },
+ { "role": "user", "content": "What is
1+1?" }
+ ]
+ }]],
+ headers = {
+ ["Content-Type"] = "application/json",
+ ["count"] = tostring(case.count),
+ ["time-window"] = tostring(case.time_window),
+ ["openai-count"] = tostring(case.openai_count),
+ }
+ }
+ )
+ if res.status ~= case.code then
+ ngx.say( i .. "th request should return " ..
case.code .. ", but got " .. res.status)
+ ngx.exit(500)
+ end
+ end
+ end
+
+ run_tests()
+ ngx.sleep(2)
+ run_tests()
+
+ ngx.say("passed")
+ }
+ }
+--- request
+GET /t
+--- timeout: 10
+--- response_body
+passed
+--- grep_error_log eval
+qr/picked instance: [^,]+/
+--- grep_error_log_out
+picked instance: deepseek
+picked instance: deepseek
+picked instance: openai
+picked instance: openai
+picked instance: openai
+picked instance: nil
+picked instance: deepseek
+picked instance: deepseek
+picked instance: openai
+picked instance: openai
+picked instance: openai
+picked instance: nil
diff --git a/t/plugin/limit-conn-variable.t b/t/plugin/limit-conn-variable.t
new file mode 100644
index 000000000..8c82100e4
--- /dev/null
+++ b/t/plugin/limit-conn-variable.t
@@ -0,0 +1,178 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+BEGIN {
+ if ($ENV{TEST_NGINX_CHECK_LEAK}) {
+ $SkipReason = "unavailable for the hup tests";
+
+ } else {
+ $ENV{TEST_NGINX_USE_HUP} = 1;
+ undef $ENV{TEST_NGINX_USE_STAP};
+ }
+}
+
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_shuffle();
+no_root_location();
+log_level('info');
+
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+ my $port = $ENV{TEST_NGINX_SERVER_PORT};
+
+ my $config = $block->config // <<_EOC_;
+ location /access_root_dir {
+ content_by_lua_block {
+ local httpc = require "resty.http"
+ local hc = httpc:new()
+
+ local res, err =
hc:request_uri('http://127.0.0.1:$port/limit_conn', {
+ headers = ngx.req.get_headers()
+ })
+ if res then
+ ngx.exit(res.status)
+ end
+ }
+ }
+
+ location /test_concurrency {
+ content_by_lua_block {
+ local reqs = {}
+ for i = 1, 10 do
+ reqs[i] = { "/access_root_dir" }
+ end
+ local resps = { ngx.location.capture_multi(reqs) }
+ for i, resp in ipairs(resps) do
+ ngx.say(resp.status)
+ end
+ }
+ }
+_EOC_
+
+ $block->set_value("config", $config);
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: use variable in conn and burst with default value
+--- 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": {
+ "limit-conn": {
+ "conn": "${http_conn ?? 5}",
+ "burst": "${http_burst ?? 2}",
+ "default_conn_delay": 0.1,
+ "rejected_code": 503,
+ "key": "remote_addr"
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/limit_conn"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 2: request without conn/burst headers
+--- request
+GET /test_concurrency
+--- timeout: 10s
+--- response_body
+200
+200
+200
+200
+200
+200
+200
+503
+503
+503
+--- error_log
+limit conn: 5, burst: 2
+
+
+
+=== TEST 3: request with conn header
+--- request
+GET /test_concurrency
+--- more_headers
+conn: 3
+--- timeout: 10s
+--- response_body
+200
+200
+200
+200
+200
+503
+503
+503
+503
+503
+--- error_log
+limit conn: 3, burst: 2
+
+
+
+=== TEST 4: request with conn and burst header
+--- request
+GET /test_concurrency
+--- more_headers
+conn: 3
+burst: 4
+--- timeout: 10s
+--- response_body
+200
+200
+200
+200
+200
+200
+200
+503
+503
+503
+--- error_log
+limit conn: 3, burst: 4
diff --git a/t/plugin/limit-conn.t b/t/plugin/limit-conn.t
index 0182019ee..364282f49 100644
--- a/t/plugin/limit-conn.t
+++ b/t/plugin/limit-conn.t
@@ -362,7 +362,7 @@ GET /t
GET /t
--- error_code: 400
--- response_body
-{"error_msg":"failed to check the configuration of plugin limit-conn err:
property \"conn\" validation failed: expected -1 to be greater than 0"}
+{"error_msg":"failed to check the configuration of plugin limit-conn err:
property \"conn\" validation failed: value should match only one schema, but
matches none"}
@@ -441,7 +441,7 @@ GET /t
GET /t
--- error_code: 400
--- response_body
-{"error_msg":"failed to check the configuration of plugin limit-conn err:
property \"conn\" validation failed: expected -1 to be greater than 0"}
+{"error_msg":"failed to check the configuration of plugin limit-conn err:
property \"conn\" validation failed: value should match only one schema, but
matches none"}
@@ -868,7 +868,7 @@ GET /test_concurrency
--- request
GET /t
--- response_body
-property "conn" validation failed: expected 0 to be greater than 0
+property "conn" validation failed: value should match only one schema, but
matches none
property "default_conn_delay" validation failed: expected 0 to be greater than 0
done
diff --git a/t/plugin/limit-count-redis-cluster.t
b/t/plugin/limit-count-redis-cluster.t
index 7a4798a60..1263f80a3 100644
--- a/t/plugin/limit-count-redis-cluster.t
+++ b/t/plugin/limit-count-redis-cluster.t
@@ -167,9 +167,6 @@ passed
=== TEST 4: up the limit
--- request
GET /hello
---- error_log
-try to lock with key route#1#redis-cluster
-unlock with key route#1#redis-cluster
diff --git a/t/plugin/limit-count-redis.t b/t/plugin/limit-count-redis.t
index d06188050..db13ba4e4 100644
--- a/t/plugin/limit-count-redis.t
+++ b/t/plugin/limit-count-redis.t
@@ -169,9 +169,6 @@ passed
=== TEST 4: up the limit
--- request
GET /hello
---- error_log
-try to lock with key route#1#redis
-unlock with key route#1#redis
diff --git a/t/plugin/limit-count-variable.t b/t/plugin/limit-count-variable.t
new file mode 100644
index 000000000..ad021c2d5
--- /dev/null
+++ b/t/plugin/limit-count-variable.t
@@ -0,0 +1,145 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+use t::APISIX 'no_plan';
+
+no_long_string();
+no_shuffle();
+no_root_location();
+log_level('info');
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ if (!$block->request) {
+ $block->set_value("request", "GET /t");
+ }
+
+ if (!$block->error_log && !$block->no_error_log) {
+ $block->set_value("no_error_log", "[error]\n[alert]");
+ }
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: use variable in count and time_window with default value
+--- 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,
+ [[{
+ "methods": ["GET"],
+ "plugins": {
+ "limit-count": {
+ "count": "${http_count ?? 2}",
+ "time_window": "${http_time_window ?? 5}",
+ "rejected_code": 503,
+ "key_type": "var",
+ "key": "remote_addr",
+ "policy": "local"
+ }
+ },
+ "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 2: request without count/time_window headers
+--- pipelined_requests eval
+["GET /hello", "GET /hello", "GET /hello"]
+--- error_code eval
+[200, 200, 503]
+
+
+
+=== TEST 3: request with count header
+--- pipelined_requests eval
+["GET /hello", "GET /hello", "GET /hello"]
+--- more_headers
+count: 5
+--- error_code eval
+[200, 200, 200, 200, 200, 503]
+
+
+
+=== TEST 4: request with count and time_window header
+--- config
+ location /t {
+ content_by_lua_block {
+ local http = require("resty.http")
+ local core = require("apisix.core")
+
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello"
+ local opt = {method = "GET", headers = { ["count"] = 3,
["time-window"] = "2" }}
+ local httpc = http.new()
+
+ for i = 1, 3, 1 do
+ local res = httpc:request_uri(uri, opt)
+ if res.status ~= 200 then
+ ngx.say("first two requests should return 200, but got "
.. res.status)
+ return
+ end
+ if res.headers["X-RateLimit-Limit"] ~= "3" then
+ ngx.say("X-RateLimit-Limit should be 3, but got " ..
core.json.encode(res.headers))
+ return
+ end
+ end
+ local res = httpc:request_uri(uri, opt)
+ if res.status ~= 503 then
+ ngx.say("third requests should return 503, but got " ..
res.status)
+ return
+ end
+
+ ngx.sleep(2)
+
+ for i = 1, 3, 1 do
+ local res = httpc:request_uri(uri, opt)
+ if res.status ~= 200 then
+ ngx.say("two requests after sleep 2s should return 200,
but got " .. res.status)
+ return
+ end
+ end
+ ngx.say("passed")
+ }
+ }
+--- request
+GET /t
+--- timeout: 10
+--- response_body
+passed
+--- error_log
+limit count: 3, time_window: 2
diff --git a/t/plugin/limit-count.t b/t/plugin/limit-count.t
index f150c01d0..402c93dd2 100644
--- a/t/plugin/limit-count.t
+++ b/t/plugin/limit-count.t
@@ -253,7 +253,7 @@ passed
}
--- error_code: 400
--- response_body
-{"error_msg":"failed to check the configuration of plugin limit-count err:
property \"count\" validation failed: expected -100 to be greater than 0"}
+{"error_msg":"failed to check the configuration of plugin limit-count err:
property \"count\" validation failed: value should match only one schema, but
matches none"}
@@ -291,7 +291,7 @@ passed
}
--- error_code: 400
--- response_body
-{"error_msg":"failed to check the configuration of plugin limit-count err:
property \"count\" validation failed: expected -100 to be greater than 0"}
+{"error_msg":"failed to check the configuration of plugin limit-count err:
property \"count\" validation failed: value should match only one schema, but
matches none"}
@@ -363,7 +363,7 @@ passed
}
--- error_code: 400
--- response_body
-{"error_msg":"failed to check the configuration of plugin limit-count err:
property \"count\" validation failed: expected -100 to be greater than 0"}
+{"error_msg":"failed to check the configuration of plugin limit-count err:
property \"count\" validation failed: value should match only one schema, but
matches none"}
@@ -400,7 +400,7 @@ passed
}
--- error_code: 400
--- response_body
-{"error_msg":"failed to check the configuration of plugin limit-count err:
property \"count\" validation failed: expected -100 to be greater than 0"}
+{"error_msg":"failed to check the configuration of plugin limit-count err:
property \"count\" validation failed: value should match only one schema, but
matches none"}
@@ -1171,7 +1171,7 @@ passed
}
}
--- response_body eval
-qr/property \"count\" validation failed: expected 0 to be greater than 0/
+qr/property \"count\" validation failed: value should match only one schema,
but matches none/
@@ -1189,4 +1189,4 @@ qr/property \"count\" validation failed: expected 0 to be
greater than 0/
}
}
--- response_body eval
-qr/property \"time_window\" validation failed: expected 0 to be greater than 0/
+qr/property \"time_window\" validation failed: value should match only one
schema, but matches none/