Skip to content

Lifecycle Hooks

Interceptors fire at seven distinct points in the request/response lifecycle, each with different input data, mutation capabilities, and short-circuit rules. This page covers each hook in detail.

Requests flow through hooks in this order. Each hook runs all interceptors registered for that hook before moving to the next:

1. on_request_headers ── request headers only, can short-circuit
2. on_request ── phase1: headers │ phase2: full body, can short-circuit
3. before_upstream ── final headers before upstream, respond ignored
4. on_response ── response status + headers, respond ignored
5. on_response_body ── full response body, respond ignored
6. on_gateway_error ── triggered by any gateway error (global only)

Fires as soon as the request is received, before the request body is read. This is the lightest hook — ideal for authentication, rate-limiting checks, and header validation where you don’t need the body.

  • Input: method, route, path, headers, query, queryParams, params, operation, ctx, options
  • Body: Not available
  • Rate limits: Not yet evaluated (rate_limits is unavailable)
  • Can short-circuit: Yes (action: "respond")
  • Can modify headers: Yes
interceptors/auth.js
exports.checkApiKey = function checkApiKey(request) {
if (request.headers["x-api-key"] !== "secret") {
return { action: "respond", status: 401, body: { error: "Unauthorized" } };
}
return { action: "continue" };
};

Runs in two phases. Phase 1 executes with headers only (same input as on_request_headers, plus rate_limits is now available). Phase 2 executes after the full request body has been buffered and provides body and bodyEncoding to each interceptor in sequence.

  • Phase 1 input: method, route, path, headers, query, queryParams, params, operation, rate_limits, ctx, options
  • Phase 2 input: same, plus body and bodyEncoding
  • Can short-circuit: Yes (action: "respond") — skips remaining interceptors in this phase
  • Can modify body: Yes (phase 2 only) — each interceptor’s output body becomes the next interceptor’s input
x-plenum-interceptor:
- module: "./interceptors/validate-body.js"
hook: on_request
function: validate
interceptors/validate-body.js
exports.validate = function validate(request) {
if (!request.body?.name) {
return { action: "respond", status: 400, body: { error: "name is required" } };
}
return { action: "continue" };
};

When on_request interceptors are configured, the request body is fully buffered before phase 2. For details on buffering, types, and binary handling, see Body Buffering.

Fires after all on_request interceptors have completed, just before the request is forwarded to the upstream. Use this hook for final header mutations — logging headers, injecting tracing IDs, or signing requests.

  • Input: method, route, path, headers, query, queryParams, params, operation, rate_limits, ctx, options
  • Body: Not available (interceptors only see headers)
  • Can short-circuit: No — action: "respond" is logged and silently ignored. The request is already committed to the upstream.
  • Can modify headers: Yes
interceptors/signer.js
exports.signRequest = function signRequest(request) {
const signature = hmacSha256(request.path, secret);
return {
action: "continue",
headers: { "x-signature": signature },
};
};

The gateway automatically strips Content-Length and sets Transfer-Encoding: chunked before forwarding when on_request interceptors are configured (the body may have changed size). It also forces accept-encoding: identity when on_response_body is configured (to prevent compressed upstream responses). See Body Buffering.

Fires as soon as the upstream response headers are received, before the response body is streamed. Use this to inspect or mutate response status and headers — adding CORS headers, logging status codes, or normalising header casing.

  • Input: status, method, route, headers, operation, rate_limits, ctx, options
  • Body: Not available
  • Can short-circuit: No — action: "respond" is logged and silently ignored. The response is already in flight to the client.
  • Can modify status: Yes
  • Can modify headers: Yes
interceptors/add-headers.js
exports.addHeaders = function addHeaders(response) {
return {
action: "continue",
headers: { "x-response-time": response.ctx.startTime ? Date.now() - response.ctx.startTime : null },
};
};

Fires after the full upstream response body has been buffered. Requires buffer-response: true on the upstream configuration — the gateway refuses to start if this is misconfigured.

  • Input: status, method, route, headers, operation, rate_limits, ctx, options, body, bodyEncoding
  • Can short-circuit: No — action: "respond" is logged and silently ignored.
  • Can modify body: Yes — returned body replaces the response to the client
  • Can modify status/headers: Yes
upstreams:
- name: api
url: https://api.example.com
buffer-response: true # required for on_response_body
x-plenum-interceptor:
- module: "./interceptors/transform-response.js"
hook: on_response_body
function: transform
interceptors/transform-response.js
exports.transform = function transform(response) {
const data = response.body;
data.enrichedAt = new Date().toISOString();
return {
action: "continue",
body: data,
};
};

Response body interceptors run synchronously (blocking the thread) and must buffer the entire response. For details, see Body Buffering.

A global error handler configured in x-plenum-config — not per-operation like other hooks. Fires for all gateway-originated errors: 404 (no matching route), 405 (method not allowed), 413 (body too large), 502 (upstream connection failed), 504 (request timeout), and interceptor errors.

  • Input: status, method, route, path, headers, error.code, error.message, ctx
  • Can short-circuit: No — action: "respond" is logged and silently ignored. Use action: "continue" with a modified status, headers, and body.
  • Can modify status/headers/body: Yes
x-plenum-config:
on-gateway-error:
module: "./interceptors/error-handler.js"
function: onError
interceptors/error-handler.js
exports.onError = function onError(input) {
return {
action: "continue",
status: input.status,
body: {
error: input.error.code,
message: input.error.message,
},
};
};

Not all fields are available in every hook. Use this table to determine what your interceptor can access:

Fieldon_request_headerson_requestbefore_upstreamon_responseon_response_bodyon_gateway_error
method
route
path
headers
params
operation
ctx
options
query
queryParams
rate_limits
body✓*
bodyEncoding✓*
status
error

* body and bodyEncoding are only available in on_request phase 2 (body interceptors). Phase 1 interceptors in on_request see the same input as on_request_headers.

Only request-phase hooks can short-circuit with action: "respond". Hooks after the request is committed (before_upstream onwards) silently ignore respond — the gateway has already begun forwarding the request or streaming the response.

HookCan short-circuit?Behaviour
on_request_headersYesReturns immediate response to client
on_requestYesReturns immediate response. Skips remaining on_request interceptors
before_upstreamNorespond is logged and silently ignored
on_responseNorespond is logged and silently ignored
on_response_bodyNorespond is logged and silently ignored
on_gateway_errorNorespond is logged and silently ignored. Use continue with modified status/body

The ctx object carries data between interceptors within a single request. When an interceptor returns ctx, it is shallow-merged: keys you return overwrite existing ones; keys you don’t return are preserved.

// Interceptor A (on_request_headers) — sets ctx.userId
exports.extractUser = function extractUser(request) {
return {
action: "continue",
ctx: { userId: request.headers["x-user-id"] },
};
};
// Interceptor B (on_request) — reads ctx.userId, adds ctx.permissions
exports.checkAccess = function checkAccess(request) {
return {
action: "continue",
ctx: { permissions: ["read"] }, // ctx.userId is preserved
};
};

The gateway key on ctx is reserved for internal use. Values written to it by interceptors are silently dropped.

If an interceptor throws an unhandled exception:

  • A 500 response is returned to the client
  • The error is logged by the gateway
  • If an on_gateway_error handler is configured, it fires — letting you customise the error response format
// Faulty interceptor — throws on missing body
exports.parseBody = function parseBody(request) {
const data = JSON.parse(request.body); // throws if undefined
return { action: "continue", ctx: { data } };
};
// → 500 {"error": "internal server error"}
// → on_gateway_error fires (if configured)

The on_gateway_error handler receives error.code ("interceptor_error") and error.message (the exception text). Use continue — not respond — to modify the error response.