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.
When buffering happens
Section titled “When buffering happens”Body buffering is automatic and conditional — it only occurs when interceptors need it:
- Request buffering: triggered when
on_requestinterceptors are configured on an operation - Response buffering: triggered when
on_response_bodyinterceptors are configured, plus requiresbuffer-response: trueon the upstream config
Without these interceptors, bodies stream through the gateway without being buffered.
Request body buffering
Section titled “Request body buffering”When on_request interceptors are configured, the request body buffering flow is:
- Each incoming chunk is copied into an internal buffer
- If the request timeout fires during buffering, the gateway returns a
504 - At end-of-stream, the full buffered body is passed to each
on_requestinterceptor in sequence - Each interceptor’s returned body becomes the next interceptor’s input — interceptors can transform the body via chaining
- 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 clientexports.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
Section titled “Response body buffering”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: redactPIIResponse buffering flow:
Content-Lengthis stripped from upstream response headers (the body may change size after interception)accept-encoding: identityis forced on the upstream request — the gateway receives raw, uncompressed bytes- Each response chunk is copied into an internal buffer
- At end-of-stream, interceptors run synchronously on the thread (blocking other async tasks until complete)
- 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.
Body types
Section titled “Body types”How the body appears in your JavaScript depends on the Content-Type header:
| Content-Type | JS type | bodyEncoding | Notes |
|---|---|---|---|
application/json (valid) | Object | absent | Parsed JSON object |
application/json (malformed) | String | absent | Falls back to text — no error thrown |
text/*, application/xml, application/x-www-form-urlencoded | String | absent | Raw text |
| Everything else (binary) | String (base64) | "base64" | Must decode with Buffer.from |
// Interceptor handles all body typesexports.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 body handling
Section titled “Binary body handling”Binary payloads (images, protobuf, file uploads) arrive as a base64-encoded string with bodyEncoding: "base64". To work with binary data, decode and re-encode:
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-typeheader on your return value.
Malformed JSON
Section titled “Malformed JSON”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:
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" };};Before-upstream header mutations
Section titled “Before-upstream header mutations”When body-modifying interceptors are configured, the gateway automatically adjusts upstream request headers:
| Condition | Header change | Reason |
|---|---|---|
on_request interceptors configured | Content-Length stripped, Transfer-Encoding: chunked set | Body may change size after interception |
on_response_body interceptors configured | accept-encoding: identity forced | Gateway 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.
HEAD requests
Section titled “HEAD requests”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.
Performance considerations
Section titled “Performance considerations”- 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_headersoron_response— they don’t trigger buffering and are faster. - Scope
buffer-response: only enablebuffer-response: trueon upstreams that haveon_response_bodyinterceptors. Enabling it globally buffers every response regardless of need.