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

11.1 The Rewrite Engine

Three directives — return, rewrite, set — run before any content handler, before access rules, before almost everything. That earliness is their power and their danger.

Levels: Advanced ~35 min read

Advanced

What you will learn

  1. The rewrite phase: where these directives run, and why return bypasses auth_basic (the Module 6 receipt, explained).

  2. return — the cheapest redirect and the cheapest response.

  3. rewrite with captures, and the flag you choose on purpose: last vs break (vs plain redirects).

  4. $uri vs $request_uri — the rewritten world vs the client's world, proven in one log line.

  5. rewrite_log on — the engine narrating its own work.


1. The phase model (finally, the full story)

Lesson 6.4 left you a receipt: a $ssl_session_reused flag that survives redirects. rewrite has an equivalent paper trail: the $uri/ $request_uri pair you will meet in §3.

text
 1. server/vhost selection        (listen, server_name)
 2. location matching             (Module 2's algorithm)
 3. REWRITE PHASE                 <- return / rewrite / set live HERE
 4. access phase                  <- auth_basic, allow/deny, auth_request
 5. content handler               <- proxy_pass, fastcgi, static, try_files
 6. log phase

A return in step 3 finishes the request with its own response — steps 4 and 5 never run. No auth challenge, no upstream, no file lookup. This is not a bug; it is the documented phase order, and it is why rewrites and returns are both your fastest tools and your quietest privilege-escalation paths. (Corollary from Module 7: rate limiting runs in the preaccess phase — after rewrite, before access — so a return in rewrite also skips limit_req. Breadcrumbs for your security review.)

2. return — cheapest first

nginx
location = /moved {
    return  301  /destination;
}

No regex, no capture, no second pass through location matching. return supports redirect codes (301/302/307/308) with a URL, or any other code with optional text — return 200 "you have arrived at $uri\n" is a complete, upstream-less response. The receipt (real capture):

text
HTTP/1.1 301 Moved Permanently
Server: nginx/1.30.5
...
modern page for uri=/destination        <- after following it

House rule that scales: if you don't need to inspect or capture the URI, return . Only reach for rewrite when the regex is load-bearing.

3. rewrite, captures, and the two flags that matter

nginx
location /legacy/ {
    rewrite  ^/legacy/(.+)$  /modern/$1  last;
}
location /modern/ {
    add_header  X-Modern-Path  "yes";
    return  200  "modern page for uri=$uri original=$request_uri\n";
}

The regex captures (.+) from /legacy/widget and builds /modern/widget. The flag decides what happens next — and the choice is the whole lesson:

Flag

Meaning

Consequence

(none)

finish the rewrite directives; if the URI changed, re-run location matching

internal — the client sees only the final result (no Location header; a scheme-prefixed replacement is the one automatic exception)

last

stop this location, re-run location matching with the new URI

a different location usually wins; internal, client sees only the result

break

stop rewrite processing, stay in this location

the content handler serves the rewritten URI from this location's root

The difference, proven with the same regex pointed at the same file (real captures):

nginx
location /breakdemo/ {
    root  www/main;
    rewrite  ^/breakdemo/(.*)$  /docs/$1  break;   # stays HERE
}
location /lastdemo/ {
    rewrite  ^/lastdemo/(.*)$  /modern/$1  last;   # re-matches
}
text
break -> 200  (handbook.pdf served FROM /breakdemo/, via its own root)
last  -> modern page for uri=/modern/handbook.pdf original=/lastdemo/handbook.pdf

break served the file's bytes because the static handler inside /breakdemo/ looked up the rewritten /docs/handbook.pdf against root www/main. last restarted matching, /modern/ won, and its return answered instead.

Two sharp edges to keep in mind:

  • break halts rewrite processing in context — a set or return after it in the same block never runs. (The /breakdemo/ example above deliberately has no directives after its rewrite.)

  • last can loop: the engine re-matching a URI that keeps matching the same rewrite re-runs it, up to 10 cycles, then 500. The /self/ shape (a regex that matches its own output) is the classic way to meet the limit.

And the /lastdemo/handbook.pdf receipt above shows the pair of variables that keep you sane: $uri is the current, rewritten URI; $request_uri is the original, verbatim request (path + args), immune to every rewrite. Logging both — $uri $request_uri side by side — turns every rewrite bug into a one-line diagnosis.

4. The engine narrates itself

One directive turns the rewrite phase into an observable system:

nginx
location /shuttle/ {
    rewrite_log  on;
    rewrite  ^/shuttle/(.*)$  /modern/$1  last;
}

rewrite_log writes to the error log at notice level — which is why the example config's error_log runs notice while production usually stays at warn. The narration from a real run:

text
2026/09/23 01:04:44 [notice] 3430#3430: *43 rewritten data: "/modern/thing", args: "",
       client: 127.0.0.1, server: rewrite.example.test,
       request: "GET /shuttle/thing HTTP/1.1", host: "127.0.0.1:9300"

That single line answers: what came in, what it became, whether args survived, which server/location context ran it. When a rewrite misbehaves in production, rewrite_log on at notice in the one suspicious location is the surgical instrument — not a global log-level change.

5. Why set belongs to this family

set $tier free; looks like a variable assignment but runs in the rewrite phase, before content handlers exist. That's why it can feed add_header and proxy_set_header (both evaluated later), and why an if containing only set/ return behaves predictably — all of it is one phase, evaluated in order. Lesson 11.2 builds the whole if debate on this fact.

Try it yourself

  • nginx -t && nginx -s reload — the configs above run on :9300 in this lesson, verbatim

  • curl -i http://127.0.0.1:9300/legacy/widget — no Location header, body shows both URIs

  • tail /var/log/nginx/error.log | grep rewritten — the narration receipts

  • Flip the /breakdemo/ flag to last and nginx -s reload — watch it start answering like /lastdemo/

Q&A

Q: rewrite without a flag — when does it still produce an external redirect? A: When the replacement string itself starts with http://, https://, or $scheme — nginx answers with a 302 automatically (301 still requires the permanent flag). A plain-path replacement never redirects: it re-matches internally, and the client's bookmark must change case — URL rebranding, trailing-slash enforcement — is exactly when you should reach for permanent (301) for stable changes or redirect (302) while experimenting; clients cache 301s aggressively.

Q: Why did my query string survive the rewrite? A: The replacement string only replaces the path; args append automatically unless you end the replacement with ? (which deliberately drops them).

Q: Can I rewrite to a different server block? A: last re-runs location matching in the same server. Crossing servers means a real redirect — or internal proxied hops, which is X-Accel-Redirect's territory (Lesson 11.5).

Takeaways

  • Rewrite-family directives run in the rewrite phase — before access rules and content handlers — so a bare return skips auth_basic, limit_req, and the rest. Powerful; audit accordingly.

  • Flags are intent: last = re-match locations, break = stay here and serve the rewritten URI, none = client-visible redirect. break also stops any later rewrite directives.

  • $uri is the rewritten world, $request_uri the client's — log both and rewrites stop being mysterious.

  • rewrite_log on + error_log notice gives you per-rewrite receipts straight from the engine.

Was this page helpful?

Mark this learning resource complete to track your learning progress.