An nginx site serving a single-page app uses `try_files $uri $uri/ /index.html;` and its error log fills with "rewrite or internal redirection cycle while internally redirecting to /index.html". What does `try_files` actually do, and what produces that cycle?
answer
- last argument is not tested
- fallback means internal redirect
- matching restarts from the top
- the missing file is the cause
- named location ends the chain
basics
~20 stry_files tests each argument as a file or directory under the current root and serves the first that exists; the last argument is a fallback URI that triggers an internal redirect. If that fallback file is missing, the redirect re-enters the same location and loops until nginx gives up with a 500.
solid answer
~50 s`try_files` walks its arguments left to right, testing each as a path under the location's `root` or `alias` — `$uri` as a file, `$uri/` as a directory — and serves the first that exists. The **last** argument is different: it is not tested, it is a fallback. If it is a URI, nginx performs an *internal redirect*, which restarts location matching for the rewritten path; if it is `=404`, nginx just returns that code. The cycle happens when the fallback URI lands back in the same location and `try_files` fails again: `/index.html` does not exist under that root — wrong root, wrong `alias`, an unbuilt frontend — so nginx redirects to `/index.html` again, and again. After about ten internal redirects it aborts with 500 and logs that message. The fix is to make the fallback terminal: correct the root, or point it at a named location such as `@spa` that serves the file directly.
code
nginx · 17 linesserver {
listen 80;
server_name app.example.com;
root /srv/app;
location /assets/ {
try_files $uri =404;
}
location / {
try_files $uri $uri/ @spa;
}
location @spa {
try_files /index.html =404;
}
}go deeper
Know that try_files checks each path in order and serves the first that exists, and that the final argument is a fallback rather than another candidate file.
Explain the internal redirect: nginx rewrites the URI and restarts location matching, so a fallback that fails again re-enters the same block and loops until nginx aborts with 500.
Diagnose it end to end — read the cycle message, resolve the fallback against the location's root, stat it as the worker user, and make the fallback terminal with a named location or =404 so the next failure is legible.
Own the pattern across the estate: app routes and static assets get separate locations with different fallbacks, so a deploy that ships an incomplete build fails as a visible 404 rather than as HTML served with a 200 under every asset URL.
## What try_files evaluates ```nginx location / { root /srv/app; try_files $uri $uri/ /index.html; } ``` Nginx tests the arguments in order against the filesystem, rooted at the location's `root` (or `alias`): - `$uri` — does `/srv/app<uri>` exist as a file? Serve it. - `$uri/` — does it exist as a **directory**? Then the `index` directive takes over inside it. - the **last** argument — never tested. It is the fallback, and it is one of two things: a **URI**, which triggers an *internal redirect*, or `=<code>` (for example `=404`), which returns that status. The asymmetry between "tested" and "fallback" is the whole question. `try_files $uri $uri/ /index.html;` does not mean "serve /index.html if it exists" — it means "give up and re-request /index.html". ## What an internal redirect really is An internal redirect is not a 302. Nothing goes to the client. Nginx rewrites the request URI to the fallback and **restarts location matching from the top**. The new URI may well select the same `location /` block, which runs the same `try_files` again. So when `/srv/app/index.html` does not exist: ``` /dashboard -> $uri missing, $uri/ missing -> internal redirect to /index.html /index.html -> $uri missing, $uri/ missing -> internal redirect to /index.html ... x10 ``` Nginx caps internal redirects (ten in the standard build) and then aborts the request with **500 Internal Server Error**, logging: ``` rewrite or internal redirection cycle while internally redirecting to "/index.html" ``` The symptom for users is a blank 500 page; the cause is a **missing file**, which is why the message reads so misleadingly. The first thing to check is not the config syntax but whether `/srv/app/index.html` exists and is readable by the worker user. ## The three usual root causes 1. **The file genuinely is not there** — the frontend build never ran, or the deploy dropped `dist/` somewhere else. 2. **The root is wrong**, often because `root` was set inside `location /` while a different location has its own `root` and the fallback selects that one instead. 3. **`alias` plus `try_files`** — the combination has historically been fragile because the alias prefix is stripped before the tests, and the fallback then re-enters a location that resolves differently. Using `root` in a location that runs `try_files` avoids the whole class. ## Named locations: the terminal fallback A named location begins with `@`, takes no part in request matching, and can be reached only through an internal redirect. That makes it the clean way to end the chain: ```nginx location / { root /srv/app; try_files $uri $uri/ @spa; } location @spa { root /srv/app; try_files /index.html =404; } ``` Now if `index.html` is missing the request ends in an honest **404**, not a redirect loop and not a 500. Only the last argument of `try_files` may be a named location, and named locations may not be nested. ## The other terminal form For locations that serve assets rather than app routes, `=404` is the right fallback: ```nginx location /assets/ { root /srv/app; try_files $uri =404; } ``` This matters beyond tidiness: with the SPA fallback applied to `/assets/`, a mistyped bundle URL returns `index.html` with a 200 and `Content-Type: text/html`, and the browser reports a baffling syntax error instead of a missing file. Splitting assets into their own location with `=404` keeps failures legible. ## How to diagnose it in production 1. Read the error log at `error` level — the cycle message names the exact fallback URI. 2. Resolve that URI by hand against the location's `root` and `ls` the path as the worker user (`nginx` or `www-data`); permissions on a parent directory produce the same 404 as absence. 3. Confirm which location the fallback re-enters, by tagging locations with `set $loc ...` and logging the tag. 4. Make the fallback terminal — `@named` or `=404` — so the next occurrence surfaces as a 404 with an obvious cause rather than a 500.
- What exactly is a named location, and why does routing the fallback into one break the cycle?A named location starts with `@` and is excluded from ordinary request matching — no request path can select it, only an internal redirect from `try_files` or `error_page`. Because the loop needs the fallback to re-select a location that runs the same failing `try_files`, sending it to a named location that ends in `=404` gives the chain a terminal step.
- Why is `try_files $uri $uri/ /index.html;` a bad idea inside a location that serves static assets?A mistyped bundle URL then returns index.html with a 200 and `Content-Type: text/html`. The browser tries to parse HTML as JavaScript or CSS and reports a syntax error, hiding the real cause. Give asset locations their own block with `try_files $uri =404;` so a missing file fails as a missing file.
- How would you confirm the cause is a missing file rather than a configuration error?Take the URI named in the cycle message, resolve it against that location's `root`, and stat the path as the worker user — nginx runs workers as `nginx` or `www-data`, so a directory the deploy left unreadable looks identical to absence. `nginx -t` passing tells you nothing here; the config is valid, the file is not there.
saying these in an interview costs you the question
- Thinks try_files tests the last argument too
- Reads the internal redirect as a 302 to the client
- Blames the config syntax rather than a missing file
- Adds the SPA fallback to asset locations as well
- Believes a named location can be requested directly