diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua index c1e46220a4e4..7a1f4e9c7722 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -276,6 +276,7 @@ http { {% if enabled_plugins["limit-count"] then %} lua_shared_dict plugin-limit-count {* http.lua_shared_dict["plugin-limit-count"] *}; lua_shared_dict plugin-limit-count-redis-cluster-slot-lock {* http.lua_shared_dict["plugin-limit-count-redis-cluster-slot-lock"] *}; + lua_shared_dict plugin-limit-count-reset-header {* http.lua_shared_dict["plugin-limit-count"] *}; {% end %} {% if enabled_plugins["prometheus"] and not enabled_stream_plugins["prometheus"] then %} diff --git a/apisix/plugins/limit-count/init.lua b/apisix/plugins/limit-count/init.lua index c9051d2e14ef..ce7434a6c82d 100644 --- a/apisix/plugins/limit-count/init.lua +++ b/apisix/plugins/limit-count/init.lua @@ -14,18 +14,20 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -- -local limit_local_new = require("resty.limit.count").new local core = require("apisix.core") local apisix_plugin = require("apisix.plugin") local tab_insert = table.insert local ipairs = ipairs local pairs = pairs - local plugin_name = "limit-count" local limit_redis_cluster_new local limit_redis_new +local limit_local_new do + local local_src = "apisix.plugins.limit-count.limit-count-local" + limit_local_new = require(local_src).new + local redis_src = "apisix.plugins.limit-count.limit-count-redis" limit_redis_new = require(redis_src).new @@ -39,7 +41,6 @@ local group_conf_lru = core.lrucache.new({ type = 'plugin', }) - local policy_to_additional_properties = { redis = { properties = { @@ -242,7 +243,6 @@ local function gen_limit_obj(conf, ctx) return core.lrucache.plugin_ctx(lrucache, ctx, extra_key, create_limit_obj, conf) end - function _M.rate_limit(conf, ctx) core.log.info("ver: ", ctx.conf_version) @@ -283,10 +283,17 @@ function _M.rate_limit(conf, ctx) key = gen_limit_key(conf, ctx, key) core.log.info("limit key: ", key) - local delay, remaining = lim:incoming(key, true) + local delay, remaining, reset = lim:incoming(key, true, conf) if not delay then local err = remaining if err == "rejected" then + -- show count limit header when rejected + if conf.show_limit_quota_header then + core.response.set_header("X-RateLimit-Limit", conf.count, + "X-RateLimit-Remaining", 0, + "X-RateLimit-Reset", reset) + end + if conf.rejected_msg then return conf.rejected_code, { error_msg = conf.rejected_msg } end @@ -302,7 +309,8 @@ function _M.rate_limit(conf, ctx) if conf.show_limit_quota_header then core.response.set_header("X-RateLimit-Limit", conf.count, - "X-RateLimit-Remaining", remaining) + "X-RateLimit-Remaining", remaining, + "X-RateLimit-Reset", reset) end end diff --git a/apisix/plugins/limit-count/limit-count-local.lua b/apisix/plugins/limit-count/limit-count-local.lua new file mode 100644 index 000000000000..37e7a1dbef5b --- /dev/null +++ b/apisix/plugins/limit-count/limit-count-local.lua @@ -0,0 +1,81 @@ +-- +-- 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_local_new = require("resty.limit.count").new +local ngx = ngx +local ngx_time = ngx.time +local assert = assert +local setmetatable = setmetatable +local core = require("apisix.core") + +local _M = {} + +local mt = { + __index = _M +} + +local function set_endtime(self, key, time_window) + -- set an end time + local end_time = ngx_time() + time_window + -- save to dict by key + local success, err = self.dict:set(key, end_time, time_window) + + if not success then + core.log.error("dict set key ", key, " error: ", err) + end + + local reset = time_window + return reset +end + +local function read_reset(self, key) + -- read from dict + local end_time = (self.dict:get(key) or 0) + local reset = end_time - ngx_time() + if reset < 0 then + reset = 0 + end + return reset +end + +function _M.new(plugin_name, limit, window, conf) + assert(limit > 0 and window > 0) + + local self = { + limit_count = limit_local_new(plugin_name, limit, window, conf), + dict = ngx.shared["plugin-limit-count-reset-header"] + } + + return setmetatable(self, mt) +end + +function _M.incoming(self, key, commit, conf) + local delay, remaining = self.limit_count:incoming(key, commit) + local reset = 0 + if not delay then + return delay, remaining, reset + end + + if remaining == conf.count - 1 then + reset = set_endtime(self, key, conf.time_window) + else + reset = read_reset(self, key) + end + + return delay, remaining, reset +end + +return _M diff --git a/apisix/plugins/limit-count/limit-count-redis-cluster.lua b/apisix/plugins/limit-count/limit-count-redis-cluster.lua index 27d4e85faa4e..1fa57aac8fb9 100644 --- a/apisix/plugins/limit-count/limit-count-redis-cluster.lua +++ b/apisix/plugins/limit-count/limit-count-redis-cluster.lua @@ -30,11 +30,12 @@ local mt = { local script = core.string.compress_script([=[ - if redis.call('ttl', KEYS[1]) < 0 then + local ttl = redis.call('ttl', KEYS[1]) + if ttl < 0 then redis.call('set', KEYS[1], ARGV[1] - 1, 'EX', ARGV[2]) - return ARGV[1] - 1 + return {ARGV[1] - 1, ARGV[2]} end - return redis.call('incrby', KEYS[1], -1) + return {redis.call('incrby', KEYS[1], -1), ttl} ]=]) @@ -91,16 +92,20 @@ function _M.incoming(self, key) local window = self.window key = self.plugin_name .. tostring(key) - local remaining, err = red:eval(script, 1, key, limit, window) + local ttl = 0 + local res, err = red:eval(script, 1, key, limit, window) if err then - return nil, err + return nil, err, ttl end + local remaining = res[1] + ttl = res[2] + if remaining < 0 then - return nil, "rejected" + return nil, "rejected", ttl end - return 0, remaining + return 0, remaining, ttl end diff --git a/apisix/plugins/limit-count/limit-count-redis.lua b/apisix/plugins/limit-count/limit-count-redis.lua index d5c4648248bc..bed920229b0c 100644 --- a/apisix/plugins/limit-count/limit-count-redis.lua +++ b/apisix/plugins/limit-count/limit-count-redis.lua @@ -30,32 +30,17 @@ local mt = { local script = core.string.compress_script([=[ - if redis.call('ttl', KEYS[1]) < 0 then + local ttl = redis.call('ttl', KEYS[1]) + if ttl < 0 then redis.call('set', KEYS[1], ARGV[1] - 1, 'EX', ARGV[2]) - return ARGV[1] - 1 + return {ARGV[1] - 1, ARGV[2]} end - return redis.call('incrby', KEYS[1], -1) + return {redis.call('incrby', KEYS[1], -1), ttl} ]=]) - -function _M.new(plugin_name, limit, window, conf) - assert(limit > 0 and window > 0) - - local self = { - limit = limit, - window = window, - conf = conf, - plugin_name = plugin_name, - } - return setmetatable(self, mt) -end - - -function _M.incoming(self, key) - local conf = self.conf +local function redis_cli(conf) local red = redis_new() local timeout = conf.redis_timeout or 1000 -- 1sec - core.log.info("ttl key: ", key, " timeout: ", timeout) red:set_timeouts(timeout, timeout, timeout) @@ -85,27 +70,52 @@ function _M.incoming(self, key) -- core.log.info(" err: ", err) return nil, err end + return red, nil +end + +function _M.new(plugin_name, limit, window, conf) + assert(limit > 0 and window > 0) + + local self = { + limit = limit, + window = window, + conf = conf, + plugin_name = plugin_name, + } + return setmetatable(self, mt) +end + +function _M.incoming(self, key) + local conf = self.conf + local red, err = redis_cli(conf) + if not red then + return red, err, 0 + end local limit = self.limit local window = self.window - local remaining + local res key = self.plugin_name .. tostring(key) - remaining, err = red:eval(script, 1, key, limit, window) + local ttl = 0 + res, err = red:eval(script, 1, key, limit, window) if err then - return nil, err + return nil, err, ttl end + local remaining = res[1] + ttl = res[2] + local ok, err = red:set_keepalive(10000, 100) if not ok then - return nil, err + return nil, err, ttl end if remaining < 0 then - return nil, "rejected" + return nil, "rejected", ttl end - return 0, remaining + return 0, remaining, ttl end diff --git a/docs/en/latest/plugins/limit-count.md b/docs/en/latest/plugins/limit-count.md index b098dbd32e20..09f22f43f5d5 100644 --- a/docs/en/latest/plugins/limit-count.md +++ b/docs/en/latest/plugins/limit-count.md @@ -254,7 +254,7 @@ curl -i http://127.0.0.1:9180/apisix/admin/routes/1 \ ## Example usage -The above configuration limits to 2 requests in 60 seconds. The first two requests will work and the response headers will contain the headers `X-RateLimit-Limit` and `X-RateLimit-Remaining`: +The above configuration limits to 2 requests in 60 seconds. The first two requests will work and the response headers will contain the headers `X-RateLimit-Limit` and `X-RateLimit-Remaining` and `X-RateLimit-Reset`, represents the total number of requests that are limited, the number of requests that can still be sent, and the number of seconds left for the counter to reset: ```shell curl -i http://127.0.0.1:9080/index.html @@ -267,16 +267,20 @@ Content-Length: 13175 Connection: keep-alive X-RateLimit-Limit: 2 X-RateLimit-Remaining: 0 +X-RateLimit-Reset: 58 Server: APISIX web server ``` -When you visit for a third time in the 60 seconds, you will receive a response with 503 code: +When you visit for a third time in the 60 seconds, you will receive a response with 503 code. Currently, in the case of rejection, the limit count headers is also returned: ```shell HTTP/1.1 503 Service Temporarily Unavailable Content-Type: text/html Content-Length: 194 Connection: keep-alive +X-RateLimit-Limit: 2 +X-RateLimit-Remaining: 0 +X-RateLimit-Reset: 58 Server: APISIX web server ``` @@ -287,6 +291,9 @@ HTTP/1.1 503 Service Temporarily Unavailable Content-Type: text/html Content-Length: 194 Connection: keep-alive +X-RateLimit-Limit: 2 +X-RateLimit-Remaining: 0 +X-RateLimit-Reset: 58 Server: APISIX web server {"error_msg":"Requests are too frequent, please try again later."} diff --git a/docs/zh/latest/plugins/limit-count.md b/docs/zh/latest/plugins/limit-count.md index 3055d22677a6..ffcc3bf6608b 100644 --- a/docs/zh/latest/plugins/limit-count.md +++ b/docs/zh/latest/plugins/limit-count.md @@ -253,7 +253,7 @@ curl -i http://127.0.0.1:9180/apisix/admin/routes/1 \ curl -i http://127.0.0.1:9080/index.html ``` -在执行测试命令的前两次都会正常访问。其中响应头中包含了 `X-RateLimit-Limit` 和 `X-RateLimit-Remaining` 字段,分别代表限制的总请求数和剩余还可以发送的请求数: +在执行测试命令的前两次都会正常访问。其中响应头中包含了 `X-RateLimit-Limit` 和 `X-RateLimit-Remaining` 和 `X-RateLimit-Reset` 字段,分别代表限制的总请求数和剩余还可以发送的请求数以及计数器剩余重置的秒数: ```shell HTTP/1.1 200 OK @@ -262,16 +262,20 @@ Content-Length: 13175 Connection: keep-alive X-RateLimit-Limit: 2 X-RateLimit-Remaining: 0 +X-RateLimit-Reset: 58 Server: APISIX web server ``` -当第三次进行测试访问时,会收到包含 `503` HTTP 状态码的响应头,表示插件生效: +当第三次进行测试访问时,会收到包含 `503` HTTP 状态码的响应头,目前在拒绝的情况下,也会返回相关的头,表示插件生效: ```shell HTTP/1.1 503 Service Temporarily Unavailable Content-Type: text/html Content-Length: 194 Connection: keep-alive +X-RateLimit-Limit: 2 +X-RateLimit-Remaining: 0 +X-RateLimit-Reset: 58 Server: APISIX web server ``` @@ -282,6 +286,9 @@ HTTP/1.1 503 Service Temporarily Unavailable Content-Type: text/html Content-Length: 194 Connection: keep-alive +X-RateLimit-Limit: 2 +X-RateLimit-Remaining: 0 +X-RateLimit-Reset: 58 Server: APISIX web server {"error_msg":"Requests are too frequent, please try again later."} diff --git a/t/APISIX.pm b/t/APISIX.pm index 22e143ba2244..fe534b20c73b 100644 --- a/t/APISIX.pm +++ b/t/APISIX.pm @@ -533,6 +533,7 @@ _EOC_ lua_shared_dict plugin-limit-req 10m; lua_shared_dict plugin-limit-count 10m; + lua_shared_dict plugin-limit-count-reset-header 10m; lua_shared_dict plugin-limit-conn 10m; lua_shared_dict internal-status 10m; lua_shared_dict upstream-healthcheck 32m; diff --git a/t/plugin/limit-count-redis-cluster2.t b/t/plugin/limit-count-redis-cluster2.t new file mode 100644 index 000000000000..d5363c016d8b --- /dev/null +++ b/t/plugin/limit-count-redis-cluster2.t @@ -0,0 +1,89 @@ +# +# 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'; + +repeat_each(1); +no_long_string(); +no_shuffle(); +no_root_location(); + +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: update route, use new limit configuration +--- 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": "/hello2", + "plugins": { + "limit-count": { + "count": 1, + "time_window": 60, + "key": "remote_addr", + "policy": "redis-cluster", + "redis_cluster_nodes": [ + "127.0.0.1:5000", + "127.0.0.1:5001" + ], + "redis_cluster_name": "redis-cluster-1" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + local old_X_RateLimit_Reset = 61 + for i = 1, 3 do + local _, _, headers = t('/hello2', ngx.HTTP_GET) + ngx.sleep(1) + if tonumber(headers["X-RateLimit-Reset"]) < old_X_RateLimit_Reset then + old_X_RateLimit_Reset = tonumber(headers["X-RateLimit-Reset"]) + ngx.say("OK") + else + ngx.say("WRONG") + end + end + ngx.say("Done") + } + } +--- response_body +OK +OK +OK +Done diff --git a/t/plugin/limit-count-redis3.t b/t/plugin/limit-count-redis3.t new file mode 100644 index 000000000000..781f527206c0 --- /dev/null +++ b/t/plugin/limit-count-redis3.t @@ -0,0 +1,188 @@ +# +# 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(); + +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: set route, counter will be shared +--- 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-count": { + "count": 1, + "time_window": 60, + "policy": "redis", + "redis_host": "127.0.0.1", + "redis_port": 6379, + "redis_database": 1, + "redis_timeout": 1001 + } + }, + "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: test X-RateLimit-Reset second number could be decline +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local old_X_RateLimit_Reset = 61 + for i = 1, 2 do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + ngx.sleep(2) + if tonumber(res.headers["X-RateLimit-Reset"]) < old_X_RateLimit_Reset then + old_X_RateLimit_Reset = tonumber(res.headers["X-RateLimit-Reset"]) + ngx.say("OK") + else + ngx.say("WRONG") + end + end + ngx.say("Done") + } + } +--- response_body +OK +OK +Done + + + +=== TEST 3: set router +--- 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-count": { + "count": 2, + "time_window": 10, + "policy": "redis", + "redis_host": "127.0.0.1", + "redis_port": 6379, + "redis_database": 1, + "redis_timeout": 1001 + } + }, + "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 4: test header X-RateLimit-Remaining exist when limit rejected +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local ress = {} + for i = 1, 3 do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + ngx.sleep(1) + table.insert(ress, res.headers["X-RateLimit-Remaining"]) + + end + ngx.say(json.encode(ress)) + } + } +--- response_body +["1","0","0"] diff --git a/t/plugin/limit-count4.t b/t/plugin/limit-count4.t new file mode 100644 index 000000000000..bcefe5156fd0 --- /dev/null +++ b/t/plugin/limit-count4.t @@ -0,0 +1,173 @@ +# +# 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'; + +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: set route, counter will be shared +--- 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-count": { + "count": 1, + "time_window": 60 + } + }, + "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: test X-RateLimit-Reset second number could be decline +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local old_X_RateLimit_Reset = 61 + for i = 1, 2 do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + ngx.sleep(2) + if tonumber(res.headers["X-RateLimit-Reset"]) < old_X_RateLimit_Reset then + old_X_RateLimit_Reset = tonumber(res.headers["X-RateLimit-Reset"]) + ngx.say("OK") + else + ngx.say("WRONG") + end + end + ngx.say("Done") + } + } +--- response_body +OK +OK +Done + + + +=== TEST 3: set route, counter will be shared +--- 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-count": { + "count": 2, + "time_window": 60 + } + }, + "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 4: test header X-RateLimit-Remaining exist when limit rejected +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local ress = {} + for i = 1, 3 do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + ngx.sleep(1) + table.insert(ress, res.headers["X-RateLimit-Remaining"]) + + end + ngx.say(json.encode(ress)) + } + } +--- response_body +["1","0","0"]