skip to content

.htaccess & mod_rewrite

Apache lets a directory carry its own config file that shared-hosting users can edit without touching the server, and mod_rewrite turns that into a small URL-rewriting language. Interviewers ask it because rewrite rules are where real Apache bugs live, and because explaining why .htaccess is a per-request filesystem cost shows you understand what the server does on every hit.

on this pageshow

questions

6

In Apache mod_rewrite, what is the difference between `RewriteRule ^old/(.*)$ /new/$1 [L]` and the same rule written `[R=301,L]`?

level: juniorimportance: must knowfreq 72%

answer

  1. one request or two
  2. who learns about the change
  3. Location header or nothing
  4. L means last, not redirect
  5. scheme in the substitution forces it

basics

~20 s

With [L] alone the rewrite is internal: Apache serves the new path itself and the client never learns anything changed. With [R=301,L] Apache sends a redirect response carrying a Location header, so the browser issues a second request and its address bar updates.

solid answer

~50 s

`[L]` performs an *internal* rewrite. The URL is remapped inside the server, one request is served, the client sees the original URL in its address bar and has no idea the mapping happened. `[R]` makes it an *external* redirect: httpd answers with a 3xx status and a `Location` header pointing at the substitution, and the browser then makes a second, separate request for the new URL. `R` with no value defaults to 302; `R=301` asks for a permanent one. So the choice is really about who should know: use `[L]` for internal routing you never want exposed — a front controller, a pretty URL mapped onto a real file — and use `[R=…]` when the canonical address genuinely changed and you want clients, caches and search engines to adopt it. One trap: if the substitution starts with a scheme and host such as `https://example.com/x`, mod_rewrite forces an external redirect whether or not you wrote `R`.

code

apache · 10 lines
apache
RewriteEngine On

# Silent internal routing: address bar keeps showing /products/1234
RewriteRule ^products/([0-9]+)$ /product.php?id=$1 [L]

# Permanent move: client is redirected and updates its address bar
RewriteRule ^blog/(.*)$ /articles/$1 [R=301,L]

# Implicit redirect: an absolute target redirects even without R
RewriteRule ^shop/(.*)$ https://shop.example.com/$1 [L]

go deeper

for a junior

Be able to say plainly that [L] keeps everything inside the server while [R] sends the client a redirect it must follow, and that the address bar only changes in the second case.

for a middle

Explain that L only stops rule processing and carries no redirect meaning, that bare [R] is 302, and that a substitution starting with a scheme redirects implicitly.

for a senior

Show the diagnostic instinct: check the status line with curl before touching config, and account for the extra round trip an external redirect adds on every affected request.

for a principal

Own the canonicalisation policy — which URL forms are permanent redirects versus silent rewrites — so that redirect chains do not accumulate across teams and years.

## Two different things called "rewriting" mod_rewrite can do two operations that look nearly identical in the config file and are completely different on the wire. An **internal rewrite** changes which resource the server maps this request to. It happens entirely inside httpd. One request arrives, one response goes back, and the URL the client asked for is the URL the client still believes in. An **external redirect** ends the current request with a 3xx status and a `Location` header. The client then decides to make a second request to the new URL. Two requests, two round trips, and the client's address bar now shows the new address. ```apache RewriteEngine On # Internal: /old/report.html is served from /new/report.html, silently. RewriteRule ^old/(.*)$ /new/$1 [L] # External: the client is told to go and ask for /new/report.html itself. RewriteRule ^old/(.*)$ /new/$1 [R=301,L] ``` ## What each flag means `L` stands for "last": stop processing further rules in this ruleset for this pass. It says nothing at all about redirecting. On its own it simply means "this substitution is the answer, don't keep matching". `R` stands for "redirect", and it is what turns the substitution into a response rather than a remapping. Written bare, `[R]`, it uses 302. You may name the status explicitly — `R=301`, `R=302`, `R=303`, `R=307` — and mod_rewrite will also accept other 3xx values. What each of those status codes means to a browser or a cache is HTTP's business, not the rewrite engine's; what mod_rewrite guarantees is only that this is the status it will send with the `Location` header it builds. The two are almost always written together as `[R=301,L]`, because once you have decided to answer with a redirect, continuing to evaluate more rules against the same request is rarely what you want. ## The implicit redirect The substitution's shape can force the decision. If the substitution begins with a scheme and hostname — `http://`, `https://`, or `//host/path` — mod_rewrite cannot serve it internally, because the target is a different origin. It issues an external redirect even without `R`. This surprises people who write a rule pointing at their own site's absolute URL, expecting a silent internal remap, and instead find every request bouncing through a 302. The converse trap is a rule that *should* be a redirect but was written relative, so users keep seeing the old URL and nothing ever migrates. ## Choosing between them Use an internal rewrite when the external URL is the contract and the internal path is an implementation detail: - A front controller: every unmatched URL is served by `/index.php`, and the client never sees `index.php` in the address bar. - Clean URLs: `/products/1234` is really `/product.php?id=1234`, and you do not want the query-string form to leak or to become the address people bookmark. - Serving a different file per locale or per device from one URL. Use an external redirect when the address itself has changed and you want the world to update: - A moved or renamed section, where old links should end up at the new canonical URL. - Canonicalisation — one host, one trailing-slash form, one scheme — where having two live URLs for the same content is the problem you are solving. - Anything a search engine or a bookmark should learn about. ## Cost and visibility An internal rewrite is free in network terms: no extra round trip. An external redirect costs a full extra request, which on a mobile connection is tens to hundreds of milliseconds, and chains of them multiply that. That is a genuine argument for keeping internal routing internal. Visibility cuts the other way when you are debugging. An internal rewrite is invisible from outside: `curl -v` shows a single 200 for the original URL, so you cannot tell from the client whether a rule fired. `curl -sI https://example.com/old/x` immediately shows a redirect, status and `Location` and all. When an internal rule misbehaves you need the server side — raise `LogLevel` to a trace level (httpd 2.4 removed the old `RewriteLog` directive) and read the rewrite trace in the error log. One more consequence worth internalising: because an internal rewrite re-enters the request-mapping machinery, a per-directory rule whose output still matches its own pattern will loop. An external redirect loops too, but the browser stops it and shows a redirect-loop error; the internal version burns server-side recursion until httpd gives up and returns 500.

  • If you write [R] with no status code, what does mod_rewrite send?
    302. Bare `[R]` is equivalent to `[R=302]` — mod_rewrite's default is the temporary form, on the principle that a temporary redirect is the safer thing to get wrong. If you mean the move to be permanent you must say `R=301` (or `R=308`) explicitly. Getting this wrong is common and one-directional in its pain: a wrongly-permanent redirect is cached by clients long after you fix the config.
  • How can you tell from outside the server whether a URL was rewritten internally or redirected?
    Ask for it and look at the status line — `curl -sI https://example.com/old/x`. A redirect shows a 3xx status and a `Location` header, and `curl -L` will visibly follow it. An internal rewrite shows a single 200 for the URL you asked for, with nothing to distinguish it from a file that really lives there. To confirm an internal rule fired you need the server side: raise `LogLevel` to a trace level and read the rewrite trace.
  • Why might a rule you wrote without R still redirect the client?
    Because the substitution begins with a scheme and host — `https://…` or `//host/…`. mod_rewrite cannot map another origin to a local file, so it issues an external redirect regardless of flags. If you intended a silent remap within your own site, write the substitution as an absolute *path* (`/new/$1`) rather than an absolute URL.

saying these in an interview costs you the question

  • Says [L] updates the browser's address bar
  • Thinks bare [R] sends 301
  • Believes [L] means "redirect last"
  • Assumes an internal rewrite costs an extra round trip
  • Writes an absolute URL and expects a silent rewrite

context

open as a page

In Apache httpd, what work does the server do on every single request when .htaccess files are enabled, and how does the AllowOverride directive change it?

level: middleimportance: must knowfreq 66%

basics

~20 s

Apache httpd looks for an .htaccess file in every directory along the path to the requested file, on every request, and re-parses each one it finds. AllowOverride None removes that lookup entirely; any other value enables it.

open as a page

An Apache rewrite rule that worked in the main server configuration stops matching after you move it into an .htaccess file: `RewriteRule ^/products/(.*)$ /catalog/$1 [L]` now never fires. Why, and what is RewriteBase for?

level: middleimportance: should knowfreq 54%

basics

~20 s

In an .htaccess file, mod_rewrite strips the directory's own path prefix and the leading slash before matching, so a pattern beginning with ^/ can never match. RewriteBase declares the URL path that directory corresponds to, so relative substitutions expand correctly.

open as a page

An Apache site whose .htaccess routes everything to a front controller starts returning 500s, and the error log reports that the request exceeded the limit of 10 internal redirects. What causes that loop, and how do you write the rule so it cannot happen?

level: seniorimportance: should knowfreq 46%

basics

~20 s

A per-directory rewrite is re-injected into the request-mapping cycle, so the .htaccess runs again against its own output. If the rule matches the rewritten URL too, it loops until httpd hits LimitInternalRecursion and returns 500. Guard the rule so it cannot match its result.

open as a page

You own an Apache fleet where every application ships its own .htaccess file. How would you decide whether to keep AllowOverride enabled or move those rules into the server configuration?

level: principalimportance: should knowfreq 33%

basics

~20 s

Decide on who needs to change rules without a server reload. Where teams control the server config, hoist rules into <Directory> blocks and set AllowOverride None: config is parsed once, reviewable and syntax-checked. Keep .htaccess only where delegation to untrusted or reload-less tenants is the actual requirement.

open as a page

In Apache mod_rewrite, what happens to a request's original query string when a RewriteRule substitution contains a query string of its own, and what does the QSA flag change?

level: middleimportance: nice to knowfreq 42%

basics

~20 s

By default the original query string survives only if the substitution has none of its own; a substitution containing a question mark replaces it. The QSA flag appends the original to the new one instead, and QSD discards it deliberately.

open as a page