Skip to content

Body Buffering

Interceptors that access the body (on_request phase 2 and on_response_body) require the gateway to buffer the entire body before calling your code. This page covers how buffering works, how body content types are represented in JavaScript, and the edge cases to be aware of.

Body buffering is automatic and conditional — it only occurs when interceptors need it:

  • Request buffering: triggered when on_request interceptors are configured on an operation
  • Response buffering: triggered when on_response_body interceptors are configured, plus requires buffer-response: true on the upstream config

Without these interceptors, bodies stream through the gateway without being buffered.

When on_request interceptors are configured, the request body buffering flow is:

  1. Each incoming chunk is copied into an internal buffer
  2. If the request timeout fires during buffering, the gateway returns a 504
  3. At end-of-stream, the full buffered body is passed to each on_request interceptor in sequence
  4. Each interceptor’s returned body becomes the next interceptor’s input — interceptors can transform the body via chaining
  5. The final body is forwarded to the upstream
x-plenum-interceptor:
- module: "./interceptors/validate.js"
hook: on_request
function: validateBody
- module: "./interceptors/enrich.js"
hook: on_request
function: addTimestamp
// interceptors/validate.js — sees raw body from client
exports.validateBody = function validateBody(request) {
if (!request.body) {
return { action: "respond", status: 400, body: { error: "body required" } };
}
return { action: "continue" };
};
// interceptors/enrich.js — sees body after validate (unchanged since validate returned no body)
exports.addTimestamp = function addTimestamp(request) {
request.body.receivedAt = new Date().toISOString();
return { action: "continue", body: request.body };
};

The body is buffered once and consumed once through the interceptor chain. There is no way to re-read the original body after an upstream interceptor has modified it.

Response body buffering requires explicit upstream configuration. The gateway refuses to start at boot time if on_response_body interceptors are configured without buffer-response: true.

upstreams:
- name: api
url: https://api.example.com
buffer-response: true # required
actions:
- target: "$.paths['/users'].get"
update:
x-plenum-interceptor:
- module: "./interceptors/redact.js"
hook: on_response_body
function: redactPII

Response buffering flow:

  1. Content-Length is stripped from upstream response headers (the body may change size after interception)
  2. accept-encoding: identity is forced on the upstream request — the gateway receives raw, uncompressed bytes
  3. Each response chunk is copied into an internal buffer
  4. At end-of-stream, interceptors run synchronously on the thread (blocking other async tasks until complete)
  5. The final body is streamed back to the client with Transfer-Encoding: chunked

Response body interceptors should be fast. They run synchronously (block_in_place), blocking the worker thread until complete. Long-running operations (network calls, disk reads) in response body interceptors will stall the entire gateway process.

How the body appears in your JavaScript depends on the Content-Type header:

Content-TypeJS typebodyEncodingNotes
application/json (valid)ObjectabsentParsed JSON object
application/json (malformed)StringabsentFalls back to text — no error thrown
text/*, application/xml, application/x-www-form-urlencodedStringabsentRaw text
Everything else (binary)String (base64)"base64"Must decode with Buffer.from
// Interceptor handles all body types
exports.inspect = function inspect(request) {
if (request.bodyEncoding === "base64") {
// Binary body — use Buffer to decode
const buffer = Buffer.from(request.body, "base64");
console.log(`Binary body: ${buffer.length} bytes`);
} else if (typeof request.body === "object") {
console.log(`JSON body: keys = ${Object.keys(request.body)}`);
} else {
console.log(`Text body: ${request.body.length} chars`);
}
return { action: "continue" };
};

Binary payloads (images, protobuf, file uploads) arrive as a base64-encoded string with bodyEncoding: "base64". To work with binary data, decode and re-encode:

interceptors/process-image.js
exports.optimiseImage = function optimiseImage(request) {
if (request.bodyEncoding !== "base64") {
return { action: "continue" };
}
// Decode base64 → bytes
const imageBuffer = Buffer.from(request.body, "base64");
// Process binary data (e.g., resize, compress)
const optimised = sharp(imageBuffer).resize(800).jpeg().toBuffer();
// Re-encode bytes → base64 for return
const base64 = optimised.toString("base64");
return {
action: "continue",
body: base64,
headers: { "content-type": "image/jpeg" },
};
};

Returned bodies are always treated as JSON at the gateway level. For binary, return the base64 string — the gateway forwards the content to the client as-is. Make sure to set the correct content-type header on your return value.

If an incoming application/json body fails to parse (invalid syntax, truncated payload), it is not treated as an error. Instead, the raw text is provided as a string. This allows validation interceptors to catch and return meaningful 400 responses:

interceptors/validate-json.js
exports.validateJson = function validateJson(request) {
if (request.headers["content-type"]?.startsWith("application/json") && typeof request.body === "string") {
// Malformed JSON — caught here, not by the gateway
return {
action: "respond",
status: 400,
body: { error: "Invalid JSON", raw: request.body },
};
}
return { action: "continue" };
};

When body-modifying interceptors are configured, the gateway automatically adjusts upstream request headers:

ConditionHeader changeReason
on_request interceptors configuredContent-Length stripped, Transfer-Encoding: chunked setBody may change size after interception
on_response_body interceptors configuredaccept-encoding: identity forcedGateway needs raw bytes to buffer and parse

These mutations happen during the before_upstream phase and are automatic — you don’t need to configure them.

on_response_body interceptors are skipped for HEAD requests. HEAD responses have no body to buffer, so the hook cannot fire. If you need HEAD-specific logic, use on_response instead.

  • Latency: buffering means the entire body must arrive before any processing begins. For large uploads or slow upstreams, this adds end-to-end latency.
  • Memory: the full body is held in memory. Large payloads (file uploads, multi-MB JSON responses) increase memory pressure and risk out-of-memory conditions.
  • Single consumption: the body buffer is consumed once through the interceptor chain. If no interceptor returns a body, the original data is forwarded; if any interceptor returns a body, it replaces the original. Later interceptors see the transformed version.
  • Prefer lightweight hooks: if you only need headers, use on_request_headers or on_response — they don’t trigger buffering and are faster.
  • Scope buffer-response: only enable buffer-response: true on upstreams that have on_response_body interceptors. Enabling it globally buffers every response regardless of need.