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

Reply via email to