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

11.2 The if Trap and Its Cures

"location if" is a rewrite-phase directive pretending to be a control structure. Inside it, you are standing in a different location than you think — reproduced step by step.

Levels: Advanced ~35 min read

Advanced

What you will learn

  1. What if actually is (a rewrite-phase context jump, not flow control) — and what that breaks.

  2. The two sanctioned patterns: return inside if, set inside if.

  3. The canonical trap, reproduced live: try_files + a matching if = 404.

  4. The cure that scales: map, and when you genuinely can't avoid if.


1. What if really is

Lesson 2.4 told you NGINX "has no if you should love." Here's the debt paid in full. if comes from the rewrite module — the same phase family as set and return (Lesson 11.1). It is not a general-purpose branch; it is a conditional context switch:

When an if condition matches, NGINX continues processing the request inside the if's implicit nested location. Rewrite-phase directives ( set, return, rewrite) work there — they're evaluated early. But content handlers are not inherited into that nested context — proxy_pass, try_files, fastcgi_pass set up in the outer location simply do not exist inside the if.

So the semantics depend entirely on what you put in the if: only rewrite-phase directives → they run and processing continues in the if-context; anything that assumed the outer content handler → silently gone. This is why the famous community wisdom says if is "if (…){ rewrite… } or die trying."

2. The two sanctioned patterns

Both are safe because they never rely on the outer content handler surviving.

Pattern A — return inside if: the request dies right there, on your terms, with your message.

nginx
location /api {
    if ($arg_token = "") {
        return  401  "missing token — add ?token=anything\n";
    }
    proxy_pass  http://127.0.0.1:9001;
}

Receipt (real capture):

text
missing token — add ?token=anything
status=401            <- with ?token=abc: 200, proxied to b-node-1

Pattern B — set inside if: decide a flag early, consume it later. Since set runs in the rewrite phase, the variable is ready before headers or handlers evaluate it.

nginx
location /tier {
    set  $tier  free;
    if ($arg_pro = 1) { set  $tier  pro; }
    add_header  X-Tier  $tier;
    return      200    "tier=$tier\n";
}

Receipts (real captures — the flag moved, the world didn't):

text
X-Tier: pro          <- /tier?pro=1
X-Tier: free         <- /tier

Note what makes this safe: the if contains only set. The content phase (the return, the add_header evaluation) happens after, in the outer location, reading the variable the if prepared. Phase separation is the whole discipline.

3. The trap, reproduced

The canonical foot-gun, straight from a decade of "nginx if is evil" threads — the museum's centerpiece:

nginx
location /trap {
    try_files  /this-file-never-exists  @fallback;
    if ($arg_mode = special) {
        set  $note  "if matched";
    }
}
location @fallback {
    return  200  "fallback served (try_files worked — no if matched)\n";
}

The intent is obvious: "when mode=special, note it, then serve like always." The reality (real captures):

text
no arg:          status=200    (try_files -> @fallback)
?mode=special:   status=404    (if matched: handler LOST -> 404)

Walk the failure with the phase model from Lesson 11.1: the if matches → processing jumps into the if's nested context → the content phase looks for a content handler there and finds none (try_files doesn't inherit) → NGINX falls back to the implicit static handler → serves /this-file-never-exists as a file →

  1. The @fallback you so carefully configured never existed as far as that request is concerned.

Every variant of this trap has the same skeleton:

You wrote

What breaks when the if matches

try_files + if

fallback logic vanishes (this lesson's 404)

proxy_pass + if { set }

requests serve static files from the if-context — often 404s "randomly"

if + rewrite … last

extra location-matching passes — occasionally loops

add_header inside if

the if's headers replace the outer set entirely (Module 2's receipt)

4. The cure: map first

The museum's final exhibit rewrites the same intent with zero foot-guns:

nginx
# http{} context — map never lives inside a server/location
map $arg_mode $trap_mode {
    special  special;
    default  normal;
}
...
location /cured {
    try_files  /this-file-never-exists  @cured_fallback;
}
location @cured_fallback {
    return  200  "mode=$trap_mode — map decided, try_files intact\n";
}

Receipt (real capture, ?mode=special):

text
mode=special — map decided, try_files intact

Same input, same decision, but map is a lookup that happens when the variable is read, not a context jump. Nothing moves; no phase is entered or skipped; try_files does its job and reads the answer off $trap_mode. Module 2 taught the mechanics; this is the payoff: any if whose branches only choose values is a map wearing a disguise.

The decision procedure that keeps configs honest:

  1. Do both branches only set or return? → if is acceptable (patterns A/B).

  2. Does the condition choose a value used elsewhere? → map, always.

  3. Does the condition need to switch handlers (different proxy_pass)? → split into two locations and let location matching do the branching — it's what it's for.

  4. Still tempted to put a content directive inside if? → re-read section 3, then restructure.

5. When you truly can't avoid if

Some conditions can't be pre-computed by map — most often regex-matching a header and acting on the match's captures ( map regexes can extract via named captures too, but complex multi-condition logic reads worse than it works). Then:

  • keep only rewrite-phase directives inside ( set/ return/ rewrite … break),

  • compute a routing variable,

  • let a location or a proxy_pass variable consume it.

That's exactly the Module 7 WAF pattern ( map blacklist → sanctioned if { return 403 }) and the Module 10 SNI router's spirit, now fully explained.

Try it yourself

  • curl -i 'http://127.0.0.1:9301/tier' → X-Tier: free; ?pro=1 → pro. Then the other :9301 endpoints, the trap and the cure:

  • curl -i 'http://127.0.0.1:9301/trap?mode=special' → 404; drop the arg → 200. Show a colleague; enjoy their face.

  • curl -i 'http://127.0.0.1:9301/cured?mode=special' → the map version doing the same job safely.

  • Try to break /api (pattern A) by adding a proxy_pass inside the if — nginx -t accepts it without a murmur ( proxy_pass is legal in if-in-location), and that IS the trap: the match runs in the if's implicit nested location. For a refusal, try try_files inside the if — that one draws the error.

Q&A

Q: Why doesn't NGINX just fix if ? A: Backward compatibility, and the phase model itself: content handlers are bound to locations at config time; a runtime branch can't conjure a second binding without becoming… a location. Which is the answer: use locations as your branches.

Q: Is if in server{} (outside location) also dangerous? A: Less so — there's no content handler to lose yet; it's still rewrite-phase. But map + variables still reads better.

Q: What about error_page tricks instead of if ? A: error_page 418 = @special; + return 418 when the condition matches is a classic phase-safe branch — the "sanctioned-if pattern" Module 6's mTLS lesson used. It's pattern A with extra steps, and it's legitimate.

Takeaways

  • if is a rewrite-phase context jump: matching requests continue inside an implicit nested location where outer content handlers ( try_files, proxy_pass) do not exist.

  • Sanctioned contents: return (die on your terms) and set (decide a flag for later). Anything else is a trap wearing a condition.

  • The live receipt from section 3: try_files + matching if = 404 where 200 belongs — the canonical "nginx if is evil" evidence.

  • Reach for map by default; branch with locations when handlers must differ; treat any if containing a content directive as a bug until proven otherwise.

Was this page helpful?

Mark this learning resource complete to track your learning progress.