Context vars
The legacy $var/${var} syntax documented on this page only ever worked in a fixed
set of ~15 plugin config fields. The gateway now also renders {{namespace.path}}
templates — the same request/response/message/client data, unambiguous syntax, and
applied to every traffic-bound plugin config field, not just a chosen few. New configs
should prefer {{...}}; this page remains the reference for the legacy syntax (which
still works, unchanged, in the fields that always supported it) and for the full var
catalog both syntaxes share. See Templates for the {{...}}
grammar, namespaces, load- vs request-time resolution, pass-through/warning semantics,
exclusions, and the legacy $var → {{path}} mapping table.
Plugin configs reference live request/response data through $var interpolation — $uri, $http_user_agent, $arg_page, and friends — resolved by src/vars/mod.rs against the request's Context at execution time. This page is the full catalog: every name the resolver understands, the two ways to write a reference, which plugin config fields actually interpolate, and how the web UI's autocomplete popover and var legend help you avoid typos.
The catalog below is generated from the same source the gateway serves at GET /api/vars (Admin API, authed) and a Rust test (test_catalog_matches_resolver in src/vars/catalog.rs) keeps it from drifting out of sync with the resolver — if you see a var here, it works, and nothing the resolver accepts is missing from this list.
Syntax
Two forms, both interpolated by the same pass over the template string:
$name— bare form. The name is read as a maximal run of[A-Za-z0-9_]immediately after the$. Use this for every var whose name is only letters, digits, and underscores (which is all of them,msg_*included, as long as thecontext.messagekey itself has no dots).${name}— brace form, required when the name contains a character outside[A-Za-z0-9_]— in practice, a dottedcontext.messagekey such as${msg_consumer.name}. Without braces,$msg_consumer.namewould resolvemsg_consumer(stopping at the dot) and leave the literal text.namebehind.
Rules that apply to both forms:
- No escape syntax. A literal dollar sign followed by something that isn't a valid var start (e.g.
cost: 5$,$at end of string) passes through unchanged — there is no\$to force a literal dollar before a name-like token. - Unknown vars silently interpolate to empty string. A typo'd name, or a known family member that is absent on this request (missing header, missing query param, ...), resolves to
""— no error, no warning in the logs. This is the exact gap the autocomplete and legend exist to close: see them before you rely on a name you haven't verified.
Statics
Fixed names, each resolving to at most one value.
| Var | Description |
|---|---|
$uri | Request path (no query string) |
$request_uri | Path plus ?query when query params exist |
$method | HTTP method (alias: $request_method) |
$request_method | HTTP method (alias of $method) |
$host | Request Host |
$scheme | http or https |
$protocol | HTTP protocol version (http1, http2, ...) |
$remote_addr | Client IP without port |
$remote_port | Client port |
$query_string | Full query string, rebuilt and sorted |
$status | Response status code |
$resp_body | Response body (lossy UTF-8) |
$request_body | Request body (lossy UTF-8) |
$consumer_name | Authenticated consumer name (set by auth plugins) |
$consumer_group_id | Authenticated consumer group |
Families
Prefix families — one entry per actual value present on the request/response, not a fixed set. <name> is filled in by you (or picked from the autocomplete popover's live suggestions).
| Var | Source | Description |
|---|---|---|
$arg_<name> | query params | First value of a query parameter |
$http_<name> | request headers | First value of a request header; underscores in <name> map to dashes, compared case-insensitively ($http_user_agent → User-Agent) |
$cookie_<name> | cookies | Value from the Cookie request header |
$post_arg_<name> | form body | Form field, only for application/x-www-form-urlencoded request bodies |
$msg_<key> | context.message | Any context.message key, stringified; dotted keys need ${msg_key.with.dots} |
$sent_http_<name> | response headers | First value of a response header; same underscore→dash mapping as $http_*, mirrored exactly |
$sent_http_* and $request_body complete the var surface so every field of the Context (request and response alike) is reachable — see the Context object.
Where $var templates work
$var/${var} interpolation keeps working, unchanged, in the 15 fields that have always
supported it — see Templates → the 15 legacy fields
for the exhaustive, per-plugin list. Every one of the 15 also accepts the new
{{namespace.path}} syntax — the two compose safely (see
Templates → legacy interop).
A handful of config values that genuinely interpolate $var/${var} (and now also
{{namespace.path}}) templates aren't backed by a schema-form field the popover can attach
to, because they're edited as a raw JSON/YAML blob instead — the logging
family's log_format, forward-auth's extra_headers, and
response-rewrite's header maps are the current examples.
Both syntaxes still work there exactly as documented on this page and on
Templates — you just type them by hand, without a dropdown or live
preview.
No other field ever interpolated $var, and that hasn't changed: the sweep that made
{{...}} render everywhere left the legacy $ pass exactly where it always was.
What has changed is autocomplete coverage — the web UI now offers suggestions in config
fields that previously had no popover at all, though not the same suggestions everywhere:
context-aware {{...}} completion where a field is genuinely templated, and — on every
other text-like field — an env-only popover that inserts the legacy ${NAME} form (not
{{env.NAME}}; see the next paragraph for why). See
Templates → where the web UI offers suggestions.
Env-var and secret-looking fields (API keys, tokens, connection strings), and every other
field the plugin doesn't parse into a Template, still only ever get env-group
suggestions, never live request/response/message context — to avoid nudging you toward
putting request data where credentials go, and because those fields can't render request
data even if you typed a valid {{...}} reference into them. Picking one inserts
${NAME}, not {{env.NAME}}: {{env.NAME}} only resolves in a field the plugin actually
parses as a Template, and these fields never are, so {{env.NAME}} would sit there as
inert text forever. ${NAME}/${NAME:-default} is a different, older mechanism —
interpolate_env/interpolate_env_json in src/config/loader.rs — that substitutes every
string leaf of every node's config, templated or not, at compile time; it's what actually
resolves in these fields. See
Templates → exclusions for the full accounting.
Autocomplete and live value preview
Typing $ or ${ in a flagged field opens a popover: a substring-filtered list of var names (filter = whatever you typed after the $), navigable with the arrow keys and Enter/Tab to insert, Esc to dismiss. Each row shows the var name in monospace and, when one is available, a dimmed preview of its current value. A footer row explains why a value isn't shown when it isn't, or links to the full var legend (also reachable from a "Context vars" button in the node inspector header) — the same catalog as this page, grouped, with the live values inlined for whichever node is selected.
Names are always offered; live values require all of the following:
- Debug mode is enabled (
debug.enabledinsystem.yaml— see Debugging & sandbox). - The node under edit has an incoming edge, so there's a predecessor whose output the values come from (the success-edge predecessor wins on fan-in; a node with no incoming edge, or a node inside a supernode's own definition editor, only ever gets names).
- A debug trace exists for the policy containing that predecessor — i.e. a request has actually gone through it since the trace buffer was last cleared or the process last started.
When a value is available it comes from the latest handled request's debug trace: family members are discovered from what that request actually carried ($http_x_request_id shows up once a request sent that header), not guessed. A few caveats carry over from debug mode itself:
- Redacted fields show
<redacted>verbatim — the popover does not un-redact anything; a header or query param on the built-in or configured redaction lists previews as the literal string<redacted>, same as the trace it's drawn from. - Cookies are never previewable.
$cookie_*family members list by name only, with no value, by design (see Redaction). $resp_body/$request_bodypreviews needcapture_bodies: true. With bodies off,body.lenis captured but not the content, so these two previews stay unavailable even with a trace present.- Values are single-line and truncated (~80 characters) for the popover and legend display — a preview, not a full dump; use the debug trace API to see the real thing in full.
The var catalog API
GET /api/vars (Admin API, same Basic Auth as every other admin route) returns the catalog as JSON:
curl -u admin:admin http://localhost:9090/api/vars | jq
{
"vars": [
{ "name": "uri", "kind": "static", "description": "Request path (no query string)", "example": "$uri" },
{ "name": "http_*", "kind": "family", "family_source": "request_headers", "description": "First value of a request header (underscores map to dashes)", "example": "$http_user_agent" }
]
}
This is the same payload the UI fetches once per session to drive the autocomplete popover and the legend; there is no per-request cost to calling it yourself when building tooling against the same var surface.