Rewriting JSON payloads on the fly
Modifying request or response bodies at the edge requires precise buffer management and strict adherence to HTTP/1.1 and HTTP/2 transport semantics. When implementing Middleware Chains & Request Transformation, engineers must account for synchronous parsing overhead, worker memory allocation, and downstream service contract expectations. This reference details exact configuration syntax, streaming constraints, and critical failure modes encountered when rewriting JSON payloads on the fly.
Execution Context & Gateway Architecture
Dynamic JSON mutation executes within the proxy layer before routing to upstream services or returning to the client. The transformation lifecycle follows a strict sequence: intercept the body stream, deserialize to an in-memory object, apply deterministic mutations, serialize back to bytes, and update transport headers. This process diverges fundamentally from static header injection and requires deliberate integration into your broader Request & Response Transformation strategy.
The gateway must explicitly choose between two execution models:
- Full Buffering: Accumulates the entire payload in worker memory before processing. Predictable but memory-intensive.
- Streaming/Chunked Processing: Applies mutations incrementally as data arrives. Lower memory footprint but requires stateful chunk handling and careful EOF synchronization.
Exact Configuration Syntax by Platform
Envoy (Lua Filter)
Envoy executes Lua filters synchronously within the HTTP filter chain. The envoy_on_response callback provides direct access to the response body buffer.
http_filters:
- name: envoy.filters.http.lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
inline_code: |
function envoy_on_response(response_handle)
local body = response_handle:body()
local json = require("json")
-- Deserialize full buffer
local data = json.decode(body:getBytes(0, body:length()))
-- Apply mutation
data.metadata = { processed_at = os.time() }
local new_body = json.encode(data)
-- Replace buffer and recalculate transport headers
response_handle:body():setBytes(new_body)
response_handle:headers():replace("content-length", tostring(#new_body))
end
Kong (Custom Plugin)
Kong plugins execute in defined phases. The access phase intercepts the request body before upstream routing.
-- plugin.lua
local cjson = require "cjson"
local MyTransformer = {}
function MyTransformer:access(conf)
-- Read and parse request body
local body = kong.request.get_raw_body()
local data = cjson.decode(body)
-- Apply mutation
data._gateway_transform = true
local new_body = cjson.encode(data)
-- Update request body and recalculate Content-Length
kong.service.request.set_raw_body(new_body)
kong.service.request.set_header("Content-Length", tostring(#new_body))
end
MyTransformer.PRIORITY = 1000
MyTransformer.VERSION = "1.0.0"
return MyTransformer
Nginx/OpenResty (body_filter_by_lua)
Nginx processes response bodies in chunks via body_filter_by_lua_block. State must be maintained across chunk invocations until EOF.
location /api/v1/transform {
proxy_pass http://upstream;
body_filter_by_lua_block {
local chunk = ngx.arg[1]
local eof = ngx.arg[2]
-- Accumulate or process only at EOF
if eof then
local json = require "cjson"
local data = json.decode(chunk)
-- Apply mutation
data.transformed = true
ngx.arg[1] = json.encode(data)
-- Force chunked transfer by removing fixed length
ngx.header["Content-Length"] = nil
end
}
}
Critical Failure Modes & Debugging
Content-Length & Chunked Transfer Mismatch
Symptom: Clients receive truncated responses, or upstreams return 400 Bad Request / 413 Payload Too Large.
Root Cause: The original Content-Length header remains valid for the pre-mutation payload size. HTTP/1.1 requires exact byte alignment between header and body.
Resolution:
- Immediately after serialization, calculate the exact byte length of the new payload.
- Explicitly replace
Content-Lengthwith the new value, OR - Strip
Content-Lengthentirely to forceTransfer-Encoding: chunked. Never rely on implicit gateway recalculation.
Streaming Buffer Exhaustion
Symptom: Worker processes OOM-kill, or proxy returns 502 Bad Gateway under load.
Root Cause: Full deserialization of payloads >5MB exceeds proxy_buffer_size or Lua VM heap limits.
Resolution:
- Tune
client_max_body_sizeandproxy_buffer_sizeto match expected payload ceilings. - For high-throughput environments, replace standard parsers with streaming JSON libraries (
yajl,simdjson) that support field-level mutation without full AST construction. - Implement hard size guards: reject or bypass transformation if
Content-Lengthexceeds worker memory thresholds.
Character Encoding & BOM Artifacts
Symptom: Silent JSON parse error or malformed string boundaries despite valid-looking payloads.
Root Cause: UTF-8 Byte Order Marks (EF BB BF) or mismatched charset declarations corrupt the first token of the JSON stream.
Resolution:
- Strip leading BOM bytes before deserialization:
body = body:gsub("^\239\187\191", "")(matches UTF-8 BOM:EF BB BF) - Enforce strict headers post-transformation:
Content-Type: application/json; charset=utf-8 - Validate against strict JSON schemas before serialization to catch encoding drift early.
Integration with Middleware Pipelines
JSON mutation must be isolated within a dedicated transformation stage to prevent state corruption in adjacent middleware. Position the filter strictly after TLS termination and header validation, but before authentication, rate limiting, and caching layers.
This ordering guarantees:
- Auth modules evaluate the mutated payload state, not the raw upstream response.
- Rate limiters count against the correct resource boundaries.
- Cache keys reflect the transformed payload structure, preventing stale or mismatched cache hits.
Performance Benchmarks & Best Practices
- Parser Selection: Use compiled JSON libraries (
cjson,simdjson) over standard Lua/Python parsers. Target sub-millisecond serialization/deserialization latency. - Structural Integrity: Never apply regex-based string replacement to JSON. It violates escaping rules and breaks nested object boundaries.
- Fault Isolation: Wrap transformation logic in circuit breakers. If parsing fails, bypass mutation and pass-through the original payload to prevent cascading proxy errors.
- Observability: Emit structured metrics per request:
bytes_processed,parse_errors,mutation_latency_ms, andheader_updates. Correlate with upstream response codes to detect contract drift.
Properly architected payload rewriting maintains deterministic routing latency while enforcing strict data contracts across distributed systems.