Which location handled the request
Before reading any proxy setting, work out which location block nginx used, because an unexpected block explains many wrong pages. nginx documents the order: it first finds the longest matching prefix location and remembers it. If that prefix has the ^~ modifier, the search stops. Otherwise it tests the regular-expression locations in the order they appear in the file and uses the first that matches, falling back to the remembered prefix only if none does. An exact location with = ends the search at once. So a regex block placed earlier in the file can capture a request you expected a longer prefix to handle, and moving blocks around changes the result.
- Exact (=) wins immediately; ^~ prefix stops the regex search; plain prefixes lose to a matching regex.
- Regex locations are order-dependent, so check their order in the file.
- Normalisation applies first: percent-escapes are decoded and repeated slashes are merged before matching.
What proxy_pass does with a path
If proxy_pass names a URI, even just a trailing slash, nginx replaces the part of the request URI that matched the location with that URI. If it names only the server, nginx passes the request URI through unchanged. So a location of /app/ with proxy_pass http://127.0.0.1:3000/ sends /app/page to the upstream as /page, while the same location with proxy_pass http://127.0.0.1:3000 sends /app/page unchanged. That single slash is the most common reason an application returns its own 404 for a path that looks correct in nginx. Decide which path the application expects and write the form that produces it.
- Trailing slash on the upstream address means the matched prefix is stripped.
- No URI means the full request path reaches the upstream.
- Check what the application receives by reading its own access log, not nginx's.
Where the substitution cannot work
nginx cannot work out which part to replace in some cases, and the documentation says what to do. In a regex location or a named location, proxy_pass must be written without a URI. If a rewrite with the break flag changes the URI first, the URI in proxy_pass is ignored and the changed URI is passed. If proxy_pass contains variables and a URI, that URI is passed exactly as written in place of the original. Mixing these without noticing produces paths that look nothing like the request. A rewrite that keeps matching its own location can also loop; nginx stops after 10 internal redirects and returns a 500 error, which a visitor sees as a failure that has nothing to do with the application.
- Regex or named location: no URI in proxy_pass.
- rewrite with break: the changed URI is what the upstream receives.
- Variables in proxy_pass with a URI: that URI replaces the request URI.
A trailing slash that redirects
For a prefix location that ends in a slash and is handled by proxy_pass, nginx answers a request for the same path without the slash with a 301 redirect to the slash version. That surprises teams who expected the bare path to reach the application, and it can look like a redirect loop if the application also redirects back. The documented remedy is an exact-match location for the bare path. Check the response with a request that does not follow redirects, so you can see whether the redirect comes from nginx or from the application.
- Use a request that shows the status and Location header without following it.
- An exact-match block for the bare path avoids the automatic redirect.
- If the application also redirects, decide which side owns the trailing-slash rule.
Confirm it with a table, then change one thing
Write a small table of the paths that matter: the request path, the location that handled it, the path the upstream received and the status. Fill the third column from the application's own log. The first row where the paths differ from your intention is the fault. Change only that block, run the configuration test, reload, and repeat the table. This is the same method the nginx repair job uses for routing faults, though that fixed job is scoped to 502 and 504 errors; a pure routing fault on a site with no gateway errors is outside that fixed job and would be quoted separately after an enquiry.
Sources and limits
- nginx: ngx_http_core_module (location) Checked 2026-10-11.
- Prefix locations are checked first and the longest match is remembered; a ^~ prefix stops the regex search; otherwise regex locations are checked in order and the first match wins.
- A prefix location ending in / handled by proxy_pass gets a 301 redirect for the same URI without the trailing slash.
- nginx: ngx_http_proxy_module (proxy_pass) Checked 2026-10-11.
- With a URI in proxy_pass, the part of the request URI matching the location is replaced by that URI.
- Without a URI, the request URI is passed as the client sent it; in regex and named locations proxy_pass should be specified without a URI.
- nginx: ngx_http_rewrite_module Checked 2026-10-11.
- A rewrite loop is limited to 10 iterations and then returns a 500 error.