Rewrites, redirects and try_files
Return 301 and 302, rewrite flags last and break, try_files for single-page apps and fallbacks, trailing slash normalisation, and diagnosing redirect loops.
Return versus rewrite
# permanent redirect, remembered by browsers
location = /pricing {
return 301 https://example.com/plans;
}
# temporary: use 302 while a migration is still in progress
location /beta/ {
return 302 /app/;
}
# return can also build a response directly
location = /robots.txt {
add_header Content-Type text/plain;
return 200 "User-agent: *\nAllow: /\n";
}
# rewrite is for transforming the URI, not for answering
location /old/ {
rewrite ^/old/(.*)$ /new/$1 permanent; # 301, ends the request
}
# in server context, redirect every http request to https
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}returnis simpler and faster thanrewrite; use it whenever the target is fixed.- A
rewritewith no flag keeps processing in the same location, which is the source of most confusing behaviour. return 301is cached aggressively by browsers, so a mistake lives in user browsers long after you fix it.- Use
$request_urirather than$uriin a redirect: it preserves the query string and the original encoding.
Rewrite flags
| Flag | Effect | Use when |
|---|---|---|
last | Stop and re-run location matching | The target belongs to another location |
break | Stop, stay in this location | You only need to change the file path |
redirect | 302 to the client | Temporary, path changed |
permanent | 301 to the client | Permanent move, safe to cache |
| none | Keep processing in place | Almost never what you want |
location /downloads/ {
# rewrite to the real file path and stay here
rewrite ^/downloads/(.*)$ /releases/$1 break;
root /srv/files;
}
location /app/ {
# hand off to another location: re-run matching with the new URI
rewrite ^/app/(.*)$ /dashboard/$1 last;
}
location /dashboard/ {
proxy_pass http://127.0.0.1:3000;
}try_files for SPAs and fallbacks
# single-page app: serve the file if it exists, else the shell
server {
listen 443 ssl;
server_name app.example.com;
root /var/www/app;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# never fall back to the shell for asset paths
location ^~ /assets/ {
try_files $uri =404;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# proxy fallback: try a local file, then a cache, then the backend
location / {
try_files /maintenance.html @backend;
}
location @backend {
proxy_pass http://app_upstream;
}⚠️
Falling back to
index.html for a missing JavaScript or image file returns a 200 with an HTML body. The browser then fails to parse the module with an error that points at the wrong place, which is why asset paths deserve their own location with =404.FAQ
How do I debug an infinite redirect loop?
Check whether the redirect target matches the same rule again, often because the rule uses a prefix that also matches the destination. Add a condition on the scheme or host, or scope the rule with an exact
= location.Should try_files end with a named location or a URI?
A named location (
@name) lets you define real proxy or error handling with its own directives. A URI is shorter but everything after it happens inside the same location context.Related
How nginx matches a request: locations and rewrite phases WebSockets, gRPC and streaming responses
Last refreshed 2026-09-18.