Skip to content

Commit

Permalink
feat(limit-req): support multiple variables as key (#5302)
Browse files Browse the repository at this point in the history
  • Loading branch information
spacewander authored Oct 22, 2021
1 parent 71a2259 commit 2f250d5
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 16 deletions.
33 changes: 24 additions & 9 deletions apisix/plugins/limit-req.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ local schema = {
properties = {
rate = {type = "number", exclusiveMinimum = 0},
burst = {type = "number", minimum = 0},
key = {type = "string",
enum = {"remote_addr", "server_addr", "http_x_real_ip",
"http_x_forwarded_for", "consumer_name"},
key = {type = "string"},
key_type = {type = "string",
enum = {"var", "var_combination"},
default = "var",
},
rejected_code = {
type = "integer", minimum = 200, maximum = 599, default = 503
Expand Down Expand Up @@ -83,17 +84,31 @@ function _M.access(conf, ctx)
return 500
end

local conf_key = conf.key
local key
if conf.key == "consumer_name" then
if not ctx.consumer_name then
core.log.error("consumer not found.")
return 500, { message = "Consumer not found."}
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
key = ctx.consumer_name .. ctx.conf_type .. ctx.conf_version

else
key = (ctx.var[conf.key] or "") .. ctx.conf_type .. ctx.conf_version
key = ctx.var[conf_key]
end

if key == nil then
core.log.info("bypass the limit req as the key is empty")
-- Bypass the limit req when the key is empty.
-- This behavior is the same as Nginx
return
end

key = key .. ctx.conf_type .. ctx.conf_version
core.log.info("limit key: ", key)

local delay, err = lim:incoming(key, true)
Expand Down
3 changes: 2 additions & 1 deletion docs/en/latest/plugins/limit-req.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ limit request rate using the "leaky bucket" method.
| ------------- | ------- | ----------- | ------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| rate | integer | required | | rate > 0 | the specified request rate (number per second) threshold. Requests exceeding this rate (and below `burst`) will get delayed to conform to the rate. |
| burst | integer | required | | burst >= 0 | the number of excessive requests per second allowed to be delayed. Requests exceeding this hard limit will get rejected immediately. |
| key | string | required | | ["remote_addr", "server_addr", "http_x_real_ip", "http_x_forwarded_for", "consumer_name"] | the user specified key to limit the rate, now accept those as key: "remote_addr"(client's IP), "server_addr"(server's IP), "X-Forwarded-For/X-Real-IP" in request header, "consumer_name"(consumer's username). |
| key_type | string | optional | "var" | ["var", "var_combination"] | the type of key. |
| key | string | required | | | the user specified key to limit the rate. If the `key_type` is "var", the key will be treated as a name of variable, like "remote_addr" or "consumer_name". If the `key_type` is "var_combination", the key will be a combination of variables, like "$remote_addr|$consumer_name". |
| rejected_code | integer | optional | 503 | [200,...,599] | The HTTP status code returned when the request exceeds the threshold is rejected. |
| rejected_msg | string | optional | | non-empty | The response body returned when the request exceeds the threshold is rejected. |
| nodelay | boolean | optional | false | | If nodelay flag is true, bursted requests will not get delayed |
Expand Down
5 changes: 3 additions & 2 deletions docs/zh/latest/plugins/limit-req.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ title: limit-req
| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 |
| ------------- | ------- | ------ | ------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| rate | integer | 必须 | | rate > 0 | 指定的请求速率(以秒为单位),请求速率超过 `rate` 但没有超过 (`rate` + `brust`)的请求会被加上延时。 |
| burst | integer | 必须 | | burst >= 0 | t请求速率超过 (`rate` + `brust`)的请求会被直接拒绝。 |
| key | string | 必须 | | ["remote_addr", "server_addr", "http_x_real_ip", "http_x_forwarded_for", "consumer_name"] | 用来做请求计数的依据,当前接受的 key 有:"remote_addr"(客户端IP地址), "server_addr"(服务端 IP 地址), 请求头中的"X-Forwarded-For" 或 "X-Real-IP","consumer_name"(consumer 的 username)。 |
| burst | integer | 必须 | | burst >= 0 | 请求速率超过 (`rate` + `brust`)的请求会被直接拒绝。 |
| key_type | string | 可选 | "var" | ["var", "var_combination"] | key 的类型 |
| key | string | 必须 | | | 用来做请求计数的依据。如果 `key_type` 为 "var",那么 key 会被当作变量名称,如 "remote_addr" 和 "consumer_name"。如果 `key_type` 为 "var_combination",那么 key 会当作变量组合,如 "$remote_addr|$consumer_name"。 |
| rejected_code | integer | 可选 | 503 | [200,...,599] | 当请求超过阈值被拒绝时,返回的 HTTP 状态码。 |
| rejected_msg | string | 可选 | | 非空 | 当请求超过阈值被拒绝时,返回的响应体。 |
| nodelay | boolean | 可选 | false | | 如果 nodelay 为 true, 请求速率超过 `rate` 但没有超过 (`rate` + `brust`)的请求不会加上延迟, 如果是 false,则会加上延迟。 |
Expand Down
2 changes: 1 addition & 1 deletion t/admin/plugins.t
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ GET /apisix/admin/plugins
ngx.HTTP_GET,
nil,
[[
{"properties":{"rate":{"exclusiveMinimum":0,"type":"number"},"burst":{"minimum":0,"type":"number"},"key":{"enum":["remote_addr","server_addr","http_x_real_ip","http_x_forwarded_for","consumer_name"],"type":"string"},"rejected_code":{"type":"integer","default":503,"minimum":200,"maximum":599}},"required":["rate","burst","key"],"type":"object"}
{"type":"object","required":["rate","burst","key"],"properties":{"rate":{"type":"number","exclusiveMinimum":0},"key_type":{"type":"string","enum":["var","var_combination"],"default":"var"},"burst":{"type":"number","minimum":0},"disable":{"type":"boolean"},"nodelay":{"type":"boolean","default":false},"key":{"type":"string"},"rejected_code":{"type":"integer","minimum":200,"maximum":599,"default":503},"rejected_msg":{"type":"string","minLength":1},"allow_degradation":{"type":"boolean","default":false}}}
]]
)
Expand Down
5 changes: 2 additions & 3 deletions t/plugin/limit-req.t
Original file line number Diff line number Diff line change
Expand Up @@ -693,11 +693,10 @@ passed
=== TEST 18: get "consumer_name" is empty
--- request
GET /hello
--- error_code: 500
--- response_body
{"message":"Consumer not found."}
hello world
--- error_log
[error]
bypass the limit req as the key is empty



Expand Down
137 changes: 137 additions & 0 deletions t/plugin/limit-req2.t
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,140 @@ passed
["GET /hello", "GET /hello"]
--- error_code eval
[200, 200]



=== TEST 5: key type is var_combination
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"limit-req": {
"rate": 0.1,
"burst": 0.1,
"rejected_code": 503,
"key": "$http_a $http_b",
"key_type": "var_combination"
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/hello"
}]]
)

if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- request
GET /t
--- response_body
passed
--- no_error_log
[error]



=== TEST 6: exceed the burst
--- 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, 2 do
local httpc = http.new()
local res, err = httpc:request_uri(uri, {headers = {a = 1}})
if not res then
ngx.say(err)
return
end
table.insert(ress, res.status)
end
ngx.say(json.encode(ress))
}
}
--- request
GET /t
--- no_error_log
[error]
--- response_body
[200,503]



=== TEST 7: don't exceed the burst
--- 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, 2 do
local httpc = http.new()
local res, err = httpc:request_uri(uri, {headers = {a = i}})
if not res then
ngx.say(err)
return
end
table.insert(ress, res.status)
end
ngx.say(json.encode(ress))
}
}
--- request
GET /t
--- no_error_log
[error]
--- response_body
[200,200]



=== TEST 8: bypass empty key
--- 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, 2 do
local httpc = http.new()
local res, err = httpc:request_uri(uri)
if not res then
ngx.say(err)
return
end
table.insert(ress, res.status)
end
ngx.say(json.encode(ress))
}
}
--- request
GET /t
--- no_error_log
[error]
--- response_body
[200,200]
--- error_log
bypass the limit req as the key is empty

0 comments on commit 2f250d5

Please sign in to comment.