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.
Lifecycle order
Section titled “Lifecycle order”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-circuit2. on_request ── phase1: headers │ phase2: full body, can short-circuit3. before_upstream ── final headers before upstream, respond ignored4. on_response ── response status + headers, respond ignored5. on_response_body ── full response body, respond ignored6. on_gateway_error ── triggered by any gateway error (global only)Hook details
Section titled “Hook details”on_request_headers
Section titled “on_request_headers”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_limitsis unavailable) - Can short-circuit: Yes (
action: "respond") - Can modify headers: Yes
exports.checkApiKey = function checkApiKey(request) { if (request.headers["x-api-key"] !== "secret") { return { action: "respond", status: 401, body: { error: "Unauthorized" } }; } return { action: "continue" };};on_request
Section titled “on_request”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
bodyandbodyEncoding - 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: validateexports.validate = function validate(request) { if (!request.body?.name) { return { action: "respond", status: 400, body: { error: "name is required" } }; } return { action: "continue" };};When
on_requestinterceptors are configured, the request body is fully buffered before phase 2. For details on buffering, types, and binary handling, see Body Buffering.
before_upstream
Section titled “before_upstream”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
exports.signRequest = function signRequest(request) { const signature = hmacSha256(request.path, secret); return { action: "continue", headers: { "x-signature": signature }, };};The gateway automatically strips
Content-Lengthand setsTransfer-Encoding: chunkedbefore forwarding whenon_requestinterceptors are configured (the body may have changed size). It also forcesaccept-encoding: identitywhenon_response_bodyis configured (to prevent compressed upstream responses). See Body Buffering.
on_response
Section titled “on_response”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
exports.addHeaders = function addHeaders(response) { return { action: "continue", headers: { "x-response-time": response.ctx.startTime ? Date.now() - response.ctx.startTime : null }, };};on_response_body
Section titled “on_response_body”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
bodyreplaces 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_bodyx-plenum-interceptor: - module: "./interceptors/transform-response.js" hook: on_response_body function: transformexports.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.
on_gateway_error
Section titled “on_gateway_error”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. Useaction: "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: onErrorexports.onError = function onError(input) { return { action: "continue", status: input.status, body: { error: input.error.code, message: input.error.message, }, };};Data availability matrix
Section titled “Data availability matrix”Not all fields are available in every hook. Use this table to determine what your interceptor can access:
| Field | on_request_headers | on_request | before_upstream | on_response | on_response_body | on_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.
Short-circuit rules
Section titled “Short-circuit rules”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.
| Hook | Can short-circuit? | Behaviour |
|---|---|---|
on_request_headers | Yes | Returns immediate response to client |
on_request | Yes | Returns immediate response. Skips remaining on_request interceptors |
before_upstream | No | respond is logged and silently ignored |
on_response | No | respond is logged and silently ignored |
on_response_body | No | respond is logged and silently ignored |
on_gateway_error | No | respond is logged and silently ignored. Use continue with modified status/body |
Context merging
Section titled “Context merging”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.userIdexports.extractUser = function extractUser(request) { return { action: "continue", ctx: { userId: request.headers["x-user-id"] }, };};
// Interceptor B (on_request) — reads ctx.userId, adds ctx.permissionsexports.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.
Interceptor errors
Section titled “Interceptor errors”If an interceptor throws an unhandled exception:
- A
500response is returned to the client - The error is logged by the gateway
- If an
on_gateway_errorhandler is configured, it fires — letting you customise the error response format
// Faulty interceptor — throws on missing bodyexports.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_errorhandler receiveserror.code("interceptor_error") anderror.message(the exception text). Usecontinue— notrespond— to modify the error response.