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 5f028f3b7 perf(ctx): cache parsed request body to avoid repeated
decode for post_arg.* (#13356)
5f028f3b7 is described below
commit 5f028f3b7898a0c4ed69d6c5cba91195d435b0f7
Author: AlinsRan <[email protected]>
AuthorDate: Wed May 13 12:59:30 2026 +0800
perf(ctx): cache parsed request body to avoid repeated decode for
post_arg.* (#13356)
---
apisix/core/ctx.lua | 20 +++++++-
apisix/patch.lua | 27 +++++++++++
t/core/ctx3.t | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 174 insertions(+), 1 deletion(-)
diff --git a/apisix/core/ctx.lua b/apisix/core/ctx.lua
index 08833d4b5..1f97cb38f 100644
--- a/apisix/core/ctx.lua
+++ b/apisix/core/ctx.lua
@@ -174,7 +174,9 @@ local CONTENT_TYPE_JSON = "application/json"
local CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded"
local CONTENT_TYPE_MULTIPART_FORM = "multipart/form-data"
-local function get_parsed_request_body(ctx)
+local PARSED_BODY_CACHE_KEY = "_post_arg_request_body"
+
+local function _get_parsed_request_body(ctx)
local ct_header = request.header(ctx, "Content-Type") or ""
if core_str.find(ct_header, CONTENT_TYPE_JSON) then
@@ -210,6 +212,22 @@ local function get_parsed_request_body(ctx)
return nil, err
end
+-- Wrapper that caches the parsed body in ctx for the lifetime of the request.
+-- Errors are intentionally not cached: plugins may call
ngx.req.set_body_data()
+-- in a later phase, so a transient read failure should not be frozen.
+local function get_parsed_request_body(ctx)
+ if ctx[PARSED_BODY_CACHE_KEY] ~= nil then
+ log.debug("reuse parsed request body from ctx cache")
+ return ctx[PARSED_BODY_CACHE_KEY]
+ end
+
+ local result, err = _get_parsed_request_body(ctx)
+ if result then
+ ctx[PARSED_BODY_CACHE_KEY] = result
+ end
+ return result, err
+end
+
do
local var_methods = {
diff --git a/apisix/patch.lua b/apisix/patch.lua
index 2b191b2a8..1e2303c64 100644
--- a/apisix/patch.lua
+++ b/apisix/patch.lua
@@ -31,6 +31,7 @@ local new_tab = require("table.new")
local log = ngx.log
local WARN = ngx.WARN
local ipairs = ipairs
+local pairs = pairs
local select = select
local setmetatable = setmetatable
local string = string
@@ -378,6 +379,32 @@ function _M.patch()
ngx_socket.udp = function ()
return patch_udp_socket(original_udp())
end
+
+ -- Patch ngx.req.set_body_data to invalidate the parsed post_arg request
body
+ -- cache in api_ctx. This ensures that post_arg.* variable lookups after a
body
+ -- rewrite always reflect the new body content.
+ local _orig_set_body_data = ngx.req.set_body_data
+ ngx.req.set_body_data = function(data)
+ local api_ctx = ngx.ctx.api_ctx
+ if api_ctx and api_ctx._post_arg_request_body then
+ api_ctx._post_arg_request_body = nil
+ local var = api_ctx.var
+ local cache = var and var._cache
+ if cache then
+ local keys_to_clear = {}
+ for key in pairs(cache) do
+ if type(key) == "string" and key:sub(1, 9) == "post_arg."
then
+ keys_to_clear[#keys_to_clear + 1] = key
+ end
+ end
+
+ for i = 1, #keys_to_clear do
+ cache[keys_to_clear[i]] = nil
+ end
+ end
+ end
+ return _orig_set_body_data(data)
+ end
end
diff --git a/t/core/ctx3.t b/t/core/ctx3.t
index 69f305798..058f91be6 100644
--- a/t/core/ctx3.t
+++ b/t/core/ctx3.t
@@ -99,3 +99,131 @@ qr/serving ctx value from cache for key: graphql_name/
serving ctx value from cache for key: graphql_name
serving ctx value from cache for key: graphql_name
serving ctx value from cache for key: graphql_name
+
+
+
+=== TEST 3: parse post body only once when multiple different post_arg.* keys
are accessed
+--- 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,
+ [=[{
+ "methods": ["POST"],
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "plugins": {
+ "serverless-post-function": {
+ "phase": "rewrite",
+ "functions" : ["return function(conf, ctx)
+ ngx.log(ngx.WARN, 'model: ',
ctx.var['post_arg.model']);
+ ngx.log(ngx.WARN, 'stream: ',
ctx.var['post_arg.stream']);
+ ngx.log(ngx.WARN,
'temperature: ', tostring(ctx.var['post_arg.temperature']));
+ end"]
+ }
+ },
+ "uri": "/hello",
+ "vars": [
+ ["post_arg.model", "==", "gpt-4"]
+ ]
+ }]=]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 4: send request with multiple post_arg fields - body parsed only once
+--- request
+POST /hello
+{"model":"gpt-4","stream":true,"temperature":0.7}
+--- more_headers
+Content-Type: application/json
+--- response_body
+hello world
+--- error_code: 200
+--- error_log
+model: gpt-4
+stream: true
+temperature: 0.7
+--- grep_error_log eval
+qr/reuse parsed request body from ctx cache/
+--- grep_error_log_out
+reuse parsed request body from ctx cache
+reuse parsed request body from ctx cache
+
+
+
+=== TEST 5: set_body_data invalidates both body-level cache and post_arg.*
variable cache
+--- 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,
+ [=[{
+ "methods": ["POST"],
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "plugins": {
+ "serverless-pre-function": {
+ "phase": "rewrite",
+ "functions" : ["return function(conf, ctx)
+ -- first access: parse and
cache body
+ local v1 =
ctx.var['post_arg.model']
+ ngx.log(ngx.WARN, 'before
rewrite model: ', tostring(v1));
+ -- rewrite body: must
invalidate both caches
+
ngx.req.set_body_data('{\"model\":\"claude\",\"temperature\":0.9}')
+ -- same key: must reflect new
body, not stale cache
+ local v2 =
ctx.var['post_arg.model']
+ ngx.log(ngx.WARN, 'after
rewrite model: ', tostring(v2));
+ -- different key: must also
reflect new body
+ local v3 =
ctx.var['post_arg.temperature']
+ ngx.log(ngx.WARN, 'after
rewrite temperature: ', tostring(v3));
+ end"]
+ }
+ },
+ "uri": "/hello"
+ }]=]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 6: send request - set_body_data invalidation is reflected in
post_arg.* access
+--- request
+POST /hello
+{"model":"gpt-4","temperature":0.1}
+--- more_headers
+Content-Type: application/json
+--- response_body
+hello world
+--- error_code: 200
+--- error_log
+before rewrite model: gpt-4
+after rewrite model: claude
+after rewrite temperature: 0.9