From f4f2d70fe4e2c6b4d32f22fecf4704737b0a1254 Mon Sep 17 00:00:00 2001 From: niemna Date: Tue, 21 Apr 2026 00:35:28 +0800 Subject: [PATCH 1/8] =?UTF-8?q?limit-req=E6=94=AF=E6=8C=81rules=E5=92=8C?= =?UTF-8?q?=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apisix/plugins/limit-req.lua | 166 +++---- apisix/plugins/limit-req/init.lua | 220 ++++++++ t/plugin/limit-req-variable.t | 801 ++++++++++++++++++++++++++++++ 3 files changed, 1079 insertions(+), 108 deletions(-) create mode 100644 apisix/plugins/limit-req/init.lua create mode 100644 t/plugin/limit-req-variable.t diff --git a/apisix/plugins/limit-req.lua b/apisix/plugins/limit-req.lua index 6c5b856217cb..3e2994f75a53 100644 --- a/apisix/plugins/limit-req.lua +++ b/apisix/plugins/limit-req.lua @@ -14,41 +14,58 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -- -local limit_req_new = require("resty.limit.req").new -local core = require("apisix.core") -local redis_schema = require("apisix.utils.redis-schema") -local policy_to_additional_properties = redis_schema.schema -local plugin_name = "limit-req" -local sleep = core.sleep -local apisix_plugin = require("apisix.plugin") -local error = error +local core = require("apisix.core") +local redis_schema = require("apisix.utils.redis-schema") +local policy_to_additional_properties = redis_schema.schema +local plugin_name = "limit-req" +local str_format = string.format +local ipairs = ipairs -local redis_single_new -local redis_cluster_new -do - local redis_src = "apisix.plugins.limit-req.limit-req-redis" - redis_single_new = require(redis_src).new - - local cluster_src = "apisix.plugins.limit-req.limit-req-redis-cluster" - redis_cluster_new = require(cluster_src).new -end - - -local lrucache = core.lrucache.new({ - type = "plugin", -}) +local limit_req_init = require("apisix.plugins.limit-req.init") local schema = { type = "object", properties = { - rate = {type = "number", exclusiveMinimum = 0}, - burst = {type = "number", minimum = 0}, + rate = { + oneOf = { + {type = "number", exclusiveMinimum = 0}, + {type = "string"}, + }, + }, + burst = { + oneOf = { + {type = "number", minimum = 0}, + {type = "string"}, + }, + }, key = {type = "string"}, key_type = {type = "string", enum = {"var", "var_combination"}, default = "var", }, + rules = { + type = "array", + items = { + type = "object", + properties = { + rate = { + oneOf = { + {type = "number", exclusiveMinimum = 0}, + {type = "string"}, + }, + }, + burst = { + oneOf = { + {type = "number", minimum = 0}, + {type = "string"}, + }, + }, + key = {type = "string"}, + }, + required = {"rate", "burst", "key"}, + }, + }, policy = { type = "string", enum = {"redis", "redis-cluster", "local"}, @@ -65,7 +82,14 @@ local schema = { }, allow_degradation = {type = "boolean", default = false} }, - required = {"rate", "burst", "key"}, + oneOf = { + { + required = {"rate", "burst", "key"}, + }, + { + required = {"rules"}, + } + }, ["if"] = { properties = { policy = { @@ -101,95 +125,21 @@ function _M.check_schema(conf) return false, err end - return true -end - - -local function create_limit_obj(conf) - if conf.policy == "local" then - core.log.info("create new limit-req plugin instance") - return limit_req_new("plugin-limit-req", conf.rate, conf.burst) - - elseif conf.policy == "redis" then - core.log.info("create new limit-req redis plugin instance") - return redis_single_new("plugin-limit-req", conf, conf.rate, conf.burst) - - elseif conf.policy == "redis-cluster" then - core.log.info("create new limit-req redis-cluster plugin instance") - return redis_cluster_new("plugin-limit-req", conf, conf.rate, conf.burst) - - else - return nil, "policy enum not match" - end -end - - -local function gen_limit_key(conf, ctx, key) - local parent = conf._meta and conf._meta.parent - if not parent or not parent.resource_key then - error("failed to generate key invalid parent: " .. core.json.encode(parent)) + 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 - return parent.resource_key .. ':' .. apisix_plugin.conf_version(conf) .. ':' .. key + return true end function _M.access(conf, ctx) - local lim, err = core.lrucache.plugin_ctx(lrucache, ctx, nil, - create_limit_obj, conf) - if not lim then - core.log.error("failed to instantiate a resty.limit.req object: ", err) - if conf.allow_degradation then - return - end - return 500 - end - - local conf_key = conf.key - local key - if conf.key_type == "var_combination" then - local err, n_resolved - key, err, n_resolved = core.utils.resolve_var(conf_key, ctx.var) - if err then - core.log.error("could not resolve vars in ", conf_key, " error: ", err) - end - - if n_resolved == 0 then - key = nil - end - - else - key = ctx.var[conf_key] - end - - if key == nil then - core.log.info("The value of the configured key is empty, use client IP instead") - -- When the value of key is empty, use client IP instead - key = ctx.var["remote_addr"] - end - - key = gen_limit_key(conf, ctx, key) - core.log.info("limit key: ", key) - - local delay, err = lim:incoming(key, true) - if not delay then - if err == "rejected" then - if conf.rejected_msg then - return conf.rejected_code, { error_msg = conf.rejected_msg } - end - return conf.rejected_code - end - - core.log.error("failed to limit req: ", err) - if conf.allow_degradation then - return - end - return 500 - end - - if delay >= 0.001 and not conf.nodelay then - sleep(delay) - end + return limit_req_init.access(conf, ctx) end + return _M diff --git a/apisix/plugins/limit-req/init.lua b/apisix/plugins/limit-req/init.lua new file mode 100644 index 000000000000..87b77194bc1c --- /dev/null +++ b/apisix/plugins/limit-req/init.lua @@ -0,0 +1,220 @@ +-- +-- 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. +-- +local limit_req_new = require("resty.limit.req").new +local core = require("apisix.core") +local sleep = core.sleep +local tonumber = tonumber +local type = type +local tostring = tostring +local ipairs = ipairs +local error = error +local apisix_plugin = require("apisix.plugin") + +local redis_single_new +local redis_cluster_new +do + local redis_src = "apisix.plugins.limit-req.limit-req-redis" + redis_single_new = require(redis_src).new + + local cluster_src = "apisix.plugins.limit-req.limit-req-redis-cluster" + redis_cluster_new = require(cluster_src).new +end + + +local _M = {} + + +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 function get_rules(ctx, conf) + if not conf.rules then + local rate, err = resolve_var(ctx, conf.rate) + if err then + return nil, err + end + local burst, err2 = resolve_var(ctx, conf.burst) + if err2 then + return nil, err2 + end + return { + { + rate = rate, + burst = burst, + key = conf.key, + key_type = conf.key_type, + } + } + end + + local rules = {} + for _, rule in ipairs(conf.rules) do + local rate, err = resolve_var(ctx, rule.rate) + if err then + goto CONTINUE + end + local burst, err2 = resolve_var(ctx, rule.burst) + 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, { + rate = rate, + burst = burst, + key_type = "constant", + key = key, + }) + + ::CONTINUE:: + end + return rules +end + + +local function create_limit_obj(conf, rule) + core.log.info("create new limit-req plugin instance") + + local rate = rule.rate + local burst = rule.burst + + core.log.info("limit req rate: ", rate, ", burst: ", burst) + + if conf.policy == "local" then + core.log.info("create new limit-req plugin instance") + return limit_req_new("plugin-limit-req", rate, burst) + + elseif conf.policy == "redis" then + core.log.info("create new limit-req redis plugin instance") + return redis_single_new("plugin-limit-req", conf, rate, burst) + + elseif conf.policy == "redis-cluster" then + core.log.info("create new limit-req redis-cluster plugin instance") + return redis_cluster_new("plugin-limit-req", conf, rate, burst) + + else + return nil, "policy enum not match" + end +end + + +local function gen_limit_key(conf, ctx, key) + local parent = conf._meta and conf._meta.parent + if not parent or not parent.resource_key then + error("failed to generate key invalid parent: " .. core.json.encode(parent)) + end + + return parent.resource_key .. ':' .. apisix_plugin.conf_version(conf) .. ':' .. key +end + + +local function run_limit_req(conf, rule, ctx) + local lim, err = create_limit_obj(conf, rule) + if not lim then + core.log.error("failed to instantiate a resty.limit.req object: ", err) + if conf.allow_degradation then + return + end + return 500 + end + + local conf_key = rule.key + local key + 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 + core.log.error("could not resolve vars in ", conf_key, " error: ", err) + end + + if n_resolved == 0 then + key = nil + end + elseif rule.key_type == "constant" then + key = conf_key + else + key = ctx.var[conf_key] + end + + if key == nil then + core.log.info("The value of the configured key is empty, use client IP instead") + key = ctx.var["remote_addr"] + end + + key = gen_limit_key(conf, ctx, key) + core.log.info("limit key: ", key) + + local delay, err = lim:incoming(key, true) + if not delay then + if err == "rejected" then + if conf.rejected_msg then + return conf.rejected_code, { error_msg = conf.rejected_msg } + end + return conf.rejected_code + end + + core.log.error("failed to limit req: ", err) + if conf.allow_degradation then + return + end + return 500 + end + + if delay >= 0.001 and not conf.nodelay then + sleep(delay) + end +end + + +function _M.access(conf, ctx) + 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 limit req rules: ", err) + if conf.allow_degradation then + return + end + return 500 + end + + for _, rule in ipairs(rules) do + local code, msg = run_limit_req(conf, rule, ctx) + if code then + return code, msg + end + end +end + + +return _M diff --git a/t/plugin/limit-req-variable.t b/t/plugin/limit-req-variable.t new file mode 100644 index 000000000000..55ef2b03fdb0 --- /dev/null +++ b/t/plugin/limit-req-variable.t @@ -0,0 +1,801 @@ +# +# 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) = @_; + + 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 rate 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, + [[{ + "uri": "/hello", + "plugins": { + "limit-req": { + "rate": "${http_rate ?? 10}", + "burst": "${http_burst ?? 2}", + "rejected_code": 503, + "key": "remote_addr", + "policy": "local" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 2: request without rate/burst headers - uses default values +--- pipelined_requests eval +["GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello"] +--- error_code eval +[200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 503, 503, 503] +--- error_log +limit req rate: 10, burst: 2 + + + +=== TEST 3: request with rate header +--- 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" + + local run_tests = function() + for i = 1, 5, 1 do + local res = httpc:request_uri(uri, { + method = "GET", + headers = { ["rate"] = "2" } + }) + if res.status ~= 200 then + ngx.say(i .. "th request should return 200, but got " .. res.status) + return + end + end + local res = httpc:request_uri(uri, { + method = "GET", + headers = { ["rate"] = "2" } + }) + if res.status ~= 503 then + ngx.say("6th request should return 503, but got " .. res.status) + return + end + end + + run_tests() + ngx.sleep(1) + run_tests() + + ngx.say("passed") + } + } +--- request +GET /t +--- timeout: 10 +--- response_body +passed +--- error_log +limit req rate: 2, burst: 2 + + + +=== TEST 4: request with rate and burst header +--- 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" + + local run_tests = function() + for i = 1, 6, 1 do + local res = httpc:request_uri(uri, { + method = "GET", + headers = { ["rate"] = "3", ["burst"] = "4" } + }) + if res.status ~= 200 then + ngx.say(i .. "th request should return 200, but got " .. res.status) + return + end + end + local res = httpc:request_uri(uri, { + method = "GET", + headers = { ["rate"] = "3", ["burst"] = "4" } + }) + if res.status ~= 503 then + ngx.say("7th request should return 503, but got " .. res.status) + return + end + end + + run_tests() + ngx.sleep(1) + run_tests() + + ngx.say("passed") + } + } +--- request +GET /t +--- timeout: 10 +--- response_body +passed +--- error_log +limit req rate: 3, burst: 4 + + + +=== TEST 5: schema check with both rate/burst and rules should fail +--- 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, + [[{ + "uri": "/hello", + "plugins": { + "limit-req": { + "rate": 10, + "burst": 2, + "key": "remote_addr", + "rules": [ + { + "rate": 5, + "burst": 1, + "key": "remote_addr" + } + ] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.print(body) + return + end + ngx.say(body) + } + } +--- error_code: 400 +--- response_body +{"error_msg":"failed to check the configuration of plugin limit-req err: value should match only one schema, but matches both schemas 1 and 2"} + + + +=== TEST 6: duplicate keys in rules should fail +--- 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, + [[{ + "uri": "/hello", + "plugins": { + "limit-req": { + "rules": [ + { + "rate": 5, + "burst": 1, + "key": "${http_user}" + }, + { + "rate": 10, + "burst": 2, + "key": "${http_user}" + } + ] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.print(body) + return + end + ngx.say(body) + } + } +--- error_code: 400 +--- response_body +{"error_msg":"failed to check the configuration of plugin limit-req err: duplicate key '${http_user}' in rules"} + + + +=== TEST 7: setup route with multi-level 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, + [[{ + "uri": "/hello", + "plugins": { + "limit-req": { + "rules": [ + { + "rate": 10, + "burst": 2, + "key": "${http_user}" + }, + { + "rate": 5, + "burst": 1, + "key": "${http_project}" + } + ], + "rejected_code": 503, + "rejected_msg": "rate limited" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 8: no rule matches - returns 500 +--- request +GET /hello +--- error_code: 500 +--- error_log +failed to get limit req rules + + + +=== TEST 9: match user rule - rate limiting applies +--- 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, 12, 1 do + local res = httpc:request_uri(uri, { + method = "GET", + headers = { ["user"] = "jack" } + }) + if i <= 12 and res.status ~= 200 then + ngx.say(i .. "th request should return 200, but got " .. res.status) + return + end + end + + local res = httpc:request_uri(uri, { + method = "GET", + headers = { ["user"] = "jack" } + }) + if res.status ~= 503 then + ngx.say("13th request should return 503, but got " .. res.status) + return + end + + ngx.say("passed") + } + } +--- request +GET /t +--- timeout: 10 +--- response_body +passed + + + +=== TEST 10: match project rule - rate limiting applies +--- setup + ngx.sleep(1) +--- 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, 6, 1 do + local res = httpc:request_uri(uri, { + method = "GET", + headers = { ["project"] = "apisix" } + }) + if res.status ~= 200 then + ngx.say(i .. "th request should return 200, but got " .. res.status) + return + end + end + + local res = httpc:request_uri(uri, { + method = "GET", + headers = { ["project"] = "apisix" } + }) + if res.status ~= 503 then + ngx.say("7th request should return 503, but got " .. res.status) + return + end + + ngx.say("passed") + } + } +--- request +GET /t +--- timeout: 10 +--- response_body +passed + + + +=== TEST 11: 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, + [[{ + "uri": "/hello", + "plugins": { + "limit-req": { + "rules": [ + { + "rate": "${http_rate ?? 5}", + "burst": "${http_burst ?? 1}", + "key": "${remote_addr}" + } + ], + "rejected_code": 503, + "rejected_msg": "rate limited" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 12: rules with variables in rate - default value +--- pipelined_requests eval +["GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello"] +--- error_code eval +[200, 200, 200, 200, 200, 200, 503] +--- response_body eval +["hello world\n", "hello world\n", "hello world\n", "hello world\n", "hello world\n", "hello world\n", "{\"error_msg\":\"rate limited\"}\n"] + + + +=== TEST 13: rules with variables in rate - with header +--- setup + ngx.sleep(2) +--- pipelined_requests eval +["GET /hello", "GET /hello", "GET /hello", "GET /hello"] +--- more_headers +rate: 2 +--- error_code eval +[200, 200, 200, 503] +--- response_body eval +["hello world\n", "hello world\n", "hello world\n", "{\"error_msg\":\"rate limited\"}\n"] + + + +=== TEST 14: rules with different time windows +--- 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, + [[{ + "uri": "/hello", + "plugins": { + "limit-req": { + "rules": [ + { + "rate": 2, + "burst": 0, + "key": "${remote_addr}_short" + }, + { + "rate": 3, + "burst": 1, + "key": "${remote_addr}_long" + } + ], + "rejected_code": 503, + "rejected_msg": "rate limited" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 15: test rules with different rate limits +--- 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 (rate: 2, burst: 0) + local 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(1) + + -- req 4, after sleep should pass rule 1 but might hit rule 2 + 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 +--- timeout: 10 +--- response_body +passed + + + +=== TEST 16: legacy mode with string rate and burst +--- 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, + [[{ + "uri": "/hello", + "plugins": { + "limit-req": { + "rate": "100", + "burst": "10", + "key": "remote_addr", + "policy": "local" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 17: multi-rule mode with redis policy +--- 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, + [[{ + "uri": "/hello", + "plugins": { + "limit-req": { + "rules": [ + { + "rate": 100, + "burst": 20, + "key": "remote_addr" + }, + { + "rate": "$http_x_user_id", + "burst": 50, + "key": "http_x_user_id" + } + ], + "policy": "redis", + "redis_host": "127.0.0.1", + "redis_port": 6379, + "rejected_code": 429 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.print(body) + return + end + ngx.say(body) + } + } +--- skip_eval +3: no -r system('redis-cli -h 127.0.0.1 -p 6379 ping >/dev/null 2>&1') +--- response_body +passed + + + +=== TEST 18: allow_degradation when rules fail to resolve +--- 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, + [[{ + "uri": "/hello", + "plugins": { + "limit-req": { + "rules": [ + { + "rate": "$http_nonexistent_var", + "burst": 10, + "key": "${http_nonexistent_key}" + } + ], + "rejected_code": 503, + "allow_degradation": true + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 19: request with allow_degradation - passes through +--- request +GET /hello +--- error_code: 200 +--- response_body +hello world +--- error_log +failed to get limit req rules + + + +=== TEST 20: nodelay option in multi-rule mode +--- 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, + [[{ + "uri": "/hello", + "plugins": { + "limit-req": { + "rules": [ + { + "rate": 2, + "burst": 5, + "key": "remote_addr" + } + ], + "nodelay": true, + "rejected_code": 503 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 21: nodelay - requests should not be delayed +--- 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" + + local start_time = ngx.now() + for i = 1, 7, 1 do + local res = httpc:request_uri(uri) + if res.status ~= 200 then + ngx.say(i .. "th request should return 200, but got " .. res.status) + return + end + end + local elapsed = ngx.now() - start_time + + -- with nodelay=true, 7 requests should complete quickly (< 0.5s) + if elapsed > 0.5 then + ngx.say("requests took too long: " .. elapsed .. "s") + return + end + + ngx.say("passed") + } + } +--- request +GET /t +--- timeout: 10 +--- response_body +passed From d622f30c25849a8afbe93d0119569cea092a9e0f Mon Sep 17 00:00:00 2001 From: niemna Date: Tue, 21 Apr 2026 23:44:06 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=8C=96=E6=B5=8B?= =?UTF-8?q?=E8=AF=95fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- t/plugin/limit-req-variable.t | 552 +++------------------------------- 1 file changed, 48 insertions(+), 504 deletions(-) diff --git a/t/plugin/limit-req-variable.t b/t/plugin/limit-req-variable.t index 55ef2b03fdb0..f6f6317b1b5c 100644 --- a/t/plugin/limit-req-variable.t +++ b/t/plugin/limit-req-variable.t @@ -60,8 +60,8 @@ __DATA__ "uri": "/hello", "plugins": { "limit-req": { - "rate": "${http_rate ?? 10}", - "burst": "${http_burst ?? 2}", + "rate": "${http_rate ?? 0.1}", + "burst": "${http_burst ?? 0.1}", "rejected_code": 503, "key": "remote_addr", "policy": "local" @@ -89,107 +89,26 @@ passed === TEST 2: request without rate/burst headers - uses default values --- pipelined_requests eval -["GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello"] +["GET /hello", "GET /hello"] --- error_code eval -[200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 503, 503, 503] ---- error_log -limit req rate: 10, burst: 2 - - +[200, 503] -=== TEST 3: request with rate header ---- 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" - - local run_tests = function() - for i = 1, 5, 1 do - local res = httpc:request_uri(uri, { - method = "GET", - headers = { ["rate"] = "2" } - }) - if res.status ~= 200 then - ngx.say(i .. "th request should return 200, but got " .. res.status) - return - end - end - local res = httpc:request_uri(uri, { - method = "GET", - headers = { ["rate"] = "2" } - }) - if res.status ~= 503 then - ngx.say("6th request should return 503, but got " .. res.status) - return - end - end - run_tests() - ngx.sleep(1) - run_tests() - ngx.say("passed") - } - } +=== TEST 3: request with rate and burst header --- request -GET /t ---- timeout: 10 ---- response_body -passed +GET /hello +--- more_headers +rate: 2 +burst: 2 +--- timeout: 10s +--- error_code: 200 --- error_log limit req rate: 2, burst: 2 -=== TEST 4: request with rate and burst header ---- 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" - - local run_tests = function() - for i = 1, 6, 1 do - local res = httpc:request_uri(uri, { - method = "GET", - headers = { ["rate"] = "3", ["burst"] = "4" } - }) - if res.status ~= 200 then - ngx.say(i .. "th request should return 200, but got " .. res.status) - return - end - end - local res = httpc:request_uri(uri, { - method = "GET", - headers = { ["rate"] = "3", ["burst"] = "4" } - }) - if res.status ~= 503 then - ngx.say("7th request should return 503, but got " .. res.status) - return - end - end - - run_tests() - ngx.sleep(1) - run_tests() - - ngx.say("passed") - } - } ---- request -GET /t ---- timeout: 10 ---- response_body -passed ---- error_log -limit req rate: 3, burst: 4 - - - -=== TEST 5: schema check with both rate/burst and rules should fail +=== TEST 4: schema check with both rate/burst and rules should fail --- config location /t { content_by_lua_block { @@ -235,7 +154,7 @@ limit req rate: 3, burst: 4 -=== TEST 6: duplicate keys in rules should fail +=== TEST 5: duplicate keys in rules should fail --- config location /t { content_by_lua_block { @@ -283,7 +202,7 @@ limit req rate: 3, burst: 4 -=== TEST 7: setup route with multi-level rules +=== TEST 6: setup route with multi-level rules --- config location /t { content_by_lua_block { @@ -296,18 +215,19 @@ limit req rate: 3, burst: 4 "limit-req": { "rules": [ { - "rate": 10, - "burst": 2, + "rate": 0.01, + "burst": "${http_burst1 ?? 2}", "key": "${http_user}" }, { - "rate": 5, - "burst": 1, + "rate": 0.01, + "burst": 4, "key": "${http_project}" } ], "rejected_code": 503, - "rejected_msg": "rate limited" + "rejected_msg": "rate limited", + "nodelay": true } }, "upstream": { @@ -330,7 +250,7 @@ passed -=== TEST 8: no rule matches - returns 500 +=== TEST 7: no rule matches - returns 500 --- request GET /hello --- error_code: 500 @@ -339,463 +259,87 @@ failed to get limit req rules -=== TEST 9: match user rule - rate limiting applies + +=== TEST 8: match user rule --- config location /t { content_by_lua_block { + local json = require "t.toolkit.json" local http = require("resty.http") local httpc = http.new() local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" - for i = 1, 12, 1 do + local ress = {} + for i = 1, 4, 1 do local res = httpc:request_uri(uri, { method = "GET", - headers = { ["user"] = "jack" } + headers = { ["user"] = "jack"} }) - if i <= 12 and res.status ~= 200 then - ngx.say(i .. "th request should return 200, but got " .. res.status) - return - end - end - - local res = httpc:request_uri(uri, { - method = "GET", - headers = { ["user"] = "jack" } - }) - if res.status ~= 503 then - ngx.say("13th request should return 503, but got " .. res.status) - return + table.insert(ress, res.status) end - ngx.say("passed") + ngx.say(json.encode(ress)) } } --- request GET /t --- timeout: 10 --- response_body -passed - +[200,200,200,503] -=== TEST 10: match project rule - rate limiting applies ---- setup - ngx.sleep(1) +=== TEST 9: match project rule --- config location /t { content_by_lua_block { + local json = require "t.toolkit.json" local http = require("resty.http") local httpc = http.new() local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" + local ress = {} for i = 1, 6, 1 do local res = httpc:request_uri(uri, { method = "GET", - headers = { ["project"] = "apisix" } + headers = { ["project"] = "apisix"} }) - if res.status ~= 200 then - ngx.say(i .. "th request should return 200, but got " .. res.status) - return - end - end - - local res = httpc:request_uri(uri, { - method = "GET", - headers = { ["project"] = "apisix" } - }) - if res.status ~= 503 then - ngx.say("7th request should return 503, but got " .. res.status) - return - end - - ngx.say("passed") - } - } ---- request -GET /t ---- timeout: 10 ---- response_body -passed - - - -=== TEST 11: 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, - [[{ - "uri": "/hello", - "plugins": { - "limit-req": { - "rules": [ - { - "rate": "${http_rate ?? 5}", - "burst": "${http_burst ?? 1}", - "key": "${remote_addr}" - } - ], - "rejected_code": 503, - "rejected_msg": "rate limited" - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1980": 1 - }, - "type": "roundrobin" - } - }]] - ) - - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- response_body -passed - - - -=== TEST 12: rules with variables in rate - default value ---- pipelined_requests eval -["GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello", "GET /hello"] ---- error_code eval -[200, 200, 200, 200, 200, 200, 503] ---- response_body eval -["hello world\n", "hello world\n", "hello world\n", "hello world\n", "hello world\n", "hello world\n", "{\"error_msg\":\"rate limited\"}\n"] - - - -=== TEST 13: rules with variables in rate - with header ---- setup - ngx.sleep(2) ---- pipelined_requests eval -["GET /hello", "GET /hello", "GET /hello", "GET /hello"] ---- more_headers -rate: 2 ---- error_code eval -[200, 200, 200, 503] ---- response_body eval -["hello world\n", "hello world\n", "hello world\n", "{\"error_msg\":\"rate limited\"}\n"] - - - -=== TEST 14: rules with different time windows ---- 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, - [[{ - "uri": "/hello", - "plugins": { - "limit-req": { - "rules": [ - { - "rate": 2, - "burst": 0, - "key": "${remote_addr}_short" - }, - { - "rate": 3, - "burst": 1, - "key": "${remote_addr}_long" - } - ], - "rejected_code": 503, - "rejected_msg": "rate limited" - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1980": 1 - }, - "type": "roundrobin" - } - }]] - ) - - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- response_body -passed - - - -=== TEST 15: test rules with different rate limits ---- 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 (rate: 2, burst: 0) - local 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(1) - - -- req 4, after sleep should pass rule 1 but might hit rule 2 - 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 + table.insert(ress, res.status) end - ngx.say("passed") + ngx.say(json.encode(ress)) } } --- request GET /t --- timeout: 10 --- response_body -passed - - - -=== TEST 16: legacy mode with string rate and burst ---- 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, - [[{ - "uri": "/hello", - "plugins": { - "limit-req": { - "rate": "100", - "burst": "10", - "key": "remote_addr", - "policy": "local" - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1980": 1 - }, - "type": "roundrobin" - } - }]] - ) - - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- response_body -passed +[200,200,200,200,200,503] -=== TEST 17: multi-rule mode with redis policy ---- 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, - [[{ - "uri": "/hello", - "plugins": { - "limit-req": { - "rules": [ - { - "rate": 100, - "burst": 20, - "key": "remote_addr" - }, - { - "rate": "$http_x_user_id", - "burst": 50, - "key": "http_x_user_id" - } - ], - "policy": "redis", - "redis_host": "127.0.0.1", - "redis_port": 6379, - "rejected_code": 429 - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1980": 1 - }, - "type": "roundrobin" - } - }]] - ) - - if code >= 300 then - ngx.status = code - ngx.print(body) - return - end - ngx.say(body) - } - } ---- skip_eval -3: no -r system('redis-cli -h 127.0.0.1 -p 6379 ping >/dev/null 2>&1') ---- response_body -passed - - - -=== TEST 18: allow_degradation when rules fail to resolve ---- 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, - [[{ - "uri": "/hello", - "plugins": { - "limit-req": { - "rules": [ - { - "rate": "$http_nonexistent_var", - "burst": 10, - "key": "${http_nonexistent_key}" - } - ], - "rejected_code": 503, - "allow_degradation": true - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1980": 1 - }, - "type": "roundrobin" - } - }]] - ) - - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- response_body -passed - - - -=== TEST 19: request with allow_degradation - passes through ---- request -GET /hello ---- error_code: 200 ---- response_body -hello world ---- error_log -failed to get limit req rules - - - -=== TEST 20: nodelay option in multi-rule mode ---- 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, - [[{ - "uri": "/hello", - "plugins": { - "limit-req": { - "rules": [ - { - "rate": 2, - "burst": 5, - "key": "remote_addr" - } - ], - "nodelay": true, - "rejected_code": 503 - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1980": 1 - }, - "type": "roundrobin" - } - }]] - ) - - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- response_body -passed - - - -=== TEST 21: nodelay - requests should not be delayed +=== TEST 10: match multi rules and specific burst by header --- config location /t { content_by_lua_block { + local json = require "t.toolkit.json" local http = require("resty.http") local httpc = http.new() local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" - local start_time = ngx.now() - for i = 1, 7, 1 do - local res = httpc:request_uri(uri) - if res.status ~= 200 then - ngx.say(i .. "th request should return 200, but got " .. res.status) - return - end - end - local elapsed = ngx.now() - start_time - - -- with nodelay=true, 7 requests should complete quickly (< 0.5s) - if elapsed > 0.5 then - ngx.say("requests took too long: " .. elapsed .. "s") - return + local ress = {} + for i = 1, 6, 1 do + local res = httpc:request_uri(uri, { + method = "GET", + headers = { ["user"] = "jack2" , ["project"] = "apisix2", ["burst1"] = "3"} + }) + table.insert(ress, res.status) end - ngx.say("passed") + ngx.say(json.encode(ress)) } } --- request GET /t --- timeout: 10 --- response_body -passed +[200,200,200,200,503,503] From 3da2b247cd6cbeae18d8c13464d1025602b7e86c Mon Sep 17 00:00:00 2001 From: niemna Date: Tue, 21 Apr 2026 23:49:06 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apisix/plugins/limit-req.lua | 2 +- apisix/plugins/limit-req/init.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/limit-req.lua b/apisix/plugins/limit-req.lua index 3e2994f75a53..00564a8c59b6 100644 --- a/apisix/plugins/limit-req.lua +++ b/apisix/plugins/limit-req.lua @@ -138,7 +138,7 @@ end function _M.access(conf, ctx) - return limit_req_init.access(conf, ctx) + return limit_req_init.rate_limit(conf, ctx) end diff --git a/apisix/plugins/limit-req/init.lua b/apisix/plugins/limit-req/init.lua index 87b77194bc1c..63ccd1de8a9c 100644 --- a/apisix/plugins/limit-req/init.lua +++ b/apisix/plugins/limit-req/init.lua @@ -196,7 +196,7 @@ local function run_limit_req(conf, rule, ctx) end -function _M.access(conf, ctx) +function _M.rate_limit(conf, ctx) core.log.info("ver: ", ctx.conf_version) local rules, err = get_rules(ctx, conf) From 642379845d5b5cf1ffb22fed1dcb1e5c5086c4be Mon Sep 17 00:00:00 2001 From: niemna Date: Tue, 21 Apr 2026 23:57:13 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8Bfix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- t/plugin/limit-req-redis-cluster.t | 2 +- t/plugin/limit-req.t | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/t/plugin/limit-req-redis-cluster.t b/t/plugin/limit-req-redis-cluster.t index bea85d0f1175..c59321e17570 100644 --- a/t/plugin/limit-req-redis-cluster.t +++ b/t/plugin/limit-req-redis-cluster.t @@ -233,7 +233,7 @@ passed GET /t --- error_code: 400 --- response_body -{"error_msg":"failed to check the configuration of plugin limit-req err: property \"rate\" validation failed: expected -1 to be greater than 0"} +{"error_msg":"failed to check the configuration of plugin limit-req err: property \"rate\" validation failed: value should match only one schema, but matches none"} diff --git a/t/plugin/limit-req.t b/t/plugin/limit-req.t index 0aad8996ad6f..9ab205086491 100644 --- a/t/plugin/limit-req.t +++ b/t/plugin/limit-req.t @@ -207,7 +207,7 @@ passed GET /t --- error_code: 400 --- response_body -{"error_msg":"failed to check the configuration of plugin limit-req err: property \"rate\" validation failed: expected -1 to be greater than 0"} +{"error_msg":"failed to check the configuration of plugin limit-req err: property \"rate\" validation failed: value should match only one schema, but matches none"} @@ -557,4 +557,4 @@ passed --- request GET /t --- response_body eval -qr/property \"rate\" validation failed: expected 0 to be greater than 0/ +qr/property \"rate\" validation failed: value should match only one schema, but matches none/ From 9982efcde0bcae2af2ac44b7db32dfff03ee7d61 Mon Sep 17 00:00:00 2001 From: niemna Date: Thu, 23 Apr 2026 00:57:18 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8Bfix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- t/plugin/limit-req-redis-cluster.t | 2 +- t/plugin/limit-req-redis.t | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/t/plugin/limit-req-redis-cluster.t b/t/plugin/limit-req-redis-cluster.t index c59321e17570..093169589144 100644 --- a/t/plugin/limit-req-redis-cluster.t +++ b/t/plugin/limit-req-redis-cluster.t @@ -613,7 +613,7 @@ passed --- request GET /t --- response_body eval -qr/property \"rate\" validation failed: expected 0 to be greater than 0/ +qr/property \"rate\" validation failed: value should match only one schema, but matches none/ diff --git a/t/plugin/limit-req-redis.t b/t/plugin/limit-req-redis.t index fd67468f2d05..518c66ae98b6 100644 --- a/t/plugin/limit-req-redis.t +++ b/t/plugin/limit-req-redis.t @@ -666,4 +666,4 @@ passed --- request GET /t --- response_body eval -qr/property \"rate\" validation failed: expected 0 to be greater than 0/ +qr/property \"rate\" validation failed: value should match only one schema, but matches none/ From 2be278ae2d3bad9e8993c76d52e3cc4010c5ec92 Mon Sep 17 00:00:00 2001 From: niemna Date: Thu, 23 Apr 2026 01:29:54 +0800 Subject: [PATCH 6/8] code lint --- t/plugin/limit-req-variable.t | 1 + 1 file changed, 1 insertion(+) diff --git a/t/plugin/limit-req-variable.t b/t/plugin/limit-req-variable.t index f6f6317b1b5c..117c6d72734b 100644 --- a/t/plugin/limit-req-variable.t +++ b/t/plugin/limit-req-variable.t @@ -288,6 +288,7 @@ GET /t [200,200,200,503] + === TEST 9: match project rule --- config location /t { From 4b0e63aaa3ee43be7756a4913129a82081bacb05 Mon Sep 17 00:00:00 2001 From: niemna Date: Thu, 23 Apr 2026 01:41:28 +0800 Subject: [PATCH 7/8] code lint --- t/plugin/limit-req-variable.t | 1 - 1 file changed, 1 deletion(-) diff --git a/t/plugin/limit-req-variable.t b/t/plugin/limit-req-variable.t index 117c6d72734b..5ad71b731206 100644 --- a/t/plugin/limit-req-variable.t +++ b/t/plugin/limit-req-variable.t @@ -259,7 +259,6 @@ failed to get limit req rules - === TEST 8: match user rule --- config location /t { From a2665a649b75f4d4444cb559212188cdccbbc73f Mon Sep 17 00:00:00 2001 From: niemna Date: Fri, 24 Apr 2026 13:56:59 +0800 Subject: [PATCH 8/8] docs --- docs/en/latest/plugins/limit-req.md | 10 +++++++--- docs/zh/latest/plugins/limit-req.md | 12 ++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/en/latest/plugins/limit-req.md b/docs/en/latest/plugins/limit-req.md index 44dc822ace99..6df1462e3adf 100644 --- a/docs/en/latest/plugins/limit-req.md +++ b/docs/en/latest/plugins/limit-req.md @@ -49,10 +49,14 @@ The `limit-req` Plugin supports two modes of rate limiting: | Name | Type | Required | Default | Valid values | Description | |------|------|----------|---------|--------------|-------------| -| rate | number | True | | > 0 | The maximum number of requests allowed per second. Requests exceeding the rate and below burst will be delayed. | -| burst | number | True | | >= 0 | The number of requests allowed to be delayed per second for throttling. Requests exceeding the rate and burst will get rejected. | +| rate | number/string | False | | > 0 or string | The maximum number of requests allowed per second. Can be a number or a string variable (e.g., `$http_rate`). Requests exceeding the rate and below burst will be delayed. Required if `rules` is not configured. | +| burst | number/string | False | | >= 0 or string | The number of requests allowed to be delayed per second for throttling. Can be a number or a string variable (e.g., `$http_burst`). Requests exceeding the rate and burst will get rejected. Required if `rules` is not configured. | +| rules | array[object] | False | | | A list of rate limiting rules. Each rule is an object containing `rate`, `burst`, and `key`. If rate/burst is not configured, then rules is required. rules and rate/burst are mutually exclusive and cannot be configured simultaneously. | +| rules.rate | number/string | True | | > 0 or string | The maximum number of requests allowed per second. Can be a number or a string variable. Requests exceeding the rate and below burst will be delayed. | +| rules.burst | number/string | True | | >= 0 or string | The number of requests allowed to be delayed per second for throttling. Can be a number or a string variable. Requests exceeding the rate and burst will get rejected. | +| 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"] | The type of key. If the `key_type` is `var`, the `key` is interpreted as a variable. If the `key_type` is `var_combination`, the `key` is interpreted as a combination of variables. | -| key | string | True | | | The key to count requests by. If the `key_type` is `var`, the `key` is interpreted as 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`. | +| key | string | False | remote_addr | | The key to count requests by. Used when `rules` is not configured. If the `key_type` is `var`, the `key` is interpreted as 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`. | | rejected_code | integer | False | 503 | [200,...,599] | The HTTP status code returned when a request is rejected for exceeding the threshold. | | rejected_msg | string | False | | non-empty | The response body returned when a request is rejected for exceeding the threshold. | | nodelay | boolean | False | false | | If true, do not delay requests within the burst threshold. | diff --git a/docs/zh/latest/plugins/limit-req.md b/docs/zh/latest/plugins/limit-req.md index ff88087b1419..286bb7788c60 100644 --- a/docs/zh/latest/plugins/limit-req.md +++ b/docs/zh/latest/plugins/limit-req.md @@ -49,10 +49,14 @@ import TabItem from '@theme/TabItem'; | 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | |------|------|----------|---------|--------------|-------------| -| rate | number | 是 | | > 0 | 每秒允许的最大请求数。超过速率且低于突发的请求将被延迟。| -| burst | number | 是 | | >= 0 | 每秒允许延迟的请求数,以进行节流。超过速率和突发的请求将被拒绝。| -| key_type | string | 否 | var | ["var", "var_combination"] | key 的类型。如果 `key_type` 为 `var`,则 `key` 将被解释为变量。如果 `key_type` 为 `var_combination`,则 `key` 将被解释为变量的组合。| -| key | string | 是 | | | 用于计数请求的 key。如果 `key_type` 为 `var`,则 `key` 将被解释为变量。变量不需要以美元符号(`$`)为前缀。如果 `key_type` 为 `var_combination`,则 `key` 会被解释为变量的组合。所有变量都应该以美元符号(`$`)为前缀。例如,要配置 `key` 使用两个请求头 `custom-a` 和 `custom-b` 的组合,则 `key` 应该配置为 `$http_custom_a $http_custom_b`。| +| rate | number/string | 否 | | > 0 或字符串 | 每秒允许的最大请求数。可以是数字或字符串变量(例如 `$http_rate`)。超过速率且低于突发的请求将被延迟。如果未配置 `rules`,则为必填项。 | +| burst | number/string | 否 | | >= 0 或字符串 | 每秒允许延迟的请求数,以进行节流。可以是数字或字符串变量(例如 `$http_burst`)。超过速率和突发的请求将被拒绝。如果未配置 `rules`,则为必填项。 | +| rules | array[object] | 否 | | | 速率限制规则列表。每个规则是一个包含 `rate`、`burst` 和 `key` 的对象。如果未配置 rate/busrst,则rules为必填项。rules与rate/burst互斥,不能同时配置。 | +| rules.rate | number/string | 是 | | > 0 或字符串 | 每秒允许的最大请求数。可以是数字或字符串变量。超过速率且低于突发的请求将被延迟。 | +| rules.burst | number/string | 是 | | >= 0 或字符串 | 每秒允许延迟的请求数,以进行节流。可以是数字或字符串变量。超过速率和突发的请求将被拒绝。 | +| rules.key | string | 是 | | | 用于统计请求的键。如果配置的键不存在,则不会执行该规则。`key` 被解释为变量的组合,例如:`$http_custom_a $http_custom_b`。 | +| key_type | string | 否 | var | ["var", "var_combination"] | key 的类型。如果 `key_type` 为 `var`,则 `key` 将被解释为变量。如果 `key_type` 为 `var_combination`,则 `key` 将被解释为变量的组合。 | +| key | string | 否 | remote_addr | | 用于统计请求的键。在未配置 `rules` 时使用。如果 `key_type` 为 `var`,则 `key` 将被解释为变量。变量不需要以美元符号(`$`)为前缀。如果 `key_type` 为 `var_combination`,则 `key` 会被解释为变量的组合。所有变量都应该以美元符号(`$`)为前缀。例如,要配置 `key` 使用两个请求头 `custom-a` 和 `custom-b` 的组合,则 `key` 应该配置为 `$http_custom_a $http_custom_b`。 | | rejected_code | integer | 否 | 503 | [200,...,599] | 请求因超出阈值而被拒绝时返回的 HTTP 状态代码。| | rejected_msg | string | 否 | | 非空 | 请求因超出阈值而被拒绝时返回的响应主体。| | nodelay | boolean | 否 | false | | 如果为 true,则不延迟突发阈值内的请求。|