From 5007079e3613d62cafa94daf611e32b0e01ccf20 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Thu, 5 Feb 2026 15:51:19 +0545 Subject: [PATCH 1/8] feat(limit-count): support configuring multiple rules Signed-off-by: Abhishek Choudhary --- apisix/plugins/limit-count/init.lua | 176 ++++++++++--- t/plugin/limit-count-rules.t | 380 ++++++++++++++++++++++++++++ t/plugin/limit-count-variable.t | 3 +- 3 files changed, 516 insertions(+), 43 deletions(-) create mode 100644 t/plugin/limit-count-rules.t diff --git a/apisix/plugins/limit-count/init.lua b/apisix/plugins/limit-count/init.lua index 7d5fe7ca9baf..47def1ba1b42 100644 --- a/apisix/plugins/limit-count/init.lua +++ b/apisix/plugins/limit-count/init.lua @@ -25,6 +25,7 @@ local get_phase = ngx.get_phase local tonumber = tonumber local type = type local tostring = tostring +local str_format = string.format local limit_redis_cluster_new local limit_redis_new @@ -82,6 +83,28 @@ local schema = { {type = "string"}, }, }, + rules = { + type = "array", + items = { + type = "object", + properties = { + count = { + oneOf = { + {type = "integer", exclusiveMinimum = 0}, + {type = "string"}, + }, + }, + time_window = { + oneOf = { + {type = "integer", exclusiveMinimum = 0}, + {type = "string"}, + }, + }, + key = {type = "string"}, + }, + required = {"count", "time_window", "key"}, + }, + }, group = {type = "string"}, key = {type = "string", default = "remote_addr"}, key_type = {type = "string", @@ -102,7 +125,14 @@ local schema = { allow_degradation = {type = "boolean", default = false}, show_limit_quota_header = {type = "boolean", default = true} }, - required = {"count", "time_window"}, + oneOf = { + { + required = {"count", "time_window"}, + }, + { + required = {"rules"}, + } + }, ["if"] = { properties = { policy = { @@ -180,51 +210,34 @@ function _M.check_schema(conf, schema_type) end end - return true -end - - -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) + local keys = {} + for _, rule in ipairs(conf.rules or {}) do + if keys[rule.key] then + return false, str_format("duplicate key '%s' in rules", rule.key) end + keys[rule.key] = true 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 + return true +end + - core.log.info("limit count: ", count, ", time_window: ", time_window) +local function create_limit_obj(conf, rule, plugin_name) + core.log.info("create new " .. plugin_name .. " plugin instance", + ", rule: ", core.json.delay_encode(rule, true)) if not conf.policy or conf.policy == "local" then - return limit_local_new("plugin-" .. plugin_name, count, time_window) + return limit_local_new("plugin-" .. plugin_name, rule.count, + rule.time_window) end if conf.policy == "redis" then - return limit_redis_new("plugin-" .. plugin_name, count, time_window, conf) + return limit_redis_new("plugin-" .. plugin_name, rule.count, rule.time_window, conf) end if conf.policy == "redis-cluster" then - return limit_redis_cluster_new("plugin-" .. plugin_name, count, time_window, conf) + return limit_redis_cluster_new("plugin-" .. plugin_name, rule.count, + rule.time_window, conf) end return nil @@ -258,11 +271,71 @@ local function gen_limit_key(conf, ctx, key) 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 function resolve_var(ctx, value) + if type(value) == "string" then + local err, _ + value, err, _ = core.utils.resolve_var(value, ctx.var) + if err then + return nil, "could not resolve var for value: " .. value .. ", err: " .. err + end + value = tonumber(value) + if not value then + return nil, "resolved value is not a number: " .. tostring(value) + end + end + return value +end - local lim, err = create_limit_obj(conf, ctx, name) + +local function get_rules(ctx, conf) + if not conf.rules then + local count, err = resolve_var(ctx, conf.count) + if err then + return nil, err + end + local time_window, err2 = resolve_var(ctx, conf.time_window) + if err2 then + return nil, err2 + end + return { + { + count = count, + time_window = time_window, + key = conf.key, + key_type = conf.key_type, + } + } + end + + local rules = {} + for _, rule in ipairs(conf.rules) do + local count, err = resolve_var(ctx, rule.count) + if err then + goto CONTINUE + end + local time_window, err2 = resolve_var(ctx, rule.time_window) + if err2 then + goto CONTINUE + end + local key, _, n_resolved = core.utils.resolve_var(rule.key, ctx.var) + if n_resolved == 0 then + goto CONTINUE + end + core.table.insert(rules, { + count = count, + time_window = time_window, + key_type = "constant", + key = key, + }) + + ::CONTINUE:: + end + return rules +end + + +local function run_rate_limit(conf, rule, ctx, name, cost, dry_run) + local lim, err = create_limit_obj(conf, rule, name) if not lim then core.log.error("failed to fetch limit.count object: ", err) @@ -272,9 +345,9 @@ function _M.rate_limit(conf, ctx, name, cost, dry_run) return 500 end - local conf_key = conf.key + local conf_key = rule.key local key - if conf.key_type == "var_combination" then + if rule.key_type == "var_combination" then local err, n_resolved key, err, n_resolved = core.utils.resolve_var(conf_key, ctx.var) if err then @@ -284,7 +357,7 @@ function _M.rate_limit(conf, ctx, name, cost, dry_run) if n_resolved == 0 then key = nil end - elseif conf.key_type == "constant" then + elseif rule.key_type == "constant" then key = conf_key else key = ctx.var[conf_key] @@ -353,4 +426,25 @@ function _M.rate_limit(conf, ctx, name, cost, dry_run) end +function _M.rate_limit(conf, ctx, name, cost, dry_run) + core.log.info("ver: ", ctx.conf_version) + + local rules, err = get_rules(ctx, conf) + if not rules or #rules == 0 then + core.log.error("failed to get rate limit rules: ", err) + if conf.allow_degradation then + return + end + return 500 + end + + for _, rule in ipairs(rules) do + local code, msg = run_rate_limit(conf, rule, ctx, name, cost, dry_run) + if code then + return code, msg + end + end +end + + return _M diff --git a/t/plugin/limit-count-rules.t b/t/plugin/limit-count-rules.t new file mode 100644 index 000000000000..cda541e65c01 --- /dev/null +++ b/t/plugin/limit-count-rules.t @@ -0,0 +1,380 @@ +# +# 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: configure count/time_window and rules at same time +--- 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": 2, + "time_window": 5, + "rejected_code": 503, + "key_type": "var", + "key": "remote_addr", + "rules": [ + { + "count": 1, + "time_window": 10, + "key": "${http_company}" + } + ] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.print(body) + } + } +--- error_code: 400 +--- response_body +{"error_msg":"failed to check the configuration of plugin limit-count err: value should match only one schema, but matches both schemas 1 and 2"} + + + +=== TEST 2: configure multiple rules with same key +--- 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": { + "rejected_code": 503, + "rules": [ + { + "count": 5, + "time_window": 10, + "key": "${http_company}" + }, + { + "count": 8, + "time_window": 20, + "key": "${http_company}" + } + ] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.print(body) + } + } +--- error_code: 400 +--- response_body +{"error_msg":"failed to check the configuration of plugin limit-count err: duplicate key '${http_company}' in rules"} + + + +=== TEST 3: setup route with rules +--- 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": { + "rejected_code": 503, + "rejected_msg" : "rejected", + "rules": [ + { + "key": "${http_user}", + "count": "${http_jack_count}", + "time_window": 60 + }, + { + "key": "${http_project}", + "count": "${http_apisix_count}", + "time_window": 60 + } + ] + } + }, + "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: no any rule matched +--- request +GET /hello +--- error_code: 500 +--- error_log +failed to get rate limit rules + + + +=== TEST 5: match user rule +--- pipelined_requests eval +["GET /hello", "GET /hello", "GET /hello"] +--- more_headers +user: jack +jack-count: 2 +--- error_code eval +[200, 200, 503] +--- response_body eval +["hello world\n", "hello world\n", "{\"error_msg\":\"rejected\"}\n"] + + + +=== TEST 6: match project rule +--- pipelined_requests eval +["GET /hello", "GET /hello"] +--- more_headers +project: apisix +apisix-count: 3 +--- error_code eval +[200, 200, 200, 503] +--- response_body eval +["hello world\n", "hello world\n", "hello world\n", "{\"error_msg\":\"rejected\"}\n"] + + + +=== TEST 7: setup route with rules with variables with default values +--- 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": { + "rejected_code": 503, + "rejected_msg" : "rejected", + "rules": [ + { + "count": "${http_count ?? 2}", + "time_window": "${http_tw ?? 5}", + "key": "${remote_addr}" + } + ] + } + }, + "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 8: rules with variables in count - default value +--- pipelined_requests eval +["GET /hello", "GET /hello", "GET /hello"] +--- error_code eval +[200, 200, 503] +--- response_body eval +["hello world\n", "hello world\n", "{\"error_msg\":\"rejected\"}\n"] + + + +=== TEST 9: rules with variables in count - with header +--- setup + ngx.sleep(5) +--- pipelined_requests eval +["GET /hello", "GET /hello"] +--- more_headers +count: 1 +--- error_code eval +[200, 503] +--- response_body eval +["hello world\n", "{\"error_msg\":\"rejected\"}\n"] + + + +=== TEST 10: rules with same key and custom headers +--- 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": { + "rejected_code": 503, + "rejected_msg" : "rejected", + "show_limit_quota_header": true, + "rules": [ + { + "count": 2, + "time_window": 2, + "key": "${remote_addr}_2s" + }, + { + "count": 3, + "time_window": 5, + "key": "${remote_addr}_5s" + } + ] + } + }, + "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 11: test rules with same key +--- config + location /t { + content_by_lua_block { + local http = require("resty.http") + local httpc = http.new() + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" + + for i = 1, 2, 1 do + local res = httpc:request_uri(uri) + if res.status ~= 200 then + ngx.say("first two requests failed, status: " .. res.status) + return + end + end + + -- req 3, rejected by rule 1 + res = httpc:request_uri(uri) + if res.status ~= 503 then + ngx.say("req 3 should be rejected by rule 1, but got status: ", res.status) + return + end + + ngx.sleep(2) + + -- req 4, after sleep + res = httpc:request_uri(uri) + if res.status ~= 200 then + ngx.say("req 4 failed, status: ", res.status) + return + end + + -- req 5, rejected by rule 2 + res = httpc:request_uri(uri) + if res.status ~= 503 then + ngx.say("req 5 should be rejected by rule 2, but got status: ", res.status) + return + end + + ngx.say("passed") + } + } +--- request +GET /t +--- response_body +passed diff --git a/t/plugin/limit-count-variable.t b/t/plugin/limit-count-variable.t index ad021c2d5284..496ac5e812d5 100644 --- a/t/plugin/limit-count-variable.t +++ b/t/plugin/limit-count-variable.t @@ -141,5 +141,4 @@ GET /t --- timeout: 10 --- response_body passed ---- error_log -limit count: 3, time_window: 2 + From 3bef095acdf670b58533fa884ddfa31b33cc05be Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Thu, 5 Feb 2026 16:04:02 +0545 Subject: [PATCH 2/8] fix Signed-off-by: Abhishek Choudhary --- t/plugin/limit-count-variable.t | 1 - 1 file changed, 1 deletion(-) diff --git a/t/plugin/limit-count-variable.t b/t/plugin/limit-count-variable.t index 496ac5e812d5..c26a858fa1b0 100644 --- a/t/plugin/limit-count-variable.t +++ b/t/plugin/limit-count-variable.t @@ -141,4 +141,3 @@ GET /t --- timeout: 10 --- response_body passed - From 5905d4ec42e4b209fbfcd3d8d3831499f5fdcc8a Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Thu, 5 Feb 2026 16:30:43 +0545 Subject: [PATCH 3/8] doxx Signed-off-by: Abhishek Choudhary --- docs/en/latest/plugins/limit-count.md | 5 +++-- docs/zh/latest/plugins/limit-count.md | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/en/latest/plugins/limit-count.md b/docs/en/latest/plugins/limit-count.md index 3f872c21503b..65892c02d9e6 100644 --- a/docs/en/latest/plugins/limit-count.md +++ b/docs/en/latest/plugins/limit-count.md @@ -44,8 +44,9 @@ You may see the following rate limiting headers in the response: | Name | Type | Required | Default | Valid values | Description | | ----------------------- | ------- | ----------------------------------------- | ------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| count | integer | True | | > 0 | The maximum number of requests allowed within a given time interval. | -| time_window | integer | True | | > 0 | The time interval corresponding to the rate limiting `count` in seconds. | +| count | integer | False | | > 0 | The maximum number of requests allowed within a given time interval. Required if `rules` is not configured. | +| time_window | integer | False | | > 0 | The time interval corresponding to the rate limiting `count` in seconds. Required if `rules` is not configured. | +| rules | array[object] | False | | | A list of rate limiting rules. Each rule is an object containing `count`, `time_window`, and `key`. | | key_type | string | False | var | ["var","var_combination","constant"] | The type of key. If the `key_type` is `var`, the `key` is interpreted a variable. If the `key_type` is `var_combination`, the `key` is interpreted as a combination of variables. If the `key_type` is `constant`, the `key` is interpreted as a constant. | | key | string | False | remote_addr | | The key to count requests by. If the `key_type` is `var`, the `key` is interpreted a variable. The variable does not need to be prefixed by a dollar sign (`$`). If the `key_type` is `var_combination`, the `key` is interpreted as a combination of variables. All variables should be prefixed by dollar signs (`$`). For example, to configure the `key` to use a combination of two request headers `custom-a` and `custom-b`, the `key` should be configured as `$http_custom_a $http_custom_b`. If the `key_type` is `constant`, the `key` is interpreted as a constant value. | | rejected_code | integer | False | 503 | [200,...,599] | The HTTP status code returned when a request is rejected for exceeding the threshold. | diff --git a/docs/zh/latest/plugins/limit-count.md b/docs/zh/latest/plugins/limit-count.md index 44fe97970679..66fca11f6d74 100644 --- a/docs/zh/latest/plugins/limit-count.md +++ b/docs/zh/latest/plugins/limit-count.md @@ -45,8 +45,9 @@ description: limit-count 插件使用固定窗口算法,通过给定时间间 | 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | | ------------------- | ------- | ---------- | ------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| count | integer | 是 | | > 0 | 给定时间间隔内允许的最大请求数。 | -| time_window | integer | 是 | | > 0 | 速率限制 `count` 对应的时间间隔(以秒为单位)。 | +| count | integer | 否 | | > 0 | 给定时间间隔内允许的最大请求数。如果未配置 `rules`,则此项必填。 | +| time_window | integer | 否 | | > 0 | 速率限制 `count` 对应的时间间隔(以秒为单位)。如果未配置 `rules`,则此项必填。 | +| rules | array[object] | 否 | | | 速率限制规则列表。每个规则是一个包含 `count`、`time_window` 和 `key` 的对象。如果配置了 `rules`,则顶层的 `count` 和 `time_window` 将被忽略。 | | key_type | string | 否 | var | ["var","var_combination","constant"] | key 的类型。如果`key_type` 为 `var`,则 `key` 将被解释为变量。如果 `key_type` 为 `var_combination`,则 `key` 将被解释为变量的组合。如果 `key_type` 为 `constant`,则 `key` 将被解释为常量。 | | key | string | 否 | remote_addr | | 用于计数请求的 key。如果 `key_type` 为 `var`,则 `key` 将被解释为变量。变量不需要以美元符号(`$`)为前缀。如果 `key_type` 为 `var_combination`,则 `key` 会被解释为变量的组合。所有变量都应该以美元符号 (`$`) 为前缀。例如,要配置 `key` 使用两个请求头 `custom-a` 和 `custom-b` 的组合,则 `key` 应该配置为 `$http_custom_a $http_custom_b`。如果 `key_type` 为 `constant`,则 `key` 会被解释为常量值。| | rejection_code | integer | 否 | 503 | [200,...,599] | 请求因超出阈值而被拒绝时返回的 HTTP 状态代码。| From d20d38aed33e3abcbdf635ceae799792b8c3eaa6 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Fri, 6 Feb 2026 10:46:18 +0545 Subject: [PATCH 4/8] fix Signed-off-by: Abhishek Choudhary --- t/plugin/limit-count.t | 4 ++-- t/plugin/workflow.t | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/t/plugin/limit-count.t b/t/plugin/limit-count.t index 402c93dd2008..a1c96d93dfc8 100644 --- a/t/plugin/limit-count.t +++ b/t/plugin/limit-count.t @@ -215,7 +215,7 @@ passed } --- error_code: 400 --- response_body -{"error_msg":"failed to check the configuration of plugin limit-count err: property \"count\" is required"} +{"error_msg":"failed to check the configuration of plugin limit-count err: value should match only one schema, but matches none"} @@ -326,7 +326,7 @@ passed } --- error_code: 400 --- response_body -{"error_msg":"failed to check the configuration of plugin limit-count err: property \"count\" is required"} +{"error_msg":"failed to check the configuration of plugin limit-count err: value should match only one schema, but matches none"} diff --git a/t/plugin/workflow.t b/t/plugin/workflow.t index 022aa4851360..a8e8e2d57674 100644 --- a/t/plugin/workflow.t +++ b/t/plugin/workflow.t @@ -541,8 +541,8 @@ hello1 world } --- response_body done -failed to validate the 'limit-count' action: property "time_window" is required -failed to validate the 'limit-count' action: property "count" is required +failed to validate the 'limit-count' action: value should match only one schema, but matches none +failed to validate the 'limit-count' action: value should match only one schema, but matches none failed to validate the 'limit-count' action: group is not supported From fa95a319a95ce2fd98b91d60449f8f75fe21e387 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Fri, 6 Feb 2026 10:55:44 +0545 Subject: [PATCH 5/8] docs Signed-off-by: Abhishek Choudhary --- docs/en/latest/plugins/limit-count.md | 3 +++ docs/zh/latest/plugins/limit-count.md | 3 +++ 2 files changed, 6 insertions(+) diff --git a/docs/en/latest/plugins/limit-count.md b/docs/en/latest/plugins/limit-count.md index 65892c02d9e6..a9721108da27 100644 --- a/docs/en/latest/plugins/limit-count.md +++ b/docs/en/latest/plugins/limit-count.md @@ -47,6 +47,9 @@ You may see the following rate limiting headers in the response: | count | integer | False | | > 0 | The maximum number of requests allowed within a given time interval. Required if `rules` is not configured. | | time_window | integer | False | | > 0 | The time interval corresponding to the rate limiting `count` in seconds. Required if `rules` is not configured. | | rules | array[object] | False | | | A list of rate limiting rules. Each rule is an object containing `count`, `time_window`, and `key`. | +| rules.count | integer | True | | > 0 | The maximum number of requests allowed within a given time interval. | +| rules.time_window | integer | True | | > 0 | The time interval corresponding to the rate limiting `count` in seconds. | +| rules.key | string | True | | | The key to count requests by. | | key_type | string | False | var | ["var","var_combination","constant"] | The type of key. If the `key_type` is `var`, the `key` is interpreted a variable. If the `key_type` is `var_combination`, the `key` is interpreted as a combination of variables. If the `key_type` is `constant`, the `key` is interpreted as a constant. | | key | string | False | remote_addr | | The key to count requests by. If the `key_type` is `var`, the `key` is interpreted a variable. The variable does not need to be prefixed by a dollar sign (`$`). If the `key_type` is `var_combination`, the `key` is interpreted as a combination of variables. All variables should be prefixed by dollar signs (`$`). For example, to configure the `key` to use a combination of two request headers `custom-a` and `custom-b`, the `key` should be configured as `$http_custom_a $http_custom_b`. If the `key_type` is `constant`, the `key` is interpreted as a constant value. | | rejected_code | integer | False | 503 | [200,...,599] | The HTTP status code returned when a request is rejected for exceeding the threshold. | diff --git a/docs/zh/latest/plugins/limit-count.md b/docs/zh/latest/plugins/limit-count.md index 66fca11f6d74..cebefc3f7e91 100644 --- a/docs/zh/latest/plugins/limit-count.md +++ b/docs/zh/latest/plugins/limit-count.md @@ -48,6 +48,9 @@ description: limit-count 插件使用固定窗口算法,通过给定时间间 | count | integer | 否 | | > 0 | 给定时间间隔内允许的最大请求数。如果未配置 `rules`,则此项必填。 | | time_window | integer | 否 | | > 0 | 速率限制 `count` 对应的时间间隔(以秒为单位)。如果未配置 `rules`,则此项必填。 | | rules | array[object] | 否 | | | 速率限制规则列表。每个规则是一个包含 `count`、`time_window` 和 `key` 的对象。如果配置了 `rules`,则顶层的 `count` 和 `time_window` 将被忽略。 | +| rules.count | integer | 是 | | > 0 | 给定时间间隔内允许的最大请求数。 | +| rules.time_window | integer | 是 | | > 0 | 速率限制 `count` 对应的时间间隔(以秒为单位)。 | +| rules.key | string | 是 | | | 用于计数请求的 key。 | | key_type | string | 否 | var | ["var","var_combination","constant"] | key 的类型。如果`key_type` 为 `var`,则 `key` 将被解释为变量。如果 `key_type` 为 `var_combination`,则 `key` 将被解释为变量的组合。如果 `key_type` 为 `constant`,则 `key` 将被解释为常量。 | | key | string | 否 | remote_addr | | 用于计数请求的 key。如果 `key_type` 为 `var`,则 `key` 将被解释为变量。变量不需要以美元符号(`$`)为前缀。如果 `key_type` 为 `var_combination`,则 `key` 会被解释为变量的组合。所有变量都应该以美元符号 (`$`) 为前缀。例如,要配置 `key` 使用两个请求头 `custom-a` 和 `custom-b` 的组合,则 `key` 应该配置为 `$http_custom_a $http_custom_b`。如果 `key_type` 为 `constant`,则 `key` 会被解释为常量值。| | rejection_code | integer | 否 | 503 | [200,...,599] | 请求因超出阈值而被拒绝时返回的 HTTP 状态代码。| From bc5671b1409162d94fb8a86153c3f1767f13a98a Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Fri, 6 Feb 2026 10:58:32 +0545 Subject: [PATCH 6/8] test Signed-off-by: Abhishek Choudhary --- t/admin/consumer-group.t | 2 +- t/admin/plugin-configs.t | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/t/admin/consumer-group.t b/t/admin/consumer-group.t index 305afe48f3e4..fd28570f12f0 100644 --- a/t/admin/consumer-group.t +++ b/t/admin/consumer-group.t @@ -287,7 +287,7 @@ passed } } --- response_body -{"error_msg":"failed to check the configuration of plugin limit-count err: property \"count\" is required"} +{"error_msg":"failed to check the configuration of plugin limit-count err: value should match only one schema, but matches none"} --- error_code: 400 diff --git a/t/admin/plugin-configs.t b/t/admin/plugin-configs.t index ab6d2592b02c..4b72bc5c9250 100644 --- a/t/admin/plugin-configs.t +++ b/t/admin/plugin-configs.t @@ -287,7 +287,7 @@ passed } } --- response_body -{"error_msg":"failed to check the configuration of plugin limit-count err: property \"count\" is required"} +{"error_msg":"failed to check the configuration of plugin limit-count err: value should match only one schema, but matches none"} --- error_code: 400 From fa78980bf13fc8041f7d96ec4a2fdec91607ceab Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Fri, 6 Feb 2026 12:00:51 +0545 Subject: [PATCH 7/8] phix doxx Signed-off-by: Abhishek Choudhary --- docs/en/latest/plugins/limit-count.md | 2 +- docs/zh/latest/plugins/limit-count.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/latest/plugins/limit-count.md b/docs/en/latest/plugins/limit-count.md index a9721108da27..cba8f8d6a1bd 100644 --- a/docs/en/latest/plugins/limit-count.md +++ b/docs/en/latest/plugins/limit-count.md @@ -49,7 +49,7 @@ You may see the following rate limiting headers in the response: | rules | array[object] | False | | | A list of rate limiting rules. Each rule is an object containing `count`, `time_window`, and `key`. | | rules.count | integer | True | | > 0 | The maximum number of requests allowed within a given time interval. | | rules.time_window | integer | True | | > 0 | The time interval corresponding to the rate limiting `count` in seconds. | -| rules.key | string | True | | | The key to count requests by. | +| rules.key | string | True | | | The key to count requests by. If the configured key does not exist, the rule will not be executed. The `key` is interpreted as a combination of variables, for example: `$http_custom_a $http_custom_b`. | | key_type | string | False | var | ["var","var_combination","constant"] | The type of key. If the `key_type` is `var`, the `key` is interpreted a variable. If the `key_type` is `var_combination`, the `key` is interpreted as a combination of variables. If the `key_type` is `constant`, the `key` is interpreted as a constant. | | key | string | False | remote_addr | | The key to count requests by. If the `key_type` is `var`, the `key` is interpreted a variable. The variable does not need to be prefixed by a dollar sign (`$`). If the `key_type` is `var_combination`, the `key` is interpreted as a combination of variables. All variables should be prefixed by dollar signs (`$`). For example, to configure the `key` to use a combination of two request headers `custom-a` and `custom-b`, the `key` should be configured as `$http_custom_a $http_custom_b`. If the `key_type` is `constant`, the `key` is interpreted as a constant value. | | rejected_code | integer | False | 503 | [200,...,599] | The HTTP status code returned when a request is rejected for exceeding the threshold. | diff --git a/docs/zh/latest/plugins/limit-count.md b/docs/zh/latest/plugins/limit-count.md index cebefc3f7e91..df4acfa7113d 100644 --- a/docs/zh/latest/plugins/limit-count.md +++ b/docs/zh/latest/plugins/limit-count.md @@ -50,7 +50,7 @@ description: limit-count 插件使用固定窗口算法,通过给定时间间 | rules | array[object] | 否 | | | 速率限制规则列表。每个规则是一个包含 `count`、`time_window` 和 `key` 的对象。如果配置了 `rules`,则顶层的 `count` 和 `time_window` 将被忽略。 | | rules.count | integer | 是 | | > 0 | 给定时间间隔内允许的最大请求数。 | | rules.time_window | integer | 是 | | > 0 | 速率限制 `count` 对应的时间间隔(以秒为单位)。 | -| rules.key | string | 是 | | | 用于计数请求的 key。 | +| rules.key | string | 是 | | | 用于统计请求的键。如果配置的键不存在,则不会执行该规则。`key` 被解释为变量的组合,例如:`$http_custom_a $http_custom_b`。| | key_type | string | 否 | var | ["var","var_combination","constant"] | key 的类型。如果`key_type` 为 `var`,则 `key` 将被解释为变量。如果 `key_type` 为 `var_combination`,则 `key` 将被解释为变量的组合。如果 `key_type` 为 `constant`,则 `key` 将被解释为常量。 | | key | string | 否 | remote_addr | | 用于计数请求的 key。如果 `key_type` 为 `var`,则 `key` 将被解释为变量。变量不需要以美元符号(`$`)为前缀。如果 `key_type` 为 `var_combination`,则 `key` 会被解释为变量的组合。所有变量都应该以美元符号 (`$`) 为前缀。例如,要配置 `key` 使用两个请求头 `custom-a` 和 `custom-b` 的组合,则 `key` 应该配置为 `$http_custom_a $http_custom_b`。如果 `key_type` 为 `constant`,则 `key` 会被解释为常量值。| | rejection_code | integer | 否 | 503 | [200,...,599] | 请求因超出阈值而被拒绝时返回的 HTTP 状态代码。| From 9cf271568ecd1370a0feebe0a019404a349f6380 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Fri, 6 Feb 2026 12:01:33 +0545 Subject: [PATCH 8/8] log with comma Signed-off-by: Abhishek Choudhary --- apisix/plugins/limit-count/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apisix/plugins/limit-count/init.lua b/apisix/plugins/limit-count/init.lua index 47def1ba1b42..1a1427e108b5 100644 --- a/apisix/plugins/limit-count/init.lua +++ b/apisix/plugins/limit-count/init.lua @@ -223,7 +223,7 @@ end local function create_limit_obj(conf, rule, plugin_name) - core.log.info("create new " .. plugin_name .. " plugin instance", + core.log.info("create new ", plugin_name, " plugin instance", ", rule: ", core.json.delay_encode(rule, true)) if not conf.policy or conf.policy == "local" then