How nginx chooses a location
- The URI is normalised first: percent-decoding, repeated slashes merged,
.and..resolved. The query string is never part of location matching. - All prefix locations (no modifier,
=,^~) are compared. An=exact match wins immediately. Otherwise the longest matching prefix is remembered. - If that longest prefix has
^~, it wins and regexes are skipped. - Regex locations (
~case-sensitive,~*case-insensitive) are tested in the order they appear in the file. The first match wins, even if a longer prefix matched earlier. - If no regex matches, the remembered longest prefix is used.
- Inside the winning location the same procedure runs again for nested locations.
Two consequences surprise people most: a plain location /images/ loses to location ~ \.png$ for /images/a.png, because regexes are checked after prefixes; and location /api also matches /apiary, because prefixes are string prefixes, not path segments. Use /api/ or ^~ when you mean it.
proxy_pass and the trailing slash
If proxy_pass has a URI part (anything after the host, even just /), nginx replaces the part of the request URI that matched the location prefix with it. location /api/ { proxy_pass http://app/; } turns /api/users into /users. Without a URI part, the request URI goes through unchanged. Mismatched slashes (/api vs /api/, or http://app vs http://app/) produce //users or users — the tester prints the exact upstream URI so you can see it. A URI part is not allowed in a regex location unless a rewrite changed the URI first.
root vs alias
root appends the whole URI to the directory: root /srv; location /static/ serves /srv/static/x.css. alias replaces the location prefix: alias /srv/assets/; location /static/ serves /srv/assets/x.css. With alias the trailing slashes of the location and the alias must agree, and in a regex location alias must use captures.
rewrite flags
| Flag | Effect |
|---|---|
| last | stop processing rewrites, start location matching again with the new URI (max 10 times, then 500) |
| break | stop rewrites, stay in the current location with the new URI |
| redirect | send a 302 to the client |
| permanent | send a 301 to the client |
| (none) | continue with the next rewrite; at server level the result is then matched against locations |
try_files
Each argument is checked as a file (or directory, with a trailing slash) relative to root/alias; the first that exists is served. The last argument is the fallback and is not checked: it is an internal redirect to a URI (which goes through location matching again), a named location @name, or =404-style status code. The tester cannot see your filesystem, so it lists the paths that would be checked.
add_header inheritance
add_header is inherited from the server level only when the location has no add_header of its own. One add_header Cache-Control in a location silently drops the security headers set at server level. The trace flags this.
Questions
Does it support the full nginx config syntax?
The parser handles the real syntax: nested blocks, comments, quoted strings, regex arguments, include directives are listed but not followed (paste the included file too). Variables other than $uri, $request_uri, $args and regex captures are left as text, and “if” blocks are shown but not evaluated.
Why does my regex behave differently in nginx?
nginx uses PCRE; this page uses JavaScript regular expressions. They agree on almost everything used in location blocks, but possessive quantifiers, recursion and some lookbehind forms differ. The trace says when a regex could not be compiled.
Can it test which server block answers for a Host?
Yes. Paste several server blocks and fill in the Host field. Selection follows nginx: exact name, longest wildcard starting with *, longest wildcard ending with *, first matching regex, then default_server, then the first server for that port.
How do I test on the real server?
nginx -T prints the full merged config (all includes resolved) which you can paste here. For a live check, curl -sI -H "Host: example.com" http://127.0.0.1/path and read the response; add_header X-Debug-Location "$uri" temporarily if you need to see which block answered.