Skip to content

Commit 3105a55

Browse files
AlinsRanCopilot
andcommitted
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 <223556219+Copilot@users.noreply.github.com>
1 parent c7b5618 commit 3105a55

3 files changed

Lines changed: 75 additions & 12 deletions

File tree

apisix/core/request.lua

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,11 @@ end
336336

337337

338338
function _M.get_json_request_body_table()
339+
local ctx = ngx.ctx.api_ctx
340+
if ctx and ctx._request_body_tab then
341+
return ctx._request_body_tab
342+
end
343+
339344
local body, err = _M.get_body()
340345
if not body then
341346
return nil, { message = "could not get body: " .. (err or "request body is empty") }
@@ -346,6 +351,9 @@ function _M.get_json_request_body_table()
346351
return nil, { message = "could not parse JSON request body: " .. (err or "invalid JSON") }
347352
end
348353

354+
if ctx then
355+
ctx._request_body_tab = body_tab
356+
end
349357
return body_tab
350358
end
351359

apisix/patch.lua

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -386,20 +386,23 @@ function _M.patch()
386386
local _orig_set_body_data = ngx.req.set_body_data
387387
ngx.req.set_body_data = function(data)
388388
local api_ctx = ngx.ctx.api_ctx
389-
if api_ctx and api_ctx._post_arg_request_body then
390-
api_ctx._post_arg_request_body = nil
391-
local var = api_ctx.var
392-
local cache = var and var._cache
393-
if cache then
394-
local keys_to_clear = {}
395-
for key in pairs(cache) do
396-
if type(key) == "string" and key:sub(1, 9) == "post_arg." then
397-
keys_to_clear[#keys_to_clear + 1] = key
389+
if api_ctx then
390+
api_ctx._request_body_tab = nil
391+
if api_ctx._post_arg_request_body then
392+
api_ctx._post_arg_request_body = nil
393+
local var = api_ctx.var
394+
local cache = var and var._cache
395+
if cache then
396+
local keys_to_clear = {}
397+
for key in pairs(cache) do
398+
if type(key) == "string" and key:sub(1, 9) == "post_arg." then
399+
keys_to_clear[#keys_to_clear + 1] = key
400+
end
398401
end
399-
end
400402

401-
for i = 1, #keys_to_clear do
402-
cache[keys_to_clear[i]] = nil
403+
for i = 1, #keys_to_clear do
404+
cache[keys_to_clear[i]] = nil
405+
end
403406
end
404407
end
405408
end

t/core/request.t

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,3 +490,55 @@ DEPRECATED: use add_header(ctx, header_name, header_value) instead
490490
ngx
491491
test
492492
apisix
493+
494+
495+
496+
=== TEST 17: get_json_request_body_table caches result and re-decodes after set_body_data
497+
--- config
498+
location /t {
499+
content_by_lua_block {
500+
local core = require("apisix.core")
501+
local json = require("apisix.core.json")
502+
503+
ngx.ctx.api_ctx = {}
504+
505+
local decode_count = 0
506+
local orig_decode = json.decode
507+
json.decode = function(str)
508+
decode_count = decode_count + 1
509+
return orig_decode(str)
510+
end
511+
512+
-- first call: populates cache
513+
local t1 = core.request.get_json_request_body_table()
514+
-- second and third calls: hit cache, no extra decode
515+
local t2 = core.request.get_json_request_body_table()
516+
local t3 = core.request.get_json_request_body_table()
517+
518+
ngx.say("model: ", t1 and t1.model)
519+
ngx.say("same table: ", t1 == t2 and t2 == t3)
520+
ngx.say("decode_count: ", decode_count)
521+
522+
-- invalidate cache by replacing body
523+
ngx.req.set_body_data('{"model":"claude"}')
524+
525+
-- cache cleared, must re-decode
526+
local t4 = core.request.get_json_request_body_table()
527+
528+
json.decode = orig_decode
529+
530+
ngx.say("after set_body model: ", t4 and t4.model)
531+
ngx.say("decode_count: ", decode_count)
532+
}
533+
}
534+
--- request
535+
POST /t
536+
{"model":"gpt-4","messages":[{"role":"user","content":"hi"}]}
537+
--- more_headers
538+
Content-Type: application/json
539+
--- response_body
540+
model: gpt-4
541+
same table: true
542+
decode_count: 1
543+
after set_body model: claude
544+
decode_count: 2

0 commit comments

Comments
 (0)