This is an automated email from the ASF dual-hosted git repository.
nic-6443 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 8a5e11344 fix(body-transformer): rebuild table in remove_namespace
instead of mutating during traversal (#13522)
8a5e11344 is described below
commit 8a5e113441ab37dfc13ac74feda66a14de54e5b6
Author: Nic <[email protected]>
AuthorDate: Fri Jun 12 10:48:25 2026 +0800
fix(body-transformer): rebuild table in remove_namespace instead of
mutating during traversal (#13522)
---
apisix/plugins/body-transformer.lua | 24 +++--
t/plugin/body-transformer.t | 209 ++++++++++++++++++++++++++++++++++++
2 files changed, 224 insertions(+), 9 deletions(-)
diff --git a/apisix/plugins/body-transformer.lua
b/apisix/plugins/body-transformer.lua
index 9570a067f..b0debcbea 100644
--- a/apisix/plugins/body-transformer.lua
+++ b/apisix/plugins/body-transformer.lua
@@ -79,24 +79,30 @@ local function escape_json(s)
end
+-- Build a new table instead of renaming keys in place: inserting keys into
+-- the table being traversed by pairs() is undefined behavior in Lua, and
+-- can nondeterministically skip keys, leaving some keys not renamed.
local function remove_namespace(tbl)
+ local res = {}
for k, v in pairs(tbl) do
- if type(v) == "table" and next(v) == nil then
- v = ""
- tbl[k] = v
+ if type(v) == "table" then
+ if next(v) == nil then
+ v = ""
+ else
+ v = remove_namespace(v)
+ end
end
+ -- strip the namespace prefix from string keys, e.g. "ns:key" -> "key";
+ -- numeric keys (array part, i.e. repeated XML elements) are kept as is
if type(k) == "string" then
local newk = k:match(".*:(.*)")
if newk then
- tbl[newk] = v
- tbl[k] = nil
- end
- if type(v) == "table" then
- remove_namespace(v)
+ k = newk
end
end
+ res[k] = v
end
- return tbl
+ return res
end
diff --git a/t/plugin/body-transformer.t b/t/plugin/body-transformer.t
index c301eadad..46e93545a 100644
--- a/t/plugin/body-transformer.t
+++ b/t/plugin/body-transformer.t
@@ -1272,3 +1272,212 @@ GET /t
ok
--- no_error_log
[error]
+
+
+
+=== TEST 19: all namespaced keys are renamed (many siblings + repeated
elements)
+--- config
+ location /demo {
+ content_by_lua_block {
+ local fields = {
+ "orderId", "customerName", "customerEmail", "shippingAddress",
+ "billingAddress", "paymentMethod", "totalAmount",
"currencyCode",
+ "orderDate", "deliveryDate", "trackingNumber", "carrierName",
+ "productCount", "discountCode", "taxAmount", "shippingFee",
+ "orderStatus", "lastModified", "createdBy", "approvedBy",
+ "departmentCode", "warehouseId", "priorityLevel", "remarks",
+ }
+ local parts = {}
+ for i, f in ipairs(fields) do
+ parts[i] = string.format("<ns2:%s>v%d</ns2:%s>", f, i, f)
+ end
+ ngx.print(string.format(
+ [[<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns2="http://example.com/order"><soapenv:Body><ns2:getOrderResponse>%s<ns2:items><ns2:item><ns2:sku>first</ns2:sku></ns2:item><ns2:item><ns2:sku>second</ns2:sku></ns2:item></ns2:items><ns2:tags><ns2:tag>red</ns2:tag><ns2:tag>green</ns2:tag><ns2:tag>blue</ns2:tag></ns2:tags></ns2:getOrderResponse></soapenv:Body></soapenv:Envelope>]],
+ table.concat(parts)))
+ }
+ }
+
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin")
+
+ local rsp_template =
ngx.encode_base64[[{*_escape_json(Envelope.Body)*}]]
+
+ local code, body = t.test('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ string.format([[{
+ "uri": "/ws",
+ "plugins": {
+ "proxy-rewrite": {
+ "uri": "/demo"
+ },
+ "body-transformer": {
+ "response": {
+ "input_format": "xml",
+ "template": "%s"
+ }
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:%d": 1
+ }
+ }
+ }]], rsp_template, ngx.var.server_port)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ return
+ end
+ ngx.sleep(0.5)
+
+ local core = require("apisix.core")
+ local http = require("resty.http")
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/ws"
+ local httpc = http.new()
+ local res = httpc:request_uri(uri, {method = "GET"})
+ assert(res.status == 200)
+ local data = core.json.decode(res.body)
+ assert(data and data.getOrderResponse, "Body.getOrderResponse not
found: " .. res.body)
+ local resp = data.getOrderResponse
+
+ local fields = {
+ "orderId", "customerName", "customerEmail", "shippingAddress",
+ "billingAddress", "paymentMethod", "totalAmount",
"currencyCode",
+ "orderDate", "deliveryDate", "trackingNumber", "carrierName",
+ "productCount", "discountCode", "taxAmount", "shippingFee",
+ "orderStatus", "lastModified", "createdBy", "approvedBy",
+ "departmentCode", "warehouseId", "priorityLevel", "remarks",
+ }
+ for i, f in ipairs(fields) do
+ assert(resp[f] == "v" .. i,
+ string.format("field %s not renamed or wrong: %s", f,
tostring(resp[f])))
+ end
+
+ -- repeated complex elements: array preserved and keys inside
+ -- array elements renamed too
+ local items = resp.items and resp.items.item
+ assert(type(items) == "table" and #items == 2
+ and items[1].sku == "first" and items[2].sku == "second",
+ "repeated complex elements broken: " ..
core.json.encode(resp.items))
+
+ -- repeated simple elements: array preserved
+ local tags = resp.tags and resp.tags.tag
+ assert(type(tags) == "table" and #tags == 3
+ and tags[1] == "red" and tags[2] == "green" and tags[3] ==
"blue",
+ "repeated simple elements broken: " ..
core.json.encode(resp.tags))
+
+ -- no key anywhere may keep its namespace prefix
+ local function scan(tbl, path)
+ for k, v in pairs(tbl) do
+ if type(k) == "string" then
+ assert(not k:find(":"),
+ "leftover namespaced key: " .. path .. "." .. k)
+ end
+ if type(v) == "table" then
+ scan(v, path .. "." .. tostring(k))
+ end
+ end
+ end
+ scan(data, "Body")
+
+ ngx.say("passed")
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 20: key renaming must not depend on table layout (varied key sets)
+--- config
+ location /demo {
+ content_by_lua_block {
+ -- echo the request body so each request controls the XML
+ -- the response transformer has to parse
+ ngx.print(require("apisix.core").request.get_body())
+ }
+ }
+
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin")
+
+ local rsp_template =
ngx.encode_base64[[{*_escape_json(Envelope.Body.Resp)*}]]
+
+ local code, body = t.test('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ string.format([[{
+ "uri": "/ws",
+ "plugins": {
+ "proxy-rewrite": {
+ "uri": "/demo"
+ },
+ "body-transformer": {
+ "response": {
+ "input_format": "xml",
+ "template": "%s"
+ }
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:%d": 1
+ }
+ }
+ }]], rsp_template, ngx.var.server_port)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ return
+ end
+ ngx.sleep(0.5)
+
+ local core = require("apisix.core")
+ local http = require("resty.http")
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/ws"
+ local httpc = http.new()
+
+ -- LuaJIT randomizes its string hash seed per process, so a
+ -- traversal-order bug shows up only for some key sets in any
+ -- given process. Sweep many sibling counts and several naming
+ -- schemes to cover many table layouts.
+ local schemes = {
+ function (n, i) return "k" .. i end,
+ function (n, i) return "longFieldName" .. i .. "x" .. n end,
+ function (n, i) return "f" .. i .. string.rep("z", i % 7) end,
+ }
+ for n = 4, 64, 2 do
+ for si, scheme in ipairs(schemes) do
+ local names, parts = {}, {}
+ for i = 1, n do
+ names[i] = scheme(n, i)
+ parts[i] = string.format("<ns:%s>v%d</ns:%s>",
+ names[i], i, names[i])
+ end
+ local xml = string.format(
+ [[<env:Envelope xmlns:env="http://e"
xmlns:ns="http://n"><env:Body><ns:Resp>%s</ns:Resp></env:Body></env:Envelope>]],
+ table.concat(parts))
+ local res = httpc:request_uri(uri, {method = "POST", body
= xml})
+ assert(res.status == 200)
+ local data = core.json.decode(res.body)
+ assert(data, string.format(
+ "n=%d scheme=%d: transform failed, body: %s", n, si,
res.body))
+ for i = 1, n do
+ assert(data[names[i]] == "v" .. i, string.format(
+ "n=%d scheme=%d: key %s lost or not renamed",
+ n, si, names[i]))
+ end
+ end
+ end
+
+ ngx.say("passed")
+ }
+ }
+--- timeout: 30
+--- response_body
+passed