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;
}
  • return is simpler and faster than rewrite; use it whenever the target is fixed.
  • A rewrite with no flag keeps processing in the same location, which is the source of most confusing behaviour.
  • return 301 is cached aggressively by browsers, so a mistake lives in user browsers long after you fix it.
  • Use $request_uri rather than $uri in a redirect: it preserves the query string and the original encoding.

Rewrite flags

FlagEffectUse when
lastStop and re-run location matchingThe target belongs to another location
breakStop, stay in this locationYou only need to change the file path
redirect302 to the clientTemporary, path changed
permanent301 to the clientPermanent move, safe to cache
noneKeep processing in placeAlmost 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.

How nginx matches a request: locations and rewrite phases WebSockets, gRPC and streaming responses

Last refreshed 2026-09-18.