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 82fcf548f feat(proxy-rewrite): support multiple same-name headers in
headers.add/set (#13597)
82fcf548f is described below
commit 82fcf548fa3c283e81ed8517e7eec70325902122
Author: AlinsRan <[email protected]>
AuthorDate: Wed Jul 22 14:47:14 2026 +0800
feat(proxy-rewrite): support multiple same-name headers in headers.add/set
(#13597)
---
apisix/plugins/proxy-rewrite.lua | 84 +++++++++++--
docs/en/latest/plugins/proxy-rewrite.md | 38 +++++-
docs/zh/latest/plugins/proxy-rewrite.md | 38 +++++-
t/lib/server.lua | 28 +++++
t/plugin/proxy-rewrite.t | 208 ++++++++++++++++++++++++++++++++
5 files changed, 380 insertions(+), 16 deletions(-)
diff --git a/apisix/plugins/proxy-rewrite.lua b/apisix/plugins/proxy-rewrite.lua
index 6bddfa39a..f6b74e50e 100644
--- a/apisix/plugins/proxy-rewrite.lua
+++ b/apisix/plugins/proxy-rewrite.lua
@@ -93,7 +93,19 @@ local schema = {
["^[^:]+$"] = {
oneOf = {
{ type = "string" },
- { type = "number" }
+ { type = "number" },
+ {
+ -- multiple values for the same
+ -- header name, e.g. ["v1", "v2"]
+ type = "array",
+ minItems = 1,
+ items = {
+ oneOf = {
+ { type = "string" },
+ { type = "number" },
+ }
+ }
+ }
}
}
},
@@ -106,6 +118,18 @@ local schema = {
oneOf = {
{ type = "string" },
{ type = "number" },
+ {
+ -- replace the header with multiple
+ -- values, e.g. ["v1", "v2"]
+ type = "array",
+ minItems = 1,
+ items = {
+ oneOf = {
+ { type = "string" },
+ { type = "number" },
+ }
+ }
+ }
}
}
},
@@ -275,6 +299,13 @@ do
end
+ 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)
+ end
+
+
function _M.rewrite(conf, ctx)
for _, name in ipairs(upstream_names) do
if conf[name] then
@@ -384,22 +415,51 @@ function _M.rewrite(conf, ctx)
local field_cnt = #hdr_op.add
for i = 1, field_cnt, 2 do
- local val = core.utils.resolve_var_with_captures(hdr_op.add[i + 1],
-
ctx.proxy_rewrite_regex_uri_captures)
- val = core.utils.resolve_var(val, ctx.var)
- -- A nil or empty table value will cause add_header function to
throw an error.
- if val then
- local header = hdr_op.add[i]
- core.request.add_header(ctx, header, val)
+ local header = hdr_op.add[i]
+ local value = hdr_op.add[i + 1]
+ -- an array value adds the header once per element (multiple
+ -- headers with the same name); a scalar adds it once.
+ if type(value) == "table" then
+ for j = 1, #value do
+ local val = resolve_header_value(value[j], ctx)
+ -- guard nil only: add_header throws on nil, while an empty
+ -- string is a valid value kept to preserve the existing
+ -- behavior for an unresolved variable/capture.
+ if val then
+ core.request.add_header(ctx, header, val)
+ end
+ end
+ else
+ local val = resolve_header_value(value, ctx)
+ if val then
+ core.request.add_header(ctx, header, val)
+ end
end
end
local field_cnt = #hdr_op.set
for i = 1, field_cnt, 2 do
- local val = core.utils.resolve_var_with_captures(hdr_op.set[i + 1],
-
ctx.proxy_rewrite_regex_uri_captures)
- val = core.utils.resolve_var(val, ctx.var)
- core.request.set_header(ctx, hdr_op.set[i], val)
+ local header = hdr_op.set[i]
+ local value = hdr_op.set[i + 1]
+ -- an array value replaces the header with multiple values in a
+ -- single set; a scalar sets a single value.
+ if type(value) == "table" then
+ local vals = {}
+ local n = 0
+ for j = 1, #value do
+ local val = resolve_header_value(value[j], ctx)
+ if val then
+ n = n + 1
+ vals[n] = val
+ end
+ end
+ if n > 0 then
+ core.request.set_header(ctx, header, vals)
+ end
+ else
+ local val = resolve_header_value(value, ctx)
+ core.request.set_header(ctx, header, val)
+ end
end
local field_cnt = #hdr_op.remove
diff --git a/docs/en/latest/plugins/proxy-rewrite.md
b/docs/en/latest/plugins/proxy-rewrite.md
index d23ce9230..6a0052480 100644
--- a/docs/en/latest/plugins/proxy-rewrite.md
+++ b/docs/en/latest/plugins/proxy-rewrite.md
@@ -45,8 +45,8 @@ The `proxy-rewrite` Plugin offers options to rewrite requests
that APISIX forwar
| regex_uri | array[string] | False | |
| Regular expressions used to
match the URI path from client requests and compose a new Upstream URI path.
When both `uri` and `regex_uri` are configured, `uri` has a higher priority.
The array should contain one or more **key-value pairs**, with the key being
the regular expression to m [...]
| host | string | False | |
| Set
[`Host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host)
request header.
[...]
| headers | object | False | |
| Header actions to be executed. Can
be set to objects of action verbs `add`, `remove`, and/or `set`; or an object
consisting of headers to be `set`. When multiple action verbs are configured,
actions are executed in the order of `add`, `remove`, and `set`.
|
-| headers.add | object | False | | | Headers
to append to requests. If a header already present in the request, the header
value will be appended. Header value could be set to a constant, one or more
[NGINX variables](https://nginx.org/en/docs/http/ngx_http_core_module.html), or
the matched result of `regex_uri` using variables such as `$1-$2-$3`.
|
-| headers.set | object | False | | | Headers
to set to requests. If a header already present in the request, the header
value will be overwritten. Header value could be set to a constant, one or more
[NGINX variables](https://nginx.org/en/docs/http/ngx_http_core_module.html), or
the matched result of `regex_uri` using variables such as `$1-$2-$3`. Should
not be used to set `Host`.
[...]
+| headers.add | object | False | | | Headers
to append to requests. If a header already present in the request, the header
value will be appended. Header value could be set to a constant, one or more
[NGINX variables](https://nginx.org/en/docs/http/ngx_http_core_module.html), or
the matched result of `regex_uri` using variables such as `$1-$2-$3`. A value
could also be an array of such values (e.g. `["val1", "val2"]`) to append the
header multiple times, [...]
+| headers.set | object | False | | | Headers
to set to requests. If a header already present in the request, the header
value will be overwritten. Header value could be set to a constant, one or more
[NGINX variables](https://nginx.org/en/docs/http/ngx_http_core_module.html), or
the matched result of `regex_uri` using variables such as `$1-$2-$3`. A value
could also be an array of such values (e.g. `["val1", "val2"]`) to replace the
header with multiple va [...]
| headers.remove | array[string] | False | | |
Headers to remove from requests.
| use_real_request_uri_unsafe | boolean | False | false |
| If true, bypass URI
normalization and allow for the full original request URI. Enabling this option
is considered unsafe. |
@@ -227,6 +227,40 @@ You should see a response similar to the following:
Note that both headers present and the header value of `X-Api-Version`
configured in the Plugin is appended by the header value passed in the request.
+### Set or Append Multiple Values for the Same Header
+
+Both `set` and `add` accept an array value to produce multiple headers with
the same name. Use `set` to replace any incoming header with the listed values,
or `add` to append them to the existing ones.
+
+The following example sets two `X-Api-Version` headers on the upstream request:
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "id": "proxy-rewrite-route",
+ "methods": ["GET"],
+ "uri": "/",
+ "plugins": {
+ "proxy-rewrite": {
+ "uri": "/headers",
+ "headers": {
+ "set": {
+ "X-Api-Version": ["v1", "v2"]
+ }
+ }
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "httpbin.org:80": 1
+ }
+ }
+ }'
+```
+
+The upstream receives `X-Api-Version: v1` and `X-Api-Version: v2`. Replacing
`set` with `add` keeps any `X-Api-Version` already present in the client
request and appends `v1` and `v2` to it.
+
### Remove Existing Header
The following example demonstrates how you can remove an existing header
`User-Agent`.
diff --git a/docs/zh/latest/plugins/proxy-rewrite.md
b/docs/zh/latest/plugins/proxy-rewrite.md
index 42047dbd7..f8229e078 100644
--- a/docs/zh/latest/plugins/proxy-rewrite.md
+++ b/docs/zh/latest/plugins/proxy-rewrite.md
@@ -45,8 +45,8 @@ description: proxy-rewrite 插件支持重写 APISIX 转发到上游服务的请
| regex_uri | array[string] | 否 | | | 用于匹配客户端请求的 URI 路径并组成新的上游 URI
路径的正则表达式。当同时配置 `uri` 和 `regex_uri` 时,`uri` 具有更高的优先级。该数组应包含一个或多个
**键值对**,其中键是用于匹配 URI 的正则表达式,值是新的上游 URI 路径。例如,对于 `["^/iresty/(. *)/(. *)",
"/$1-$2", ^/theothers/*", "/theothers"]`,如果请求最初发送到 `/iresty/hello/world`,插件会将上游
URI 路径重写为 `/iresty/hello-world`;如果请求最初发送到 `/theothers/hello/world`,插件会将上游 URI
路径重写为 `/theothers`。|
| host | string | 否 | | | 设置
[`Host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) 请求标头。|
| headers | object | 否 | | | 要执行的标头操作。可以设置为动作动词 `add`、`remove` 和/或 `set`
的对象;或由要 `set` 的标头组成的对象。当配置了多个动作动词时,动作将按照“添加”、“删除”和“设置”的顺序执行。|
-| headers.add | object | 否 | | |
要附加到请求的标头。如果请求中已经存在标头,则会附加标头值。标头值可以设置为常量、一个或多个 [NGINX
变量](https://nginx.org/en/docs/http/ngx_http_core_module.html),或者 `regex_uri`
的匹配结果(使用变量,例如 `$1-$2-$3`)。|
-| headers.set | object | 否 | | | 要设置请求的标头。如果请求中已经存在标头,则会覆盖标头值。标头值可以设置为常量、一个或多个
[NGINX 变量](https://nginx.org/en/docs/http/ngx_http_core_module.html),或者
`regex_uri` 的匹配结果(使用变量,例如 `$1-$2-$3`)。不应将其用于设置 `Host`。|
+| headers.add | object | 否 | | |
要附加到请求的标头。如果请求中已经存在标头,则会附加标头值。标头值可以设置为常量、一个或多个 [NGINX
变量](https://nginx.org/en/docs/http/ngx_http_core_module.html),或者 `regex_uri`
的匹配结果(使用变量,例如 `$1-$2-$3`)。标头值也可以是上述值的数组(例如 `["val1",
"val2"]`),从而多次附加该标头,生成多个同名标头。|
+| headers.set | object | 否 | | | 要设置请求的标头。如果请求中已经存在标头,则会覆盖标头值。标头值可以设置为常量、一个或多个
[NGINX 变量](https://nginx.org/en/docs/http/ngx_http_core_module.html),或者
`regex_uri` 的匹配结果(使用变量,例如 `$1-$2-$3`)。标头值也可以是上述值的数组(例如 `["val1",
"val2"]`),从而将该标头替换为多个值(多个同名标头)。不应将其用于设置 `Host`。|
| headers.remove | array[string] | 否 | | | 从请求中删除的标头。
| use_real_request_uri_unsafe | boolean | 否 | false | | 如果为 True,则绕过 URI
规范化并允许完整的原始请求 URI。启用此选项被视为不安全。|
@@ -227,6 +227,40 @@ curl "http://127.0.0.1:9080/" -H '"X-Api-Version": "v2"'
请注意,两个标头均存在,并且插件中配置的 `X-Api-Version` 标头值均附加在请求中传递的标头值上。
+### 为同一标头设置或附加多个值
+
+`set` 和 `add` 都支持数组值,用于生成多个同名标头。使用 `set` 将传入标头替换为列表中的多个值,或使用 `add`
在已有标头之上追加这些值。
+
+以下示例在上游请求上设置两个 `X-Api-Version` 标头:
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "id": "proxy-rewrite-route",
+ "methods": ["GET"],
+ "uri": "/",
+ "plugins": {
+ "proxy-rewrite": {
+ "uri": "/headers",
+ "headers": {
+ "set": {
+ "X-Api-Version": ["v1", "v2"]
+ }
+ }
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "httpbin.org:80": 1
+ }
+ }
+ }'
+```
+
+上游会收到 `X-Api-Version: v1` 和 `X-Api-Version: v2`。将 `set` 替换为 `add` 则保留客户端请求中已有的
`X-Api-Version`,并在其后追加 `v1` 和 `v2`。
+
### 删除现有标头
以下示例演示了如何删除现有标头 `User-Agent`。
diff --git a/t/lib/server.lua b/t/lib/server.lua
index 35322155d..119d64f8b 100644
--- a/t/lib/server.lua
+++ b/t/lib/server.lua
@@ -1299,4 +1299,32 @@ function _M.mock_compressed_upstream_response()
end
+-- echo received request headers, emitting one line per occurrence so that
+-- same-name (multi-value) headers are distinguishable from a single
+-- comma-joined value. A genuine multi-value header arrives as a table from
+-- ngx.req.get_headers() and is printed as repeated "name: value" lines.
+function _M.plugin_proxy_rewrite_multi_header()
+ local headers = ngx.req.get_headers()
+
+ local keys = {}
+ for k in pairs(headers) do
+ if not builtin_hdr_ignore_list[k] then
+ table.insert(keys, k)
+ end
+ end
+ table.sort(keys)
+
+ for _, key in ipairs(keys) do
+ local v = headers[key]
+ if type(v) == "table" then
+ for _, item in ipairs(v) do
+ ngx.say(key, ": ", item)
+ end
+ else
+ ngx.say(key, ": ", v)
+ end
+ end
+end
+
+
return _M
diff --git a/t/plugin/proxy-rewrite.t b/t/plugin/proxy-rewrite.t
index 6b75fbe7c..7ab2abcde 100644
--- a/t/plugin/proxy-rewrite.t
+++ b/t/plugin/proxy-rewrite.t
@@ -1516,3 +1516,211 @@ GET /hello?name=a%20b&x=1 HTTP/1.1
--- response_body
ngx.var.uri: /print_uri_detailed
ngx.var.request_uri: /print_uri_detailed?name=a%20b&x=1
+
+
+
+=== TEST 58: set route(set header with multiple 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,
+ [[{
+ "plugins": {
+ "proxy-rewrite": {
+ "uri": "/plugin_proxy_rewrite_multi_header",
+ "headers": {
+ "set": {
+ "x-multi": ["val1", "val2"]
+ }
+ }
+ }
+ },
+ "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
+
+
+
+=== TEST 59: set replaces the incoming header with multiple same-name headers
+--- request
+GET /hello
+--- more_headers
+x-multi: origin
+--- response_body_like eval
+qr/x-multi: val1\nx-multi: val2/
+--- response_body_unlike eval
+qr/x-multi: origin/
+
+
+
+=== TEST 60: set route(add header with multiple 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,
+ [[{
+ "plugins": {
+ "proxy-rewrite": {
+ "uri": "/plugin_proxy_rewrite_multi_header",
+ "headers": {
+ "add": {
+ "x-multi": ["val1", "val2"]
+ }
+ }
+ }
+ },
+ "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
+
+
+
+=== TEST 61: add appends multiple same-name headers to the incoming header
+--- request
+GET /hello
+--- more_headers
+x-multi: origin
+--- response_body_like eval
+qr/x-multi: origin\nx-multi: val1\nx-multi: val2/
+
+
+
+=== TEST 62: set route(multi-value with regex_uri capture and nginx variable)
+--- 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": {
+ "proxy-rewrite": {
+ "regex_uri": ["^/test/(.*)",
+ "/plugin_proxy_rewrite_multi_header"],
+ "headers": {
+ "set": {
+ "x-multi": ["cap-$1", "$http_x_src"]
+ }
+ }
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/test/*"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 63: each array element resolves capture and variable
+--- request
+GET /test/echo
+--- more_headers
+x-src: from-src
+--- response_body_like eval
+qr/x-multi: cap-echo\nx-multi: from-src/
+
+
+
+=== TEST 64: add route(multi-value with regex_uri capture and nginx variable)
+--- 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": {
+ "proxy-rewrite": {
+ "regex_uri": ["^/test/(.*)",
+ "/plugin_proxy_rewrite_multi_header"],
+ "headers": {
+ "add": {
+ "x-multi": ["cap-$1", "$http_x_src"]
+ }
+ }
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/test/*"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 65: add resolves capture and variable for each array element
+--- request
+GET /test/echo
+--- more_headers
+x-src: from-src
+--- response_body_like eval
+qr/x-multi: cap-echo\nx-multi: from-src/