Skip to main content

Error Handling

Every plugin node has two output ports: success and error. Failures are not exceptions that abort the pipeline — they are routed through the graph like any other output, and the Context travels with them, so error handlers see the full request state.

A policy graph with red dashed error edges from key-auth, rate-limit and upstream all converging on a single error-handler node.A policy graph with red dashed error edges from key-auth, rate-limit and upstream all converging on a single error-handler node.
Error routing is visible in the graph itself: the red dashed edges are error ports. Three different failures — a rejected key, a throttled client, a dead upstream — converge on one handler, and each carries its Context with it.

What happens when a node fails

When a plugin returns an error, the engine:

  1. tags the error with the failing node's id;
  2. appends it to context.errors (an append-only list — earlier errors are preserved);
  3. picks the next node in this order:
PriorityDestinationWhen
1Per-node error edgeThe failing node's error port is wired (from: backend.error)
2Policy catch-allThe policy declares error_handler: <node_id>
3Generic 500Neither exists — execution stops

The generic fallback writes status 500 with a JSON body:

{"error": "internal_error", "message": "Unhandled error in routing policy"}

Graph execution itself never fails: every outcome, including the fallback, is expressed through the returned context's response.

Per-node error edges

Wire a specific node's error port to a handler to give that failure mode its own treatment:

edges:
- from: backend.error
to: error-handler.in
- from: error-handler.success
to: client.in

The error-handler plugin inspects the error and renders a custom response using a template:

- id: error-handler
type: error-handler
config:
status_code: 502
body_template: '{"error": "{{error.code}}", "message": "{{error.message}}"}'

Policy-level catch-all

A policy can name one node as its catch-all via the top-level error_handler field:

policies:
- name: echo-policy
error_handler: error-handler

Any node whose error port is not wired falls through to this node on failure. This prevents unhandled errors from surfacing as generic 500s. Being named as the catch-all counts as "connected" for validation purposes, so the handler node does not need explicit incoming edges.

Error handlers are regular nodes: they execute like any other node, continue through their own success edge (typically to the client node), and if they themselves fail, the same propagation rules apply to their error.

Validation rules

Every policy is validated before compilation — at startup, on hot-reload, and on Admin API writes — so malformed graphs are rejected with actionable messages instead of failing at request time. The enforced rules:

RuleDetail
Listener requiredThe policy must contain a listener node
Client requiredThe policy must contain a client node
Edges resolveEvery edge's from and to must reference an existing node
One edge per inputEach input port accepts at most one incoming edge — except inputs of client and error-handler nodes, which accept multiple (several paths can deliver the response or route errors to the same handler)
No orphansEvery node must have at least one incoming or outgoing edge; being named as the policy-level error_handler counts as connected
Catch-all resolveserror_handler, if set, must reference an existing node

Validation collects all violations rather than stopping at the first, and a failed validation on reload leaves the previous configuration serving traffic — see Architecture.