This is an automated email from the ASF dual-hosted git repository. AlinsRan pushed a commit to branch perf/cache-json-request-body in repository https://gitbox.apache.org/repos/asf/apisix.git
commit 3105a555b62aa6d04bcb93f2657fae2c2335820b Author: AlinsRan <[email protected]> AuthorDate: Fri May 15 13:05:04 2026 +0800 perf(core): cache parsed JSON request body to avoid redundant decoding `get_json_request_body_table()` is called on every request by plugins that need to inspect or modify the JSON request body (e.g. AI proxy, body transformations). Each call previously triggered a full `json.decode()`, even when the body had not changed. This commit adds a per-request cache in `ctx._request_body_tab`. On the first call the body is decoded and stored; subsequent calls within the same request return the cached table directly, skipping redundant decoding. The existing `set_body_data` patch in `patch.lua` is extended to also clear `_request_body_tab`, so any plugin that rewrites the body will cause the next call to re-decode from the new content. Performance impact (single APISIX worker, 1 MB JSON body): - Before: ~70 RPS (4 redundant decodes per request) - After: ~180 RPS (1 decode per request) Co-authored-by: Copilot <[email protected]> --- apisix/core/request.lua | 8 ++++++++ apisix/patch.lua | 27 +++++++++++++------------ t/core/request.t | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 12 deletions(-) diff --git a/apisix/core/request.lua b/apisix/core/request.lua index f49f648ab..ca74dd23d 100644 --- a/apisix/core/request.lua +++ b/apisix/core/request.lua @@ -336,6 +336,11 @@ end function _M.get_json_request_body_table() + local ctx = ngx.ctx.api_ctx + if ctx and ctx._request_body_tab then + return ctx._request_body_tab + end + local body, err = _M.get_body() if not body then return nil, { message = "could not get body: " .. (err or "request body is empty") } @@ -346,6 +351,9 @@ function _M.get_json_request_body_table() return nil, { message = "could not parse JSON request body: " .. (err or "invalid JSON") } end + if ctx then + ctx._request_body_tab = body_tab + end return body_tab end diff --git a/apisix/patch.lua b/apisix/patch.lua index 1e2303c64..3bebba6a7 100644 --- a/apisix/patch.lua +++ b/apisix/patch.lua @@ -386,20 +386,23 @@ function _M.patch() 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 + if api_ctx then + api_ctx._request_body_tab = nil + if 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 - end - for i = 1, #keys_to_clear do - cache[keys_to_clear[i]] = nil + for i = 1, #keys_to_clear do + cache[keys_to_clear[i]] = nil + end end end end diff --git a/t/core/request.t b/t/core/request.t index 322050830..14c2e901e 100644 --- a/t/core/request.t +++ b/t/core/request.t @@ -490,3 +490,55 @@ DEPRECATED: use add_header(ctx, header_name, header_value) instead ngx test apisix + + + +=== TEST 17: get_json_request_body_table caches result and re-decodes after set_body_data +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local json = require("apisix.core.json") + + ngx.ctx.api_ctx = {} + + local decode_count = 0 + local orig_decode = json.decode + json.decode = function(str) + decode_count = decode_count + 1 + return orig_decode(str) + end + + -- first call: populates cache + local t1 = core.request.get_json_request_body_table() + -- second and third calls: hit cache, no extra decode + local t2 = core.request.get_json_request_body_table() + local t3 = core.request.get_json_request_body_table() + + ngx.say("model: ", t1 and t1.model) + ngx.say("same table: ", t1 == t2 and t2 == t3) + ngx.say("decode_count: ", decode_count) + + -- invalidate cache by replacing body + ngx.req.set_body_data('{"model":"claude"}') + + -- cache cleared, must re-decode + local t4 = core.request.get_json_request_body_table() + + json.decode = orig_decode + + ngx.say("after set_body model: ", t4 and t4.model) + ngx.say("decode_count: ", decode_count) + } + } +--- request +POST /t +{"model":"gpt-4","messages":[{"role":"user","content":"hi"}]} +--- more_headers +Content-Type: application/json +--- response_body +model: gpt-4 +same table: true +decode_count: 1 +after set_body model: claude +decode_count: 2
