diff --git a/apisix/plugins/proxy-rewrite.lua b/apisix/plugins/proxy-rewrite.lua index c1609c612dfa..2f6a3922b751 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 @@ -369,22 +400,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 d23ce9230320..6a0052480166 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 match URI against and value being the new Upstream URI path. For example, with `["^/iresty/(. *)/(. *)", "/$1-$2", ^/theothers/*", "/theothers"]`, if a request is originally sent to `/iresty/hello/world`, the Plugin will rewrite the Upstream URI path to `/iresty/hello-world`; if a request is originally sent to `/theothers/hello/world`, the Plugin will rewrite the Upstream URI path to `/theothers`. | | 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, resulting in multiple headers with the same name. | +| 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 values (multiple headers with the same name). Should not be used to set `Host`. | | 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 42047dbd7bc3..f8229e07803c 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 d933478cdc6f..32b966e24a3e 100644 --- a/t/lib/server.lua +++ b/t/lib/server.lua @@ -1272,4 +1272,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 276dd02ed1ea..23f6f8e2a240 100644 --- a/t/plugin/proxy-rewrite.t +++ b/t/plugin/proxy-rewrite.t @@ -1231,3 +1231,209 @@ GET /hello uri: /uri host: test.com:6443 x-real-ip: 127.0.0.1 + + + +=== TEST 45: 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 46: 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/ + + + +=== TEST 47: 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 48: 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 49: 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 50: 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 51: 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 52: 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/