This is an automated email from the ASF dual-hosted git repository.

AlinsRan 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 7c7c8f9f4 perf(limit-count): cache parsed templates, local limiters 
and header names (#14141)
7c7c8f9f4 is described below

commit 7c7c8f9f4dc3af86aa62f8ce64119908a6366e41
Author: AlinsRan <[email protected]>
AuthorDate: Thu Nov 5 17:20:38 2026 +0800

    perf(limit-count): cache parsed templates, local limiters and header names 
(#14141)
---
 apisix/core/utils.lua               | 112 +++++++++----
 apisix/plugins/limit-count/init.lua |  60 +++++--
 apisix/plugins/proxy-rewrite.lua    |   3 +-
 t/core/utils.t                      | 219 ++++++++++++++++++++++++
 t/plugin/limit-count-rules.t        | 325 ++++++++++++++++++++++++++++++++++++
 t/plugin/proxy-rewrite3.t           |  61 +++++++
 6 files changed, 732 insertions(+), 48 deletions(-)

diff --git a/apisix/core/utils.lua b/apisix/core/utils.lua
index fd488047c..6df41b921 100644
--- a/apisix/core/utils.lua
+++ b/apisix/core/utils.lua
@@ -28,6 +28,8 @@ local json           = require("apisix.core.json")
 local string         = require("apisix.core.string")
 local dns_client     = require("apisix.core.dns.client")
 local ngx_re         = require("ngx.re")
+local lrucache       = require("resty.lrucache.pureffi")
+local tablepool      = require("tablepool")
 local ipmatcher      = require("resty.ipmatcher")
 local ffi            = require("ffi")
 local base           = require("resty.core.base")
@@ -49,6 +51,7 @@ local exiting        = ngx.worker.exiting
 local ngx_sleep      = ngx.sleep
 local ipairs         = ipairs
 local str_format     = string.format
+local tab_concat     = table.concat
 local ngx_now        = ngx.now
 local ngx_update_time = ngx.update_time
 local math_random    = math.random
@@ -312,36 +315,59 @@ _M.sleep = sleep
 
 local resolve_var
 do
-    local _ctx
-    local n_resolved
     local pat = [[(?<!\\)\$(\{\s*([^}]+?)\s*\}|([\w\.]+))]]
-    local _escaper
+    -- pureffi: once full, resty.lrucache rehashes its key table on most
+    -- evictions, so a configuration with more templates than fit would pay
+    -- for a rehash on most calls
+    local tpl_cache = lrucache.new(1024)
+
+    -- Split a template into literal strings and variables once, so resolving 
it
+    -- is a table walk instead of a regex gsub with a callback.
+    local function parse(tpl)
+        local segs = {}
+        local m_ctx = {pos = 1}
+        local last = 1
+        while true do
+            local m, err = re_match(tpl, pat, "jo", m_ctx)
+            if err then
+                return nil, err
+            end
+            if not m then
+                break
+            end
 
-    local function resolve(m)
-        local variable = m[2] or m[3]
-        -- handle the default value with ?? operator
-        local segs, err = ngx_re.split(variable, [[\s*\?\?\s*]])
-        if not segs then
-            log.error("failed to split variable ", variable, ": ", err)
-            return ""
-        end
-        local v = _ctx[segs[1]]
+            local from = m_ctx.pos - #m[0]
+            if from > last then
+                segs[#segs + 1] = sub_str(tpl, last, from - 1)
+            end
 
-        if v == nil then
-            if #segs == 1 then
-                return ""
+            local variable = m[2] or m[3]
+            local seg = {name = variable}
+            -- handle the default value with ?? operator
+            if core_str.find(variable, "??") then
+                local parts, err = ngx_re.split(variable, [[\s*\?\?\s*]], "jo")
+                if not parts then
+                    return nil, err
+                end
+                seg.name = parts[1]
+                -- "${a??}" splits into {"a"}, which means no default
+                seg.has_default = #parts ~= 1
+                seg.default = parts[2]
             end
-            v = segs[2]
+            segs[#segs + 1] = seg
+            last = m_ctx.pos
         end
-        n_resolved = n_resolved + 1
-        if _escaper then
-            return _escaper(tostring(v))
+
+        if last <= #tpl then
+            segs[#segs + 1] = sub_str(tpl, last)
         end
-        return tostring(v)
+        return segs
     end
 
-    function resolve_var(tpl, ctx, escaper)
-        n_resolved = 0
+    -- no_cache: the template is built per request, so caching it would only
+    -- evict the configured templates
+    function resolve_var(tpl, ctx, escaper, no_cache)
+        local n_resolved = 0
         if not tpl then
             return tpl, nil, n_resolved
         end
@@ -351,16 +377,42 @@ do
             return tpl, nil, n_resolved
         end
 
-        -- avoid creating temporary function
-        _ctx = ctx
-        _escaper = escaper
-        local res, _, err = re_gsub(tpl, pat, resolve, "jo")
-        _ctx = nil
-        _escaper = nil
-        if not res then
-            return nil, err
+        local segs = not no_cache and tpl_cache:get(tpl)
+        if not segs then
+            local err
+            segs, err = parse(tpl)
+            if not segs then
+                return nil, err
+            end
+            if not no_cache then
+                tpl_cache:set(tpl, segs)
+            end
+        end
+
+        local n = #segs
+        local buf = tablepool.fetch("resolve_var", n, 0)
+        for i = 1, n do
+            local seg = segs[i]
+            if type(seg) == "table" then
+                local v = ctx[seg.name]
+                if v == nil and not seg.has_default then
+                    seg = ""
+                else
+                    if v == nil then
+                        v = seg.default
+                    end
+                    n_resolved = n_resolved + 1
+                    seg = tostring(v)
+                    if escaper then
+                        seg = tostring(escaper(seg))
+                    end
+                end
+            end
+            buf[i] = seg
         end
 
+        local res = tab_concat(buf, "", 1, n)
+        tablepool.release("resolve_var", buf)
         return res, nil, n_resolved
     end
 end
diff --git a/apisix/plugins/limit-count/init.lua 
b/apisix/plugins/limit-count/init.lua
index 721586252..111ccc77f 100644
--- a/apisix/plugins/limit-count/init.lua
+++ b/apisix/plugins/limit-count/init.lua
@@ -27,6 +27,7 @@ local get_phase = ngx.get_phase
 local ngx_now = ngx.now
 local math_floor = math.floor
 local str_format = string.format
+local setmetatable = setmetatable
 
 local NO_DELAYED_SYNC = -1
 -- Redis counter storage-format version, passed into the Redis backends and
@@ -58,6 +59,9 @@ local group_conf_lru = core.lrucache.new({
 })
 local group_limit_lru = core.lrucache.new({type = 'plugin'})
 local lrucache = core.lrucache.new({type = 'plugin', serial_creating = true})
+-- pureffi: once full, resty.lrucache rehashes its key table on most evictions
+local limit_headers_lru = require("resty.lrucache.pureffi").new(1024)
+local limit_key_prefix = setmetatable({}, {__mode = "k"})
 
 policy_to_additional_properties["redis-sentinel"] = {
     properties = {
@@ -372,8 +376,12 @@ local function gen_limit_key(conf, ctx, key)
         return nil
     end
 
-    local new_key = parent.resource_key .. ':' .. 
apisix_plugin.conf_version(conf)
-                    .. ':' .. key
+    local prefix = limit_key_prefix[conf]
+    if not prefix then
+        prefix = parent.resource_key .. ':' .. 
apisix_plugin.conf_version(conf) .. ':'
+        limit_key_prefix[conf] = prefix
+    end
+    local new_key = prefix .. key
     if conf._vid then
         -- conf has _vid means it's from workflow plugin, add _vid to the key
         -- so that the counter is unique per action.
@@ -467,28 +475,46 @@ end
 
 
 
-local function construct_rate_limiting_headers(conf, rule, metadata)
-    if rule.header_prefix then
-        local base_limit = conf.limit_header or metadata.limit_header
-        local base_remaining = conf.remaining_header or 
metadata.remaining_header
-        local base_reset = conf.reset_header or metadata.reset_header
-        -- Insert rule prefix before "RateLimit-" to preserve any custom 
header base
-        -- e.g. "X-AI-RateLimit-Limit" + prefix "1" -> "X-AI-1-RateLimit-Limit"
-        local prefix = tostring(rule.header_prefix)
+local function gen_limit_headers(prefix, base_limit, base_remaining, 
base_reset)
+    if not prefix then
         return {
-            limit_header = base_limit:gsub("RateLimit%-", prefix .. 
"-RateLimit-", 1),
-            remaining_header = base_remaining:gsub("RateLimit%-", prefix .. 
"-RateLimit-", 1),
-            reset_header = base_reset:gsub("RateLimit%-", prefix .. 
"-RateLimit-", 1),
+            limit_header = base_limit,
+            remaining_header = base_remaining,
+            reset_header = base_reset,
         }
     end
-    return  {
-        limit_header = conf.limit_header or metadata.limit_header,
-        remaining_header = conf.remaining_header or metadata.remaining_header,
-        reset_header = conf.reset_header or metadata.reset_header,
+
+    -- Insert rule prefix before "RateLimit-" to preserve any custom header 
base
+    -- e.g. "X-AI-RateLimit-Limit" + prefix "1" -> "X-AI-1-RateLimit-Limit"
+    local repl = prefix .. "-RateLimit-"
+    return {
+        limit_header = base_limit:gsub("RateLimit%-", repl, 1),
+        remaining_header = base_remaining:gsub("RateLimit%-", repl, 1),
+        reset_header = base_reset:gsub("RateLimit%-", repl, 1),
     }
 end
 
 
+local function construct_rate_limiting_headers(conf, rule, metadata)
+    local base_limit = conf.limit_header or metadata.limit_header
+    local base_remaining = conf.remaining_header or metadata.remaining_header
+    local base_reset = conf.reset_header or metadata.reset_header
+    local key = base_limit .. "\0" .. base_remaining .. "\0" .. base_reset
+    local prefix
+    if rule.header_prefix then
+        prefix = tostring(rule.header_prefix)
+        key = prefix .. "\0" .. key
+    end
+
+    local headers = limit_headers_lru:get(key)
+    if not headers then
+        headers = gen_limit_headers(prefix, base_limit, base_remaining, 
base_reset)
+        limit_headers_lru:set(key, headers)
+    end
+    return headers
+end
+
+
 -- Collects what $rate_limiting_info reports about the window this request was
 -- counted in. `info` is what the limiter returned next to the decision; the
 -- request's own figures are added to it.
diff --git a/apisix/plugins/proxy-rewrite.lua b/apisix/plugins/proxy-rewrite.lua
index 29e8f5f02..90e662b3e 100644
--- a/apisix/plugins/proxy-rewrite.lua
+++ b/apisix/plugins/proxy-rewrite.lua
@@ -302,7 +302,8 @@ do
     local function resolve_header_value(value, ctx)
         local val = core.utils.resolve_var_with_captures(value,
                                         ctx.proxy_rewrite_regex_uri_captures)
-        return core.utils.resolve_var(val, ctx.var)
+        -- a value with captures substituted is a new template per request
+        return core.utils.resolve_var(val, ctx.var, nil, val ~= value)
     end
 
 
diff --git a/t/core/utils.t b/t/core/utils.t
index 638643ff1..b0897ad8f 100644
--- a/t/core/utils.t
+++ b/t/core/utils.t
@@ -453,3 +453,222 @@ hostname cached: true
 hostname valid: true
 system hostname: .+
 hostname match: true
+
+
+
+=== TEST 14: resolve_var reports how many variables it resolved
+--- config
+    location /t {
+        content_by_lua_block {
+            local resolve_var = require("apisix.core.utils").resolve_var
+            local ctx = {you = "John", me = "David", num = 42, f = false}
+            local cases = {
+                "$you and $me",
+                "${nobody}",
+                "${nobody??Rose}",
+                "${nobody??}",
+                "${ nobody ?? 0 }",
+                "$you$nobody$me",
+                "\\$me",
+                "no vars",
+                "$num",
+                "$f",
+            }
+            for _, case in ipairs(cases) do
+                local res, err, n = resolve_var(case, ctx)
+                ngx.say(case, " => ", res, " ", n, " ", tostring(err))
+            end
+        }
+    }
+--- request
+GET /t
+--- response_body
+$you and $me => John and David 2 nil
+${nobody} =>  0 nil
+${nobody??Rose} => Rose 1 nil
+${nobody??} =>  0 nil
+${ nobody ?? 0 } => 0 1 nil
+$you$nobody$me => JohnDavid 2 nil
+\$me => \$me 0 nil
+no vars => no vars 0 nil
+$num => 42 1 nil
+$f => false 1 nil
+
+
+
+=== TEST 15: resolve_var applies the escaper to every resolved value
+--- config
+    location /t {
+        content_by_lua_block {
+            local resolve_var = require("apisix.core.utils").resolve_var
+            local ctx = {sp = "x y", sl = "/"}
+            local res, err, n = resolve_var("$sp-${sl}-${none??a b}", ctx, 
ngx.escape_uri)
+            ngx.say(res, " ", n, " ", tostring(err))
+            res, err, n = resolve_var("$sp-$sl", ctx, function(v)
+                return v == "/" and 1 or nil
+            end)
+            ngx.say(res, " ", n, " ", tostring(err))
+        }
+    }
+--- request
+GET /t
+--- response_body
+x%20y-%2F-a%20b 3 nil
+nil-1 2 nil
+
+
+
+=== TEST 16: resolve_var reuses a template with different values on every call
+--- config
+    location /t {
+        content_by_lua_block {
+            local resolve_var = require("apisix.core.utils").resolve_var
+            local tpl = "${host}:${uri}"
+            for i = 1, 3 do
+                local res, _, n = resolve_var(tpl, {host = "h" .. i, uri = 
"/u" .. i})
+                ngx.say(res, " ", n)
+            end
+            local res, _, n = resolve_var(tpl, {host = "h4"})
+            ngx.say(res, " ", n)
+        }
+    }
+--- request
+GET /t
+--- response_body
+h1:/u1 2
+h2:/u2 2
+h3:/u3 2
+h4: 1
+
+
+
+=== TEST 17: a variable getter may resolve another template while it is called
+--- config
+    location /t {
+        content_by_lua_block {
+            local resolve_var = require("apisix.core.utils").resolve_var
+            local ctx = setmetatable({}, {__index = function(_, key)
+                if key == "outer" then
+                    return (resolve_var("in-$inner", {inner = "x"}))
+                end
+                if key == "b" then
+                    return "B"
+                end
+            end})
+            local res, err, n = resolve_var("$outer/$b", ctx)
+            ngx.say(res, " ", n, " ", tostring(err))
+        }
+    }
+--- request
+GET /t
+--- response_body
+in-x/B 2 nil
+
+
+
+=== TEST 18: resolve_var keeps working after a variable getter raises an error
+--- config
+    location /t {
+        content_by_lua_block {
+            local resolve_var = require("apisix.core.utils").resolve_var
+            local ctx = setmetatable({}, {__index = function(_, key)
+                if key == "boom" then
+                    error("getter failed")
+                end
+                if key == "outer" then
+                    return (resolve_var("in-$inner", {inner = "x"}))
+                end
+                return key == "b" and "B" or nil
+            end})
+            local ok = pcall(resolve_var, "keep-$b/$boom", ctx)
+            ngx.say("raised: ", not ok)
+            for _ = 1, 2 do
+                local res, err, n = resolve_var("$outer/$b", ctx)
+                ngx.say(res, " ", n, " ", tostring(err))
+            end
+        }
+    }
+--- request
+GET /t
+--- response_body
+raised: true
+in-x/B 2 nil
+in-x/B 2 nil
+
+
+
+=== TEST 19: resolve_var with no_cache resolves the template without caching it
+--- config
+    location /t {
+        content_by_lua_block {
+            local resolve_var = require("apisix.core.utils").resolve_var
+            local tpl_cache
+            for i = 1, math.huge do
+                local name, value = debug.getupvalue(resolve_var, i)
+                if not name then
+                    break
+                end
+                if name == "tpl_cache" then
+                    tpl_cache = value
+                    break
+                end
+            end
+
+            local res, err, n = resolve_var("once-$a", {a = "x"}, nil, true)
+            ngx.say(res, " ", n, " ", tostring(err))
+            ngx.say("cached: ", tpl_cache:get("once-$a") ~= nil)
+
+            res = resolve_var("kept-$a", {a = "x"})
+            ngx.say(res)
+            ngx.say("cached: ", tpl_cache:get("kept-$a") ~= nil)
+        }
+    }
+--- request
+GET /t
+--- response_body
+once-x 1 nil
+cached: false
+kept-x
+cached: true
+
+
+
+=== TEST 20: resolve_var stays correct with more templates than its cache holds
+--- config
+    location /t {
+        content_by_lua_block {
+            local resolve_var = require("apisix.core.utils").resolve_var
+            local tpl_cache
+            for i = 1, math.huge do
+                local name, value = debug.getupvalue(resolve_var, i)
+                if not name then
+                    break
+                end
+                if name == "tpl_cache" then
+                    tpl_cache = value
+                    break
+                end
+            end
+
+            -- the second pass resolves templates the first one evicted
+            local bad = 0
+            for pass = 1, 2 do
+                local ctx = {uri = "/p" .. pass}
+                for i = 1, 3000 do
+                    local res, err, n = resolve_var("/svc-" .. i .. "${uri}", 
ctx)
+                    if res ~= "/svc-" .. i .. "/p" .. pass or err or n ~= 1 
then
+                        bad = bad + 1
+                    end
+                end
+            end
+            ngx.say("mismatches: ", bad)
+            ngx.say("first template evicted: ", tpl_cache:get("/svc-1${uri}") 
== nil)
+            ngx.say("last template cached: ", tpl_cache:get("/svc-3000${uri}") 
~= nil)
+        }
+    }
+--- request
+GET /t
+--- response_body
+mismatches: 0
+first template evicted: true
+last template cached: true
diff --git a/t/plugin/limit-count-rules.t b/t/plugin/limit-count-rules.t
index a2bee51f0..9d30d07e3 100644
--- a/t/plugin/limit-count-rules.t
+++ b/t/plugin/limit-count-rules.t
@@ -649,3 +649,328 @@ GET /hello
 x-user: testuser
 x-project: myproject
 --- error_code: 200
+
+
+
+=== TEST 23: setup routes whose rule resolves its count per request
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local conf = [[{
+                "methods": ["GET"],
+                "plugins": {
+                    "limit-count": {
+                        "rejected_code": 503,
+                        "rules": [
+                            {
+                                "count": "${http_count ?? 3}",
+                                "time_window": 60,
+                                "key": "${http_x_user}"
+                            }
+                        ]
+                    }
+                },
+                "upstream": {
+                    "nodes": {
+                        "127.0.0.1:1980": 1
+                    },
+                    "type": "roundrobin"
+                },
+                "uri": "%s"
+            }]]
+            for id, uri in pairs({["1"] = "/hello", ["2"] = "/hello1"}) do
+                local code, body = t('/apisix/admin/routes/' .. id, 
ngx.HTTP_PUT,
+                                     string.format(conf, uri))
+                if code >= 300 then
+                    ngx.status = code
+                    ngx.say(body)
+                    return
+                end
+            end
+            ngx.say("passed")
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 24: the same key limited under counts that change per request
+--- config
+    location /t {
+        content_by_lua_block {
+            local http = require("resty.http")
+            local base = "http://127.0.0.1:"; .. ngx.var.server_port
+            local function hit(uri, user, count)
+                local httpc = http.new()
+                local res, err = httpc:request_uri(base .. uri, {
+                    headers = {["X-User"] = user, ["Count"] = count},
+                })
+                if not res then
+                    ngx.say("failed: ", err)
+                    return
+                end
+                ngx.say(uri, " ", user, " ", count or "-", ": ", res.status, " 
",
+                        res.headers["X-1-RateLimit-Limit"], " ",
+                        res.headers["X-1-RateLimit-Remaining"])
+            end
+            hit("/hello", "u24", "3")
+            hit("/hello", "u24", "5")
+            hit("/hello", "u24", "3")
+            hit("/hello", "u24", nil)
+            hit("/hello", "u24", "5")
+            hit("/hello", "u24", "5")
+            hit("/hello", "other24", "3")
+        }
+    }
+--- response_body
+/hello u24 3: 200 3 2
+/hello u24 5: 200 5 3
+/hello u24 3: 200 3 0
+/hello u24 -: 503 3 0
+/hello u24 5: 200 5 0
+/hello u24 5: 503 5 0
+/hello other24 3: 200 3 2
+
+
+
+=== TEST 25: a count that fails to resolve rejects only that request
+--- config
+    location /t {
+        content_by_lua_block {
+            local http = require("resty.http")
+            local base = "http://127.0.0.1:"; .. ngx.var.server_port
+            for _, count in ipairs({"abc", "2", "-1", "2", "2"}) do
+                local httpc = http.new()
+                local res, err = httpc:request_uri(base .. "/hello", {
+                    headers = {["X-User"] = "u25", ["Count"] = count},
+                })
+                if not res then
+                    ngx.say("failed: ", err)
+                    return
+                end
+                ngx.say(count, ": ", res.status, " ",
+                        res.headers["X-1-RateLimit-Remaining"])
+            end
+        }
+    }
+--- response_body
+abc: 500 nil
+2: 200 1
+-1: 500 nil
+2: 200 0
+2: 503 0
+--- error_log
+failed to get rate limit rules: resolved value is not a number
+failed to get rate limit rules: resolved value must be a positive number
+
+
+
+=== TEST 26: routes with the same rule shape keep separate counters
+--- config
+    location /t {
+        content_by_lua_block {
+            local http = require("resty.http")
+            local base = "http://127.0.0.1:"; .. ngx.var.server_port
+            for _, uri in ipairs({"/hello", "/hello1", "/hello", "/hello1", 
"/hello"}) do
+                local httpc = http.new()
+                local res, err = httpc:request_uri(base .. uri, {
+                    headers = {["X-User"] = "u26", ["Count"] = "2"},
+                })
+                if not res then
+                    ngx.say("failed: ", err)
+                    return
+                end
+                ngx.say(uri, ": ", res.status, " ", 
res.headers["X-1-RateLimit-Remaining"])
+            end
+        }
+    }
+--- response_body
+/hello: 200 1
+/hello1: 200 1
+/hello: 200 0
+/hello1: 200 0
+/hello: 503 0
+
+
+
+=== TEST 27: changing the plugin configuration starts a new counter
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local http = require("resty.http")
+            local base = "http://127.0.0.1:"; .. ngx.var.server_port
+            local function hit()
+                local httpc = http.new()
+                local res, err = httpc:request_uri(base .. "/hello", {
+                    headers = {["X-User"] = "u27", ["Count"] = "1"},
+                })
+                if not res then
+                    return "failed: " .. err
+                end
+                return res.status
+            end
+            ngx.say(hit(), " ", hit())
+
+            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PATCH, [[{
+                "plugins": {
+                    "limit-count": {
+                        "rejected_code": 429,
+                        "rules": [
+                            {
+                                "count": "${http_count ?? 3}",
+                                "time_window": 60,
+                                "key": "${http_x_user}"
+                            }
+                        ]
+                    }
+                }
+            }]])
+            if code >= 300 then
+                ngx.say(body)
+                return
+            end
+            ngx.sleep(0.5)
+            ngx.say(hit(), " ", hit())
+        }
+    }
+--- response_body
+200 503
+200 429
+
+
+
+=== TEST 28: rule header names follow a plugin metadata change
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local http = require("resty.http")
+            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, [[{
+                "plugins": {
+                    "limit-count": {
+                        "rejected_code": 503,
+                        "rules": [
+                            {
+                                "count": 5,
+                                "time_window": 60,
+                                "key": "${http_x_user}",
+                                "header_prefix": "jack"
+                            }
+                        ]
+                    }
+                },
+                "upstream": {
+                    "nodes": {"127.0.0.1:1980": 1},
+                    "type": "roundrobin"
+                },
+                "uri": "/hello"
+            }]])
+            if code >= 300 then
+                ngx.say(body)
+                return
+            end
+            ngx.sleep(0.5)
+
+            local function hit()
+                local httpc = http.new()
+                local res, err = httpc:request_uri(
+                    "http://127.0.0.1:"; .. ngx.var.server_port .. "/hello",
+                    {headers = {["X-User"] = "u28"}})
+                if not res then
+                    ngx.say("failed: ", err)
+                    return
+                end
+                ngx.say(res.headers["X-jack-RateLimit-Remaining"], " ",
+                        res.headers["APISIX-jack-RateLimit-Remaining"])
+            end
+            hit()
+
+            code, body = t('/apisix/admin/plugin_metadata/limit-count',
+                ngx.HTTP_PUT, [[{
+                    "limit_header": "APISIX-RateLimit-Limit",
+                    "remaining_header": "APISIX-RateLimit-Remaining",
+                    "reset_header": "APISIX-RateLimit-Reset"
+                }]])
+            if code >= 300 then
+                ngx.say(body)
+                return
+            end
+            ngx.sleep(0.5)
+            hit()
+
+            code, body = t('/apisix/admin/plugin_metadata/limit-count',
+                ngx.HTTP_DELETE)
+            if code >= 300 then
+                ngx.say(body)
+                return
+            end
+            ngx.sleep(0.5)
+            hit()
+        }
+    }
+--- response_body
+4 nil
+nil 3
+2 nil
+
+
+
+=== TEST 29: a count resolved per request without rules is enforced per value
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local http = require("resty.http")
+            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, [[{
+                "plugins": {
+                    "limit-count": {
+                        "count": "${http_count ?? 3}",
+                        "time_window": 60,
+                        "rejected_code": 503,
+                        "key_type": "var",
+                        "key": "http_x_user"
+                    }
+                },
+                "upstream": {
+                    "nodes": {"127.0.0.1:1980": 1},
+                    "type": "roundrobin"
+                },
+                "uri": "/hello"
+            }]])
+            if code >= 300 then
+                ngx.say(body)
+                return
+            end
+            ngx.sleep(0.5)
+
+            local function hit(user, count)
+                local httpc = http.new()
+                local res, err = httpc:request_uri(
+                    "http://127.0.0.1:"; .. ngx.var.server_port .. "/hello",
+                    {headers = {["X-User"] = user, ["Count"] = count}})
+                if not res then
+                    ngx.say("failed: ", err)
+                    return
+                end
+                ngx.say(user, " ", count or "-", ": ", res.status, " ",
+                        res.headers["X-RateLimit-Limit"], " ",
+                        res.headers["X-RateLimit-Remaining"])
+            end
+            hit("u29a", "1")
+            hit("u29a", "1")
+            hit("u29b", "2")
+            hit("u29b", "2")
+            hit("u29b", "2")
+            hit("u29c")
+        }
+    }
+--- response_body
+u29a 1: 200 1 0
+u29a 1: 503 1 0
+u29b 2: 200 2 1
+u29b 2: 200 2 0
+u29b 2: 503 2 0
+u29c -: 200 3 2
diff --git a/t/plugin/proxy-rewrite3.t b/t/plugin/proxy-rewrite3.t
index f16a03810..5e073ca29 100644
--- a/t/plugin/proxy-rewrite3.t
+++ b/t/plugin/proxy-rewrite3.t
@@ -1043,3 +1043,64 @@ GET /print%5Furi%5Fdetailed HTTP/1.1
 --- response_body
 ngx.var.uri: /print_uri_detailed
 ngx.var.request_uri: /print_uri_detailed
+
+
+
+=== TEST 44: use both a regex_uri capture and a variable in a header
+--- 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": "/test/*",
+                      "plugins": {
+                        "proxy-rewrite": {
+                            "regex_uri": ["^/test/(.*)", "/echo"],
+                            "headers": {
+                                "add": {
+                                    "X-Request-ID": "$1-$http_x_tag"
+                                }
+                            }
+                        }
+                      },
+                      "upstream": {
+                          "nodes": {
+                              "127.0.0.1:1980": 1
+                          },
+                          "type": "roundrobin"
+                      }
+                 }]]
+                 )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 45: hit with one capture and variable
+--- request
+GET /test/a
+--- more_headers
+X-Tag: t1
+--- response_headers
+X-Request-ID: a-t1
+
+
+
+=== TEST 46: hit with another capture and variable
+--- request
+GET /test/b
+--- more_headers
+X-Tag: t2
+--- response_headers
+X-Request-ID: b-t2

Reply via email to