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

4.1 proxy_pass Fundamentals: The URI Rules

One directive, four behaviors, one character that changes everything. This lesson settles the trailing-slash confusion permanently — with an echo backend as referee.

Levels: Intermediate ~40 min read

Intermediate

What you will learn

  1. What proxy_pass does mechanically (connect, forward, relay).

  2. The four URI-transformation rules — memorized via a live table.

  3. Why variables in proxy_pass disable transformation entirely.

  4. DNS, resolver behavior, and the "immutable at config load" rule.

  5. The upstream preview (Module 5 takes it from here).


1. What proxy_pass does

nginx
location /api/ {
    proxy_pass http://127.0.0.1:9001;
}

Mechanically, on each request entering the location, the proxy module:

  1. Resolves the target (IP now, or via resolver at runtime — §4).

  2. Opens (or reuses) an upstream connection — plain HTTP/1.0 by default; you'll upgrade that in §3 of the next lesson.

  3. Builds the upstream request: the client's method and body, the URI transformed per the rules below, plus headers (yours + defaults + the client's, minus hop-by-hop ones).

  4. Relays the response back through the same buffers and timeouts you configure (Lesson 4.6).

Everything interesting about "what the backend sees" is step 3 — and the single most consequential decision is whether your proxy_pass value has a URI part after the host.

2. The four URI rules — in practice

The example gateway fronting a Node echo backend (which reports the path it received) demonstrates all four forms in one server. This is the actual output, one line per form:

bash
$ for p in /plain/abc /strip/abc /map/abc /varpass/abc; do
    printf '%-12s -> %s\n' "$p" "$(curl -s -H 'Host: gw.example.test' \
      http://127.0.0.1:8400$p | jq -r .path)"
  done
/plain/abc   -> /plain/abc       # rule 1: NO URI part
/strip/abc   -> /abc             # rule 2: URI part "/"
/map/abc     -> /api/abc         # rule 3: URI part "/api/"
/varpass/abc -> /varpass/abc     # rule 4: variables

The four configurations that produced it (condensed from the working config):

nginx
# Rule 1 — NO URI part (nothing after host:port):
location /plain/ {
    proxy_pass http://127.0.0.1:9001;      # /plain/abc -> /plain/abc
}

# Rule 2 — URI part "/" (the famous trailing slash):
location /strip/ {
    proxy_pass http://127.0.0.1:9001/;     # /strip/abc -> /abc
}

# Rule 3 — URI part "/api/":
location /map/ {
    proxy_pass http://127.0.0.1:9001/api/; # /map/abc -> /api/abc
}

# Rule 4 — variables anywhere in the value:
location /varpass/ {
    set $backend_port 9002;
    proxy_pass http://127.0.0.1:$backend_port;   # no transformation at all
}

The precise semantics:

Rule 1 (no URI part). The request URI is forwarded unchanged — path, and separately, the original query string. Rules 2–3 (URI part present). The part of the request URI matching the location prefix is cut off and replaced by the URI part. /map/ + /abc − /map/ = /abc, glued to /api/ → /api/abc. Rule 4 (variables). When the value contains variables, NGINX cannot know at config load how to transform URIs — so it does none. The raw request URI goes to the backend as-is (and, unlike rules 1–3, the full $request_uri including query string if you pass none explicitly).

The query string footnote

Under rules 2–3 the query string survives untouched: /map/abc?x=1 → /api/abc?x=1. Under rule 4, if you want to modify the query you must interpolate it yourself ( proxy_pass http://$backend$uri$is_args$args;) — the subject of one of NGINX's most-copied StackOverflow snippets, now demystified.

3. What else the backend receives

Step 3 also includes a small set of default headers NGINX sends upstream when you configure nothing:

Default

Value

Host

the proxy_pass host ( 127.0.0.1:9001 — not the client's!)

Connection

close (HTTP/1.0-style, one request per connection)

Everything else of the client's headers passes through by default — User-Agent, Accept, cookies, Authorization — minus hop-by-hop headers ( Transfer-Encoding handling, Keep-Alive, etc.).

Those two defaults are both wrong for modern apps — the backend sees Host: 127.0.0.1:9001 and new connections per request — and fixing them ( proxy_set_header Host $host;, proxy_http_version 1.1 + upstream keepalive) is precisely Lesson 4.2's and Module 5's business. The header-cancellation law from Lesson 2.5 governs every line you add.

4. DNS, resolver, and when the target is frozen

For a static proxy_pass http://api.internal:8080;, NGINX resolves api.internal once at config load — the master resolves it while parsing, workers reuse the address list forever (until reload). Add these facts to your mental model:

  • A name that resolves to N addresses becomes an implicit upstream pool — NGINX load-balances across them round-robin (Module 5 relies on exactly this).

  • No re-resolution on TTL. If the DNS record changes (blue/green flip, autoscale event), a running NGINX keeps using the old addresses until you reload. This is the classic "kubernetes pod moved, NGINX still hits the grave" failure — and the reason the resolver + variables pattern exists: with a variable target ( set $up http://api.internal:8080; proxy_pass $up;) plus a resolver 127.0.0.1 valid=10s;, workers re-resolve per valid window at request time. Module 13 turns this into a full dynamic-upstream setup.

5. The upstream preview

One more form exists — naming a pool instead of an address:

nginx
upstream node-pool {
    server 127.0.0.1:9001;
    server 127.0.0.1:9002;
    server 127.0.0.1:9003;
}
…
location /pool/ {
    proxy_pass http://node-pool/;      # same URI rules apply to the part after the name
}
bash
$ curl -s -H 'Host: gw.example.test' http://127.0.0.1:8400/pool/x | jq -r .server
b-node-3

The URI rules are unchanged (the pool name is just the host part); what you gain is the entire Module 5 curriculum: scheduling algorithms, failure detection, connection reuse, and zones. For today: know that the form exists and follows today's rules exactly.

Try it yourself

  1. Reproduce the four-rule table (§2) — then extend it with query strings: does /map/abc?x=1 keep ?x=1? Does /varpass/abc?x=1?

  2. The capture-forwarding idiom. Add location ~ ^/u/(?<rest>.*)$ { proxy_pass http://127.0.0.1:9001/$rest$is_args$args; } and test /u/hello?x=1. You have just written the regex-location forwarding pattern from first principles.

  3. Watch Host change live. Through the echo:

bash
   curl -s -H 'Host: gw.example.test' http://127.0.0.1:8400/plain/ | jq .headers.host

Then compare with /own-headers/ — the next lesson's subject.

  1. Break rule 3 on purpose. Remove the trailing slash from the /map/ form ( proxy_pass http://127.0.0.1:9001/api;) and predict what /map/abc becomes before curling. (It's not /apabc — but why?) Restore.

Q&A

Q1. location /shop/ { proxy_pass http://backend; } — what URI does the backend see for GET /shop/cart?item=7?

Answer

/shop/cart?item=7 — no URI part, everything forwarded verbatim (the query string travels with the request either way).

Q2. Same request under proxy_pass http://backend/store/;?

Answer

/store/cart?item=7 — the /shop/ prefix is replaced by /store/, query string untouched.

Q3. Why does a variable in proxy_pass disable URI transformation?

Answer

Transformation is a config-time rewrite based on the fixed URI part; with variables NGINX can't compute the pattern until request time, so it declines to transform at all and forwards the URI unchanged. (This also enables the resolver-based dynamic upstreams of Module 13 — the "bug" is a feature.)

Q4. Your service discovery updates DNS, but NGINX keeps sending traffic to dead pods for hours. Diagnose in one sentence.

Answer

Static proxy_pass names are resolved once at config load and never re-resolved — switch to a variable target plus resolver … valid=10s; (Module 13's full treatment) or reload on discovery events.

Takeaways

  • The URI part of proxy_pass is a replacement for the location prefix: absent = forward verbatim; / = strip the prefix; /api/ = swap it; variables = no transformation (ever).

  • Defaults sent upstream are minimal and deliberately backwards-compatible ( Host = proxy host, Connection: close) — Lesson 4.2 fixes both.

  • Static targets are resolved at config load; dynamic resolution needs variables + resolver — the foundation of Module 13.

  • Regex locations may not carry a URI part; use captures when you must reshape there.

  • upstream pools follow all the same URI rules — they're Module 5's subject wearing today's clothes.

Was this page helpful?

Mark this learning resource complete to track your learning progress.