perf(core): cache parsed JSON request body to avoid redundant decoding#13377
Open
AlinsRan wants to merge 7 commits into
Open
perf(core): cache parsed JSON request body to avoid redundant decoding#13377AlinsRan wants to merge 7 commits into
AlinsRan wants to merge 7 commits into
Conversation
nic-6443
reviewed
May 15, 2026
membphis
previously approved these changes
May 15, 2026
shreemaan-abhishek
approved these changes
May 15, 2026
shreemaan-abhishek
previously approved these changes
May 15, 2026
3ae1190
3105a55 to
3ae1190
Compare
nic-6443
previously approved these changes
May 15, 2026
3ae1190 to
9bf8392
Compare
f8a25a7 to
3cff6a7
Compare
`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>
3cff6a7 to
3c55f9f
Compare
Replace abstract 'json' string with CONTENT_TYPE_JSON constant as the content_type parameter to get_request_body_table(), so callers and _request_body_type use the same content-type string constants throughout. Also update patch.lua to clear _request_body_type (replacing the old _json_request_body_tab approach) and fix error message pattern in test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lback Use 'local ct = content_type or _M.header(ctx, "Content-Type") or ""' so the caller-supplied content_type takes precedence over the header, removing the separate content_type == CONTENT_TYPE_JSON branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This block was accidentally removed in a previous commit. Restore it to match upstream master behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Restore get_post_args signature to (ctx) with req_get_post_args(0) to match upstream master. Remove extra blank line in get_body. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
get_json_request_body_table()previously calledjson.decode()on every invocation with no caching. In theai-proxyplugin request path, this function is called 3 times per request across different phases:Each decode is redundant: the body bytes have not changed between phases.
Changes
apisix/core/request.luaget_request_body_table(ctx): a unified, content-type-aware body parser that handlesapplication/json,application/x-www-form-urlencoded, andmultipart/form-data. Caches the parsed result inctx._request_body_tabfor the lifetime of the request.get_json_request_body_table()as a thin wrapper: validates Content-Type isapplication/json(returning an error for non-JSON requests to prevent type confusion), then delegates toget_request_body_table().apisix/core/ctx.luaRemove the local
_get_parsed_request_body/get_parsed_request_bodyfunctions and themultipartdependency. Thepost_arg.*variable resolution now callsrequest.get_request_body_table(ctx)directly, sharing the same cache.apisix/patch.luaSimplify the
set_body_datapatch: use_request_body_tabas the single guard. If it is non-nil, clear it and scanvar._cachefor stalepost_arg.*entries. If it is nil, body was never parsed — skip both cleanups entirely.Performance
Benchmarked on a single APISIX worker with a mock upstream (100 ms fixed delay, ~10 KB response):
CPU was the bottleneck in all cases. Gain scales with body size because
json.decodecost grows linearly with input.Test
Added TEST 17 in
t/core/request.t:json.decodewith a call counterngx.req.set_body_data()to replace the body, then verifies the next call re-decodes (counter becomes 2) and returns the new content