Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

2.4 Variables and map

Configuration that computes. NGINX has no if you should love (Lesson 11.2 explains why), but it has something better: variables with per-request values and map, the cleanest dispatch in town.

Levels: Beginner ~35 min read

Basic

What you will learn

  1. What an NGINX variable is — a lazily-evaluated per-request slot.

  2. The core request variables, each observed live: $host, $uri, $request_uri, $args/ $arg_*, $scheme, $request_method, $remote_addr.

  3. set — creating your own (and the two places it's legal).

  4. map — derivation tables: syntax, matching modes, defaults, and three production idioms (including the WebSocket-upgrade classic).

  5. The variable gotchas that explain 90% of "why is it empty?".


1. What a variable is

A variable is not a value stored in your config file. It is a named slot whose value is computed per request, at first use, by the module that owns it. Until something reads $remote_addr, no address is resolved; when two locations never touch $args, no argument parsing happens for them. Lazy evaluation is why a config with hundreds of variables costs nothing for the requests that don't use them.

Two consequences worth money:

  • Variables cannot be assigned from the outside world at config load. $host at parse time has no value; it materializes per request. (The compile-time world has env and command-line -g for the few genuinely global values, like $MALLOC_ARENA_MAX-style knobs.)

  • Reading order can matter for mutable pipeline variables — $uri changes if a rewrite ran first (Module 11). Everything else is stable within a request.

The example config's /whoami endpoint is a live probe — it returns nine variables as text. This is the actual output for GET /whoami?foo=bar&x=1 with Host: alpha.example.test:

bash
$ curl -s -H 'Host: alpha.example.test' \
       'http://127.0.0.1:8200/whoami?foo=bar&x=1'
host          = alpha.example.test
uri           = /whoami
request_uri   = /whoami?foo=bar&x=1
args          = foo=bar&x=1
arg_foo       = bar
method        = GET
remote_addr   = 127.0.0.1
scheme        = http
server_name   = alpha.example.test

Nine lines, nine lessons. Let's take them in pairs.

2. The core variables, observed

$host vs $server_name — where the request is vs what you configured

$host is the request's view: the host name from the request line, or the Host header (lowercased, port stripped), or the matched server_name as a last resort — never the SNI name (that lives in $ssl_server_name). $server_name is the config's view: the server_name of whichever block won the Lesson 2.2 tournament.

bash
# both equal here (Host matched the exact server_name):
host          = alpha.example.test
server_name   = alpha.example.test

Send Host: ALPHA.example.test and $host lowercases to the same value. Send a host that matched the wildcard .example.test and they diverge — $host carries what the client asked for (that's why the wildcard demo prints host=random.example.test), while $server_name prints the configured pattern. Rule of thumb: proxy $host downstream (Module 4); log $server_name for config auditing.

$uri vs $request_uri — normalized vs verbatim

  • $request_uri = the raw request line URI — path plus query string, exactly as the client sent it, percent-escapes intact, never modified by anything.

  • $uri = the decoded, current path — escapes resolved, ../ ., normalized, and rewritten if rewrite directives ran.

bash
uri           = /whoami            # decoded path, no args
request_uri   = /whoami?foo=bar&x=1  # verbatim, args included

Cache keys want $request_uri (stability). Location matching and file paths want $uri (normalization). Mixing them up is the #1 source of "my cache serves the same page for different query strings" bugs (Module 8).

$args and $arg_<name> — the query string

$args is the whole string after ?. Each $arg_name extracts one parameter (case-sensitive name, first occurrence, URL-decoded):

bash
args          = foo=bar&x=1
arg_foo       = bar

These are how you build "one endpoint, parameterized behavior" without any rewriting — the /env?env=prod in §4 is the pattern in miniature.

$scheme, $request_method, $remote_addr

http/ https (known after TLS termination — Module 6), the method, and the peer IP as seen by NGINX (which is not the client IP once a proxy chain exists — $realip_remote_addr and Module 6's real_ip fix that).

3. set — your own variables

nginx
location /set-demo/ {
    set $tier "free";
    if ($arg_pro = 1) { set $tier "pro"; }   # ( Module 11 will delete this if )
    return 200 "tier=$tier\n";
}

set is a rewrite-phase directive (that's why it belongs to Module 11's phase story) usable in server, location, and if contexts. Its value is a string — computed once per request when the rewrite phase reaches it. Two laws:

  1. Declare before use in the config text — nginx resolves variable names to indexes as it parses, top to bottom: a name that is neither declared earlier (by map, geo, set, or a module’s own variables) nor one of the prefix families ( $http_*, $arg_*, $cookie_*) fails the whole config with unknown "tier" variable — whether the use is string interpolation ( return, proxy_pass) or a bare argument ( limit_req_zone $key, proxy_cache_key). Only the values are late: they materialize per request (Lesson 2.6 shows the parse rule live, with a file rename).

  2. A set in the server block runs before location choice; a set in the winning location overwrites it. One request, one timeline (Lesson 11.1 makes the whole timeline explicit).

4. map — the branching brain

map creates a new variable from an existing one via a lookup table, evaluated at http level, on first use per request. It is NGINX's answer to "I need a conditional" — without a runtime if.

nginx
# map <source> <destination>
map $arg_env $env_label {
    prod     production;        # literal source value
    staging  staging;
    dev      development;
    default  unknown;           # when nothing matched
}

Live, from a real request:

bash
$ curl -s -H 'Host: alpha.example.test' 'http://127.0.0.1:8200/env?env=prod'
arg_env  = prod
mapped    = production

$ curl -s -H 'Host: alpha.example.test' 'http://127.0.0.1:8200/env?env=nonsense'
arg_env  = nonsense
mapped    = unknown

The full syntax card:

nginx
map $source $dest {
    default          fallback;          # required in spirit; "unknown var" otherwise
    ~^/api/v\d+/     api-versioned;     # regex values (~ and ~* allowed)
    "/exact string"  quoted;            # quoted literals for spaces
    include  snippet/*.map;             # yes, maps can include
    hostnames;                          # enable wildcard domain matching:
    *.example.test   internal;          #   *.example.test
    example.test     internal;          #   exact + implied subdomains
    default          external;
}

Three production idioms

Idiom 1 — the WebSocket upgrade header. The single most-copied NGINX snippet on earth, and now you can read it:

nginx
map $http_upgrade $connection_upgrade {
    default  upgrade;
    ''       close;      # empty header (normal requests) -> close
}
# later (Module 4):
proxy_set_header Connection $connection_upgrade;

Observed directly at the /conn-upgrade probe:

bash
$ curl -s -H 'Host: alpha.example.test' http://127.0.0.1:8200/conn-upgrade
connection_upgrade = close

$ curl -s -H 'Host: alpha.example.test' -H 'Upgrade: websocket' \
       http://127.0.0.1:8200/conn-upgrade
connection_upgrade = upgrade

Why a map? Because Connection must be Upgrade only for websocket traffic and close otherwise — a derivation, not a constant. With a map it's declarative and testable; with an if it's a rewrite-phase hazard (Module 11's cautionary tale).

Idiom 2 — traffic classification. A third map in the example config classifies hosts with the hostnames mode:

nginx
map $host $site_class {
    hostnames;
    *.example.test   internal;
    default          external;
}

Idiom 3 — per-segment policy. The same shape powers canary releases ( map $cookie_version $pool { default stable; canary canary-pool; }, Module 5), cache TTLs by file class (Module 8), and TLS redirect exceptions ( map $uri $redirect_required { ~^/healthz 0; default 1; }). Once you see "policy differs by X", the answer is map $X $policy.

5. The "why is my variable empty?" checklist

  1. It was never read. Lazy evaluation: $upstream_addr is empty in a location that doesn't proxy. Print probes like /whoami to see truth.

  2. Phase too early. Variables owned by later phases ( $upstream_*, $sent_http_*) are empty before the content phase — e.g. inside rewrite-phase if conditions or log-early locations.

  3. Scope shadowing. A set in the winning location overwrote the server-level one; or a map default silently fired.

  4. Header spelling. $http_user_agent not $http_useragent; underscores, lowercase, always.

  5. It's per-worker. Anything you set lives in one worker's request only — cross-worker state needs a shared zone (Lesson 1.4 §5).

Try it yourself

  1. Probe requests of your own design. /whoami is a microscope — aim it:

bash
   curl -s -H 'Host: alpha.example.test' \
        'http://127.0.0.1:8200/whoami?a=1&a=2&X=3&space=a%20b'

Predict first: which arg_a wins? Is arg_X (uppercase) found? What does arg_space show?

  1. Map surgery. Add a branch to $env_label ( qa -> quality-assurance), nginx -s reload, test it. Then break the default line and watch unmatched inputs return an empty string — the checklist's #3, live.

  2. Watch divergence. $host vs $server_name on the wildcard vhost:

bash
   curl -s -H 'Host: random.example.test' http://127.0.0.1:8200/class

(That endpoint is on the default server too — try Host: unknown.host.xyz and see site_class flip to external.)

  1. Regex map values. Add ~^/api/v(\d+)/ api-v$1; to a new map and probe it. (Captures in map regexes become $1 — yes, really.)

Q&A

Q1. Why is $request_uri the recommended cache-key component but $uri the recommended location/ root component?

Answer

Cache keys must distinguish requests the client made differently ( /a?x=1 vs /a?x=2) and survive rewrites unchanged → verbatim $request_uri. Filesystem mapping needs the current, decoded path → $uri. One is identity; the other is a filesystem address.

Q2. A map with no default and unmatched input — what does $destination contain?

Answer

An empty string. Not an error, not the last branch — empty. Logs fill with mysteriously blank fields; policies built on the variable silently take their "else" paths. Always write the default.

Q3. Write the one-line idiom: "add header X-Env: prod only for requests whose $arg_env is prod, nothing otherwise".

Answer

map $arg_env $env_header { prod "prod"; default ""; } then add_header X-Env $env_header; — an empty value makes add_header omit the header entirely. (This empty-value-omits trick is also the standard way to make optional headers.)

Q4. $http_upgrade is empty for regular requests. What does $connection_upgrade evaluate to, and why is that the desired behavior?

Answer

close — the map's '' branch. Proxied Connection headers must not advertise Upgrade for non-upgrade traffic, or keep-alive pools (Module 5) misbehave. The map encodes the correct protocol behavior, per request, without any if.

Takeaways

  • Variables are lazy, per-request slots owned by modules — reading them is what computes them.

  • $host/ $server_name = request view vs config view; $uri = decoded-and-possibly-rewritten path; $request_uri = verbatim identity; $args/ $arg_* = query access; $http_*/ $sent_http_* = headers.

  • set creates your own; mind declaration-before-parse-time-use and the rewrite-phase timeline.

  • map is the config's branching brain: derivation tables with literal, regex, and hostnames modes; always write the default; three idioms (upgrade header, classification, per-segment policy) cover most needs.

  • "Empty variable" has five causes — lazy evaluation, phase, shadowing, spelling, per-worker scope — and now you have the checklist.

Was this page helpful?

Mark this learning resource complete to track your learning progress.