skip to content

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%

answer

  1. context changes what the pattern sees
  2. prefix and leading slash removed
  3. a pattern with ^/ can never match
  4. relative substitution needs a base
  5. URL path may differ from disk path

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.

solid answer

~50 s

mod_rewrite runs in two different contexts. In the server configuration it matches against the full URL path, so `^/products/(.*)$` is right. In per-directory context — an `.htaccess` file, or a `<Directory>` block — httpd first removes the directory's own prefix and the leading slash, so a request for `/products/widget` handled by `/var/www/html/products/.htaccess` is matched as just `widget`. A pattern starting with `^/` therefore matches nothing, which is exactly the silent failure described. The fix is to drop the slash and the prefix: `RewriteRule ^(.*)$ /catalog/$1 [L]`. `RewriteBase` is the other half of per-directory awkwardness: when the substitution is *relative*, mod_rewrite must prepend the URL path this directory is reachable at, and its guess is derived from the filesystem path. That guess is wrong whenever the URL path and the filesystem path diverge — an aliased directory, a symlinked tree — and `RewriteBase /shop/` states it explicitly.

code

apache · 11 lines
apache
# /srv/apps/shop/.htaccess — served at https://example.com/shop/ via Alias
RewriteEngine On
Options +FollowSymLinks

# Required because the URL path (/shop/) differs from the disk path.
RewriteBase /shop/

# Pattern sees "cart" for a request to /shop/cart — no leading slash.
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]

go deeper

for a junior

Remember that patterns in .htaccess do not start with a slash, because Apache has already removed the directory prefix before matching.

for a middle

Explain both halves: the prefix stripping on the pattern side, and RewriteBase on the substitution side when the URL path and the disk path diverge.

for a senior

Reach for the rewrite trace in the error log rather than guessing, and know the prerequisites that silently disable per-directory rules — AllowOverride FileInfo and FollowSymLinks.

for a principal

Set a house convention — absolute substitution paths, rules in server config where possible — so that per-directory context stops being a recurring source of deploy-time surprises.

## Two contexts, two things being matched mod_rewrite rules can live in the server or virtual host configuration, or in per-directory context: an `.htaccess` file or a `<Directory>` block. The directives look identical. What the pattern is matched against does not. In **server context**, mod_rewrite runs early, during URL-to-filename translation, and the rule matches the URL path exactly as it appeared in the request: `/products/widget`, leading slash and all. In **per-directory context**, mod_rewrite runs later — after httpd has already worked out which directory the request maps into. By then the directory prefix is redundant, so mod_rewrite removes it, along with the leading slash. In `/var/www/html/products/.htaccess`, with a document root of `/var/www/html`, a request for `/products/widget` is matched against the string `widget`. That is the entire explanation for the reported symptom. `^/products/(.*)$` demands a leading slash the string does not have, and demands a prefix that has already been removed. It matches nothing, and mod_rewrite reports nothing, because a rule that does not match is not an error. The rule is simply inert. ```apache # /var/www/html/products/.htaccess RewriteEngine On # Never matches: the leading slash and the prefix are gone by now. # RewriteRule ^/products/(.*)$ /catalog/$1 [L] # Matches: the pattern sees "widget" for a request to /products/widget. RewriteRule ^(.*)$ /catalog/$1 [L] ``` A related consequence: because per-directory rules run after translation, they are working with a URL that has already been mapped to a file. This is why `%{REQUEST_FILENAME}` is meaningful in `.htaccess` guards and why the `-f` and `-d` tests are the idiomatic way to say "only rewrite if there is no real file here". ## What RewriteBase actually does The stripped prefix has to come back at some point, because the substitution has to name something the server can map. mod_rewrite handles this by prepending a base to any substitution that is *relative* — one that does not begin with a slash or a scheme. When you do not set `RewriteBase`, httpd derives that base from the physical directory's path relative to the document root. For an ordinary tree that is right, and you never need the directive. It becomes wrong when the URL path and the filesystem path are not the same string: - The directory is reached through an `Alias`, so `/shop/` on the URL side is `/srv/apps/shop/` on disk. - The tree is symlinked into the document root from elsewhere. - The application is mounted under a path prefix that does not correspond to a real directory of that name. In those cases a relative substitution expands against a base that does not exist as a URL, and the result is a 404, or the internal path leaking into a redirect — the classic symptom of the server's filesystem layout appearing in the browser's address bar. ```apache # /srv/apps/shop/.htaccess, reachable at https://example.com/shop/ RewriteEngine On RewriteBase /shop/ RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^ index.php [L] ``` Here the substitution `index.php` is relative, so it becomes `/shop/index.php`. Without the `RewriteBase`, mod_rewrite would try to build the base from `/srv/apps/shop/` and get something that is not a URL on this site at all. The sidestep is worth knowing: write substitutions as absolute URL paths (`/shop/index.php`) and `RewriteBase` becomes unnecessary, because nothing needs a base prepended. Many teams standardise on that precisely to remove a class of subtle breakage. `RewriteBase` earns its place when the same rule set must be droppable into different mount points, where a relative substitution plus one declared base is genuinely more portable than absolute paths edited per deployment. ## The other per-directory prerequisites Two more things must be true before per-directory rules run at all, and both produce "my rules do nothing" reports of their own: - **`AllowOverride` must permit `FileInfo`** for that tree, or httpd never reads the file — or, if it reads it and the directive is not permitted, fails the request with 500 and logs the directive as not allowed. - **`Options FollowSymLinks` (or `SymLinksIfOwnerMatch`) must be enabled.** With both off, mod_rewrite refuses to run per-directory rules and logs that the `RewriteRule` directive is forbidden because those options are off. The reason is security: a per-directory rewrite can point at a path outside the tree, and the symlink options are the existing control for that class of escape. Finally, rewrite configuration is not inherited the way most directives are. If a child directory has its own `.htaccess` with `RewriteEngine On`, the parent's per-directory rules do not apply to it unless you ask, with `RewriteOptions Inherit` (or `InheritBefore`). A rule that "stops working in one subdirectory only" is very often this. ## How to debug it in practice Do not guess at the matched string — read it. httpd 2.4 removed `RewriteLog`; you raise `LogLevel` to a trace level instead, and the error log then prints, per rule, the string mod_rewrite matched against and what it substituted. Seeing `widget` where you expected `/products/widget` ends the argument in one line, and it is the fastest route through every per-directory surprise in this topic.

  • When can you get away without RewriteBase entirely?
    Two cases. First, when the directory's URL path matches its path under the document root — mod_rewrite's derived base is then correct and the directive is redundant. Second, and more robustly, when every substitution is written as an absolute URL path such as `/shop/index.php`, because nothing relative remains for a base to be prepended to. Many teams adopt the second rule precisely to delete the whole class of base-related bugs.
  • A rewrite rule in a parent directory's .htaccess stops applying inside one subdirectory. What is the likely cause?
    That subdirectory has its own `.htaccess` with `RewriteEngine On`. Per-directory rewrite configuration is not inherited by default: once a child enables the engine, the parent's rules are dropped for that tree. Add `RewriteOptions Inherit` (or `InheritBefore`, to run the parent's rules first) in the child, or hoist the shared rules into the server config where inheritance is not in question.
  • How do you see the exact string a per-directory rule is matching against?
    Raise the error log's trace level with `LogLevel` — httpd 2.4 removed the separate `RewriteLog` directive — and mod_rewrite prints, per rule, the input string it tested and the substitution it produced. That is how you confirm in one line that the pattern is being offered `widget` rather than `/products/widget`, instead of inferring it from a 404.

saying these in an interview costs you the question

  • Keeps the leading slash in a per-directory pattern
  • Thinks RewriteBase changes the filesystem path
  • Believes RewriteBase is always required
  • Assumes .htaccess rules match the full request URI
  • Says a non-matching rule would log an error

context