Branch Chains

Branch chains add conditional paths to your filter pipeline. A filter produces a result, and a branch condition reads it to decide whether to divert, short-circuit, skip ahead, or loop back.
On this page

Branch chains add conditional paths to your filter pipeline. A filter produces a result, and a branch condition reads it to decide whether to divert, short-circuit, skip ahead, or loop back.

What Branch Chains Do

  • Short-circuit responses: block a request and return a static response without reaching the backend
  • Skip filters: bypass middleware (CORS, headers) for requests that don’t need it
  • Retry loops: re-run a section of the pipeline after mutating headers or refreshing tokens
  • Protocol-specific paths: route gRPC and JSON-RPC requests through different filter chains

Quick Start

This example blocks requests when the guardrails filter detects a dangerous header:

filter_chains:
  - name: main
    filters:
      - filter: guardrails
        action: flag
        rules:
          - target: header
            name: "X-Danger"
            contains: "true"
        branch_chains:
          - name: block_banned
            on_result:
              filter: guardrails
              result: blocked
            rejoin: terminal
            chains:
              - name: blocked_response
                filters:
                  - filter: static_response
                    status: 403

      - filter: router
        routes:
          - path_prefix: "/"
            cluster: backend
      - filter: load_balancer
        clusters:
          - name: backend
            endpoints: ["127.0.0.1:3000"]

When guardrails flags a request, it writes status=blocked to its filter results. The branch condition matches, the static 403 fires, and rejoin: terminal stops the pipeline. Normal requests pass through to routing.

See examples/configs/branching/conditional-terminal.yaml for the complete runnable config.

How It Works

Writing Filter Results

Filters write key-value pairs to a FilterResultSet, keyed by the filter’s type name (from HttpFilter::name()). Results are cleared after branch evaluation. Results a filter writes while the request body is buffered ahead of the request phase (for example json_rpc, or guardrails body rules) are held until that filter’s turn in the pipeline, so its branches see them wherever it sits. Two instances of the same filter type share one result slot. For the full lifecycle and API, see Pipeline Concepts: Filter Results.

Built-in filters that write results:

  • guardrails: status = blocked | passed
  • json_rpc: kind, method, id, id_kind, batch_len
  • grpc_detection: kind = grpc | grpc+proto | grpc+json | grpc+other

Defining Branches

Branches are defined in the branch_chains field on a filter entry:

- filter: guardrails
  branch_chains:
    - name: block_banned          # Globally unique
      on_result:                  # Condition (omit for unconditional)
        filter: guardrails        # Filter TYPE name
        result: blocked           # Expected value
      rejoin: terminal            # Where to resume
      chains:                     # Filters to execute
        - name: response
          filters:
            - filter: static_response
              status: 403

on_result.filter must name the host filter the branch is attached to: its filter type name (the return value of HttpFilter::name(), e.g. "guardrails"), not the user-assigned name: on the entry, and not some other filter in the chain. A branch only ever sees its own host filter’s results: the pipeline clears filter_results after each filter’s branch evaluation, so a condition naming a different filter could never match and is rejected at startup. See Pipeline Concepts: Two Meanings of “Name”.

on_result.result is the expected value. In the Rust code, this field is called value but is serialized as result in YAML for readability.

on_result.key defaults to "status". Override it to match other result keys (e.g., key: kind for grpc_detection).

Unconditional branches omit on_result entirely and always fire.

Multiple branches on one filter are evaluated in order; the first match with a non-next rejoin wins. Up to 16 branches per filter.

Rejoin Points

After a branch’s filters execute, the pipeline resumes at the rejoin point:

Rejoin valueBehavior
next (default)Continue with the filter after the branch point
terminal or clientStop the pipeline. In an ordinary routing pipeline, a branch can select a cluster via router + load_balancer and forward upstream. A binding-enabled pipeline must publish its request-scoped binding from a top-level router instead; branch routers are rejected. Otherwise the branch must produce the response (e.g. a static_response filter); a terminal branch that neither selects a cluster nor produces a response fails closed with a 500.
<filter_name> (forward)Skip to the named filter (must be after the branch point)
<filter_name> (backward)Re-enter at the named filter; requires max_iterations

Named rejoin targets use the entry name (the user-assigned name: on a filter entry), not the filter type name.

Re-entrance (backward rejoin) requires max_iterations to prevent infinite loops. The counter increments each time the branch fires; when exceeded, the branch falls through to Continue.

Chain References

Branch chains reference filters via chains:, which accepts two formats:

Named reference points to a top-level chain:

chains:
  - utility_chain

Inline definition defines filters directly:

chains:
  - name: inline_chain
    filters:
      - filter: static_response
        status: 403

Both can be mixed and are concatenated in order.

Patterns

Unconditional Sub-Chain

Always run an audit chain, then continue:

branch_chains:
  - name: always_audit
    chains:
      - audit

See examples/configs/branching/unconditional-branch.yaml.

Conditional Terminal

Short-circuit on a condition to block and respond without reaching the backend:

branch_chains:
  - name: block_request
    on_result:
      filter: guardrails
      result: blocked
    rejoin: terminal
    chains:
      - name: response
        filters:
          - filter: static_response
            status: 403

See examples/configs/branching/conditional-terminal.yaml.

Skip-Forward

Skip browser middleware for API requests:

- filter: headers
  name: classify
  branch_chains:
    - name: skip_browser
      on_result:
        filter: headers
        key: status
        result: api
      rejoin: routing    # Named filter ahead
      chains:
        - name: api_prep
          filters:
            - filter: headers
              request_add:
                - name: X-API
                  value: "true"

- filter: cors           # Skipped for API requests
- filter: forwarded_headers  # Skipped for API requests

- filter: router
  name: routing           # rejoin target
  routes: [...]

See examples/configs/branching/conditional-skip-to.yaml.

Re-Entrance Loop

Re-run classification after mutating headers, capped at 2 iterations:

- filter: headers
  name: classify
  branch_chains:
    - name: reclassify
      on_result:
        filter: headers
        key: action
        result: retry
      rejoin: classify    # Loop back
      max_iterations: 2
      chains:
        - name: retry_prep
          filters:
            - filter: headers
              request_add:
                - name: X-Retry
                  value: "true"

See examples/configs/branching/reentrance.yaml.

Multiple Branches

Multiple conditional branches on one filter act like a switch/case with first-match-wins:

branch_chains:
  - name: blocked
    on_result:
      filter: guardrails
      result: blocked
    rejoin: terminal
    chains: [blocked_response]
  - name: flagged
    on_result:
      filter: guardrails
      result: flagged
    rejoin: next
    chains: [flag_handler]

See examples/configs/branching/multiple-branches.yaml.

Cross-Chain Rejoin

A branch in one chain can rejoin at a named filter in another chain, because all listener chains are concatenated into one flat pipeline:

listeners:
  - name: web
    filter_chains: [preprocessing, routing]

filter_chains:
  - name: preprocessing
    filters:
      - filter: headers
        branch_chains:
          - name: skip_to_route
            rejoin: route  # In the 'routing' chain
            chains: [utility]
  - name: routing
    filters:
      - filter: router
        name: route        # Target for rejoin
        routes: [...]

See examples/configs/branching/cross-chain-flat.yaml.

Constraints

LimitValue
Maximum branch nesting depth10
max_iterations range1-100
Branches per filter16
Total branches across config256

Response hooks (on_response) run for every branch filter whose on_request ran, so filters such as cors, headers response operations, and credential_injection behave the same inside a branch as on the main path. They unwind in reverse, just like top-level filters: a branch’s filters run on_response in reverse order immediately before their host filter’s on_response, and a nested branch unwinds immediately before the branch filter that hosts it. A branch filter that rejected or answered the request still runs on_response; one skipped by its conditions, left unreached because the branch stopped early, or whose error propagated under failure_mode: closed does not. response_conditions and failure_mode apply exactly as at top level, and a rejection from a branch filter’s on_response stops the response phase. Re-entrance (max_iterations loops) does not repeat the hook: each branch filter runs on_response at most once per request, however many passes ran its on_request. Filters that rejoin skips over, or that a terminal rejoin never reaches, do not run on_response.

Body hooks (on_request_body, on_response_body) do not run for filters inside branch chains. Body-transforming filters must be in the main pipeline path. This is enforced at build time: a filter that declares body access inside a branch chain is rejected as a pipeline ordering error, and body access declared by branch filters never enables pipeline-wide body buffering.

Security filters inside branch sub-chains are held to the same rules as at top level: a security-critical filter (for example ip_acl or rate_limit) that carries request conditions or sets failure_mode: open is rejected at build time, however deeply it is nested. The branch executor honours both, so such a filter would otherwise be silently skipped for non-matching requests or have its errors swallowed. The same overrides apply: insecure_options.skip_pipeline_checks.conditional_security and insecure_options.allow_open_security_filters.

The branch’s own on_result gate is a separate matter. A conditional branch runs only when its condition matches, so a security filter inside one is skipped for every other request, even when the filter itself is unconditional and fail-closed. That is frequently deliberate (the branch is the operator’s admission decision, as in examples/configs/branching/nested-branches.yaml), so it is reported as a startup advisory, not an error:

security filter 'ip_acl' is reached only through
conditional branch 'gated'; it runs only for matching
requests

The gate is inherited, so the advisory also fires for a security filter in an unconditional branch nested inside a conditional one, naming the outermost gating branch. Put the filter on the main pipeline path, or in an unconditional branch, when it must apply to every request.

Nested control flow: SkipTo and ReEnter from nested branches (branches within branches) are discarded. Only Terminal and Reject propagate upward from nested branches.

Same-type result sharing: two instances of the same filter type in a pipeline share the same filter_results key. The second instance’s results overwrite the first’s.

Configuration Reference

BranchChainConfig

FieldTypeDefaultDescription
namestringrequiredGlobally unique branch name
chainslistrequiredChain references (named or inline)
on_resultobjectnullCondition; omit for unconditional
rejoinstring"next"Where to resume (next, terminal, client, or filter name)
max_iterationsintegernullRequired for backward rejoin; range 1-100

BranchCondition (on_result)

FieldTypeDefaultDescription
filterstringrequiredFilter TYPE name (from HttpFilter::name())
keystring"status"Result key to check
resultstringrequiredExpected value

ChainRef

Named reference (plain string):

chains:
  - utility_chain

Inline definition (object with name and filters):

chains:
  - name: inline
    filters:
      - filter: static_response
        status: 200