skip to content

In nginx, inside `location /api/ { ... }`, what is the difference between `proxy_pass http://backend;` and `proxy_pass http://backend/;`, and what URI does the upstream receive in each case?

level: middleimportance: must knowfreq 72%

answer

  1. depends on what follows the host
  2. URI present means replacement
  3. a bare slash counts as a URI
  4. matched prefix stripped, directive URI prepended
  5. no URI means the request URI passes untouched

basics

~20 s

Nginx replaces the matched location prefix with the URI in proxy_pass whenever one is present. With the trailing slash, /api/users reaches the upstream as /users; with no URI at all, the full /api/users is passed unchanged.

solid answer

~50 s

The rule turns on whether `proxy_pass` carries a URI component after the host or upstream name. `proxy_pass http://backend;` has none, so nginx forwards the request URI untouched and `/api/users` arrives as `/api/users`. `proxy_pass http://backend/;` does have one — the bare `/` counts — so nginx strips the part of the request URI that matched the location prefix and prepends the directive's URI, and `/api/users` arrives as `/users`. `proxy_pass http://backend/v2/;` rewrites it to `/v2/users`. That single character is the most common cause of a wall of upstream 404s after a config edit, so I keep the trailing slash consistent between the `location` and the `proxy_pass`. The URI form is not always legal either: inside a regex or named location, or after a `rewrite ... break`, nginx cannot determine what to replace and demands `proxy_pass` without a URI.

code

nginx · 17 lines
nginx
upstream backend { server 10.0.0.11:8080; }

server {
    listen 80;

    location /api/ {
        proxy_pass http://backend;
    }

    location /app/ {
        proxy_pass http://backend/;
    }

    location /old/ {
        proxy_pass http://backend/v2/;
    }
}

go deeper

for a junior

Be able to state the rule out loud: a trailing slash on proxy_pass strips the matched location prefix, no slash forwards the URI as-is. Knowing that one line prevents a whole class of 404s.

for a middle

Explain the mechanism, not just the symptom: nginx replaces the matched prefix with the directive's URI whenever a URI is present, and a bare slash is a URI. Walk through /api/users under both forms.

for a senior

Show how you diagnose it in production — compare $request_uri and $uri in the nginx log against the upstream's own access log — and name the cases where the URI form is illegal and rewrite ... break is the correct tool.

for a principal

Own the convention across a fleet: pick one canonical proxy_pass shape, keep path ownership at the edge rather than scattered across per-service rewrites, and treat URI rewriting as part of the routing contract you version, since a stripped prefix changes the URLs every backend logs, authorizes and caches on.

## What `proxy_pass` actually decides When nginx proxies a request it has to construct the request line it will send upstream. `proxy_pass` controls that, and its behaviour splits on one question: **does the value contain a URI component after the host, port, or upstream group name?** - **No URI** — `proxy_pass http://backend;` or `proxy_pass http://10.0.0.11:8080;`. Nginx sends the request URI as it stands, including the part that matched the location. - **A URI is present** — `proxy_pass http://backend/;`, `.../v2/`, `.../app`. Nginx takes the portion of the normalized request URI that matched the location prefix, removes it, and prepends the URI written in the directive. A lone `/` is a URI. That is the whole trick, and it is why the trailing slash is never cosmetic. ## Worked example ```nginx upstream backend { server 10.0.0.11:8080; } server { location /api/ { proxy_pass http://backend; # GET /api/users -> GET /api/users } location /app/ { proxy_pass http://backend/; # GET /app/users -> GET /users } location /old/ { proxy_pass http://backend/v2/; # GET /old/users -> GET /v2/users } } ``` The practical guidance is to keep the slashes symmetric: if the `location` ends in `/`, either give `proxy_pass` no URI at all or give it a URI that also ends in `/`. Mismatched slashes — `location /api` against `proxy_pass http://backend/` — produce URIs that look wrong at the upstream and are painful to reason about, so avoid the shape rather than memorising its output. ## Diagnosing it The failure is one-sided: the proxy is healthy, the upstream is healthy, and every response is a 404 or a redirect loop. Read the upstream's own access log, not nginx's — nginx logs `$request` (what the client sent), while the upstream logs what it actually received. `$uri` and `$request_uri` in an nginx log format make the difference visible from the proxy side too: `$request_uri` is the original, unmodified URI, `$uri` is the current normalized one after any rewrites. ## Where the URI form is forbidden Nginx can only do prefix replacement when it knows what matched. It does not in these cases, and refuses to start: - `location ~ ^/api/` — a regex location. - Named locations, `@fallback`. - Inside `if` or `limit_except`. In all of them `proxy_pass` must be written without a URI, and you shape the URI yourself with `rewrite`: ```nginx location ~ ^/old/(.*)$ { rewrite ^/old/(.*)$ /v2/$1 break; proxy_pass http://backend; } ``` The `break` flag stops rewrite processing and keeps the request in this location; the rewritten URI is then what `proxy_pass` sends. Adding a URI to `proxy_pass` here would be both illegal and redundant. ## The variable form is a third behaviour ```nginx proxy_pass http://$upstream_host:8080; ``` Once a variable appears in `proxy_pass`, nginx no longer resolves the name at configuration load time against an `upstream` group; it needs a `resolver` directive and looks the name up at request time, honouring the record's TTL. That is sometimes exactly what you want — it lets a backend's address change without a reload — but it is a different code path with different failure modes, and a `proxy_pass` that suddenly starts returning errors after someone parameterised the host is usually a missing `resolver`. ## Why interviewers ask It is the smallest possible illustration of the general point that a reverse proxy rewrites requests, and that the app behind it sees something different from what the client sent. A candidate who can state the rule crisply, name the two forms, and say what they check in the upstream's log has clearly configured a real proxy rather than copied a snippet.

  • Why can't you use the URI form of proxy_pass inside a regex location, and what do you do instead?
    With a regex location nginx has no fixed prefix it can identify as "the matched part", so it cannot compute the replacement and rejects the config at load time. You write `proxy_pass` without a URI and shape the path yourself with `rewrite ... break;` inside the location, using the regex captures to build the upstream URI.
  • What changes when the proxy_pass value contains a variable, such as proxy_pass http://$backend;?
    Nginx stops resolving the target at configuration load and resolves it per request, so a `resolver` directive is required and DNS TTL now governs where traffic goes. It also bypasses the `upstream` block's group behaviour unless the variable expands to an upstream name. A `proxy_pass` that broke right after being parameterised is usually a missing resolver.
  • How would you confirm from the logs which URI the backend actually received?
    Compare the two sides. In nginx, log both `$request_uri` (the original client URI) and `$uri` (the current, rewritten one) in the log format. Then read the upstream's own access log, which records what arrived on the wire. If nginx shows /api/users and the backend shows /users, `proxy_pass` carried a URI and stripped the prefix.

saying these in an interview costs you the question

  • Says the trailing slash is cosmetic and changes nothing
  • Thinks nginx always strips the location prefix before proxying
  • Believes proxy_pass with a URI works fine inside a regex location
  • Assumes a rewrite inside the location has no effect on what is proxied
  • Expects proxy_pass with a variable to behave identically to the static form

context