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:

  1. Full Buffering: Accumulates the entire payload in worker memory before processing. Predictable but memory-intensive.
  2. 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-Length with the new value, OR
  • Strip Content-Length entirely to force Transfer-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_size and proxy_buffer_size to 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-Length exceeds 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:

  1. Auth modules evaluate the mutated payload state, not the raw upstream response.
  2. Rate limiters count against the correct resource boundaries.
  3. 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, and header_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.