perf(ctx): cache parsed request body to avoid repeated decode for post_arg.*#13356
Open
AlinsRan wants to merge 8 commits into
Open
perf(ctx): cache parsed request body to avoid repeated decode for post_arg.*#13356AlinsRan wants to merge 8 commits into
AlinsRan wants to merge 8 commits into
Conversation
…t_arg.* When multiple post_arg.* variables are evaluated in a single request (e.g. vars expressions in route matching), each access triggers a full body read + json.decode cycle independently. Introduce a two-layer design: _get_parsed_request_body() handles pure parsing, while get_parsed_request_body() wraps it with a per-request ctx cache. Subsequent accesses to different post_arg.* keys reuse the already-decoded table, reducing CPU and memory overhead for large request bodies. Errors are intentionally not cached since plugins may call ngx.req.set_body_data() in later phases.
patch.lua increments ngx.ctx._body_version on each ngx.req.set_body_data call. get_parsed_request_body compares the cached version against the current one to detect staleness automatically. This decouples patch.lua from ctx internals and provides a general mechanism that any future body-dependent cache can reuse. Storing the version in ngx.ctx (not api_ctx) is appropriate since set_body_data is a nginx-level operation.
Body rewrite cache invalidation (ngx.ctx._body_version + patch.lua) is out of scope for this PR. The existing variable-level cache in ctx.__index has the same limitation, so fixing it here inconsistently adds complexity without solving the general problem. Keep the simpler body-level cache in get_parsed_request_body without version tracking.
Patch ngx.req.set_body_data in patch.lua to clear both the body-level cache (api_ctx._post_arg_request_body) and any post_arg.* entries in the variable-level cache (api_ctx._cache) when the request body is rewritten. The check api_ctx._post_arg_request_body ~= nil avoids iterating _cache on requests that never access post_arg.* variables. Also rename the body-level cache key from _parsed_request_body to _post_arg_request_body to better reflect its purpose.
api_ctx._cache does not exist; the variable-level cache lives at api_ctx.var._cache. Without this fix the cache iteration was a no-op and post_arg.* keys remained stale after set_body_data.
Add type(key) == "string" check before key:sub() to avoid errors if non-string keys exist in the variable cache.
…g pairs iteration
membphis
approved these changes
May 13, 2026
nic-6443
approved these changes
May 13, 2026
shreemaan-abhishek
approved these changes
May 13, 2026
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
When multiple
post_arg.*variables are evaluated in a single request(e.g.
varsexpressions in route matching or plugin-levelvarsconditionsin
traffic-split/fault-injection), each access independentlytriggers a full body read +
json.decodecycle. For large requestbodies at high QPS, this causes unnecessary repeated CPU and memory
overhead.
Changes
apisix/core/ctx.luaSplit
get_parsed_request_bodyinto two layers:_get_parsed_request_body(ctx)— pure parsing logic, no side effectsget_parsed_request_body(ctx)— wrapper that caches the decoded table inctx._post_arg_request_bodyfor the lifetime of the requestOn the first access, the decoded table is stored in
ctx._post_arg_request_body.Subsequent accesses to different
post_arg.*keys within the same requestreuse the cached result, so
json.decoderuns at most once per request.Errors are intentionally not cached: a failed parse leaves the cache
empty so that a later access (e.g. after a plugin sets a valid body) can
still succeed.
apisix/patch.luaPatch
ngx.req.set_body_datato automatically invalidate both cache layerswhen the request body is rewritten mid-request:
api_ctx._post_arg_request_body(body-level cache)post_arg.*entries fromapi_ctx.var._cache(variable-level cache)The invalidation is skipped entirely when
_post_arg_request_bodyis nil,avoiding any overhead on requests that never access
post_arg.*variables.Tests
t/core/ctx3.t): verifies all fields are read correctly andthat
reuse parsed request body from ctx cacheappears exactly twice whenthree different
post_arg.*keys are accessed in one request.t/core/ctx3.t): verifies that afterngx.req.set_body_data(),both the same key and a different key reflect the new body content.