Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

3.5 Error Pages, index, autoindex, and Redirects

The last 10% of a static site is what users remember when things go wrong. NGINX's finishing tools — and one Location-header behavior that surprises everyone once.

Levels: Beginner ~35 min read

Basic

What you will learn

  1. error_page in full: single codes, code lists, =code rewriting, and @named targets.

  2. index — the quiet machinery behind / — and autoindex as a deliberate, format-choosing feature.

  3. Redirects with return (no rewrite needed): 301/302/307/308 semantics.

  4. The Location header surprise: absolute by default, relative with absolute_redirect off — captured live.

  5. Dotfile defense: the one regex location every public site should carry.


1. error_page — owning the failure moments

nginx
error_page  404          /errors/404.html;
error_page  503 502 504  /errors/50x.html;    # one page, many codes

With both lines in place, the wiring is easy to prove end to end — the custom page, not the default gray one, answers a missing file:

bash
$ curl -s -H 'Host: static.example.test' http://127.0.0.1:8300/no/such/file
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>404 — gone fishing</title></head>
<body style="font-family:monospace;background:#111;color:#0f8;padding:3rem">
<h1>404</h1>
<p>Custom 404 page — if you can read this, error_page is wired correctly.</p>

The mechanics that make it more than a cosmetic:

  • The error URI is an internal redirect. /errors/404.html re-enters location matching (Lesson 3.2's mechanism) — so the error file can live anywhere, under any policy, even behind internal (Module 11) or proxied to an app that renders branded errors.

  • The original status survives — the response is still 404; the page content changed, not the semantics. (Search engines and monitors depend on this.)

  • = rewrites the status when you truly need to: error_page 404 =200 /shell.html; answers 200 with your SPA shell — the "soft 404" pattern. Use sparingly and knowingly: monitoring blind spots are the price.

  • error_page targets can be @named or full URLs ( error_page 403 https://… issues a redirect) — the full toolkit from Lesson 3.2 returns.

  • Proxied errors don't trigger it by default. A backend's 502/503 passes through unless proxy_intercept_errors on; (Lesson 4.6 — filed here so the mental slot exists).

2. index and autoindex

index — what / really means

A request for a directory URI is a request for the directory's index file. NGINX walks your list, left to right, serving the first that exists:

nginx
index  index.html index.htm;        # the world's default
# index  index.php index.html;      # the classic PHP-app order

The machinery is an internal redirect per candidate (Module 11 shows the subrequest economics; for now: it's cheap, cached in the location's resolution). The two production notes: the file is chosen by existence, not preference — if both exist, the leftmost wins even if the rightmost is newer; and index interacts with try_files $uri/ from Lesson 3.2 — the $uri/ arm hands control to exactly this walk.

autoindex — listings on purpose

With no index file present, a directory request 403s (Lesson 3.2's trap) — unless you choose to show a listing:

nginx
location /data/ {
    autoindex         on;
    autoindex_format  html;    # html (default) | json | xml | plain
}

Our example /data/ is a live listing (trimmed):

bash
$ curl -s -H 'Host: static.example.test' http://127.0.0.1:8300/data/
<html>
<head><title>Index of /data/</title></head>
<body>
<h1>Index of /data/</h1><hr><pre><a href="../">../</a>
<a href="users.json">users.json</a>    22-Sep-2026 18:54    68
</pre><hr></body>
</html>

Professional stances: most public sites leave it off (information disclosure — backup files, dotfiles in listings, internal naming). It shines for internal tooling, release archives, and datasets — where autoindex_format json becomes an API for directory contents (automation-friendly: jq-able listings). autoindex_exact_size off switches to human units; autoindex_localtime on for local timestamps.

3. Redirects without rewrite

Most "redirects" in a config are static URI→URI hops, and the rewrite engine (Module 11) is overkill for those. return does it in the rewrite phase, before any content work:

nginx
location = /old    { return 301 /about.html; }   # permanent
location = /moved  { return 308 /about.html; }   # permanent, method-safe
location = /teapot { return 418 "short and stout\n"; }   # status + body

The status-code decoder ring:

Code

Meaning

Method preserved?

Cacheable

301

moved permanently

❌ (may flip POST→GET)

yes

302

moved temporarily

❌

not by default

307

temporary

✅

not by default

308

permanent

✅

yes

The 307/308 pair exists precisely because 301/302 were defined in a browser era that rewrote methods — and forms that POST get silently converted to GET by 301, losing bodies. Rule of thumb: user-facing permanent moves → 308; anything else → 307; use 301/302 only for compatibility with ancient clients (and know they'll convert your POSTs).

return also takes full URLs ( return 301 https://…$request_uri; — Module 6's HTTP→HTTPS jump) and can end a chain with any status, body optional — the 418 teapot above being the friendly proof.

4. The Location surprise (in practice)

Ask NGINX to redirect to a relative URI — as our /old does — and look at what goes on the wire:

bash
$ curl -sI -H 'Host: static.example.test' http://127.0.0.1:8300/old | grep -i location
Location: http://static.example.test:8300/about.html

NGINX absolutizes the target by default: scheme + Host header + your relative URI. Standards-compliant (RFC 7231 permits both forms), and usually what browsers want — but it causes two real-world problems:

  1. Host leakage in multi-tenant setups — an internal alias or a request with a forged Host gets echoed into the redirect.

  2. Fronted/proxied deployments — behind an LB that speaks a different public name, the absolute URL points at the wrong origin.

The fix is one directive — /moved demonstrates it side-by-side with /old:

nginx
location = /old   { absolute_redirect on;  return 301 /about.html; }
absolute_redirect off;                                  # server-level default
location = /moved { return 308 /about.html; }
bash
$ curl -sI -H 'Host: static.example.test' http://127.0.0.1:8300/moved | grep -i location
Location: /about.html

Relative Location, modern-clients-only, plays perfectly with fronted origins. Its siblings server_name_in_redirect off; (default: use the request's Host, not the config's server_name) and port_in_redirect off; (drop :port when behind a port-translating LB) solve the adjacent variants of the same class of problem.

5. Dotfile defense

The one regex every public vhost deserves:

nginx
location ~ /\.(?!well-known) {
    deny all;                    # 403 for .git, .env, .htaccess, .DS_Store…
}

The output below proves both halves:

bash
$ curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: static.example.test' \
       http://127.0.0.1:8300/.hidden/secret.txt
403
$ curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: static.example.test' \
       http://127.0.0.1:8300/secret.txt
200

Why the (?!well-known) carve-out: ACME challenges (Module 6's Let's Encrypt renewals) must answer under /.well-known/acme-challenge/ while everything else dot-prefixed stays dark. And note what the regex doesn't match: /secret.txt — the pattern requires the dot immediately after a slash. Precise patterns beat broad ones; a blanket location ~ /\. version of this rule once broke half the internet's ACME renewals.

( deny here is an access-module directive — the full allow/deny/geo arsenal lands in Lesson 6.5. This is its cameo.)

Try it yourself

  1. Tour the error pages. Hit /no/such/file (404 custom), then break it: rename errors/404.html to .bak, reload… actually — does it even need a reload? (No — static files are re-read per request; the fallback-to-built-in behavior kicks in immediately. Verify.)

  2. Status rewriting. Add error_page 404 =200 /index.html;, reload, check the status of a missing file with curl -s -o /dev/null -w '%{http_code}'. Feel the monitoring trade-off. Restore.

  3. The redirect A/B. Compare /old vs /moved Location headers (§4); then flip port_in_redirect off; and watch the :8300 vanish from /old's absolute URL.

  4. Autoindex formats. Set autoindex_format json, reload, curl …/data/ | jq . — you've built a directory API in one line.

  5. Try to bypass the dotfile rule. /hidden/secret.txt (no dot)? /.hidden (no trailing path)? /%2Ehidden/secret.txt (percent-encoded dot — recall Lesson 2.3: decoded before matching)? Predict, test, explain.

Q&A

Q1. Why does a custom error_page 404 still return status 404, and when would you want otherwise?

Answer

error_page swaps the body, not the status — clients, monitors, and crawlers need truthful semantics. You opt out deliberately with error_page 404 =200 … for "soft 404" UX (SPA shells), accepting that monitoring must then watch something other than status codes.

Q2. A form POSTs to /subscribe; you must redirect permanently without losing the POST body. Which code, and why?

Answer

308 — the method-preserving permanent redirect. A 301 invites the browser to convert the POST to a GET, dropping the body. (307 if the move is temporary.)

Q3. Behind a load balancer, /old's redirects point users to http://internal-name:8300/…. Name the two fastest fixes.

Answer

absolute_redirect off; (emit relative Location — the modern default choice) or fix the host source: ensure the LB passes the public Host and X-Forwarded-Proto, and set port_in_redirect off; if the public edge hides the port. All three levers are this lesson's §4.

Q4. What does (?!well-known) buy, and what class of bug does its absence cause in the wild?

Answer

It exempts /.well-known/ (ACME challenges, security.txt) from dotfile denial. Blanket dot-denial without the carve-out breaks TLS certificate renewals fleet-wide — a legendary outage shape, and why the example config ships the carve-out as the default teaching shape.

Takeaways

  • error_page = internal redirect with status semantics: one page, many codes; =code to rewrite; @named and proxied targets are fair game.

  • index walks a list on directory URIs; autoindex turns directories into listings (or a JSON API) — on purpose or not at all.

  • Redirects: return beats rewrite for static hops; 307/308 preserve methods; absolute_redirect off + port_in_redirect off tame the Location-header class of fronted-deployment bugs.

  • Dotfile defense is one anchored regex with a .well-known carve-out — precise patterns, or you break ACME.

  • Module 3 complete: URIs map to files, files to content types, content to wire — fast, and finished like a professional.

Next: Module 4 — the architecture shift that makes NGINX the internet's favorite front door: reverse proxying. proxy_pass and the trailing-slash trap, header hygiene, FastCGI/uWSGI, WebSockets, and gRPC.

Was this page helpful?

Mark this learning resource complete to track your learning progress.