Apache tool

.htaccess tester

Paste your rules, give them a URL, and watch which line wins. Every condition is evaluated in front of you, so a rule that quietly never matches stops being a mystery.

Rules RewriteEngine, RewriteBase, RewriteCond, RewriteRule, Redirect and RedirectMatch are simulated. Everything else is listed and left alone.
Request Apache strips this prefix before matching. Leave it as / for a file in the document root.
One per line. A trailing slash means a directory. This is how -f and -d get answered, since no tester can see your filesystem.

Why a rule refuses to match

The leading slash

This is the one that costs people an afternoon. Apache strips the directory prefix from the path before a per-directory ruleset ever sees it. A .htaccess in the document root matches against blog/hello, not /blog/hello, so ^/blog/ matches nothing at all. Move the same rules into httpd.conf and the slash becomes mandatory, which is why copied snippets so often behave differently from where they came from.

L does not mean last

In a .htaccess, [L] ends the current sweep through the file. Apache then restarts the whole ruleset with the rewritten URL. A rule that rewrites to something it also matches will run until Apache hits its internal redirect limit and answers 500. Guard the rule with RewriteCond %{REQUEST_FILENAME} !-f, or use [END], which stops the restart as well.

Conditions only run after the pattern matches

A RewriteCond is not a filter that runs first. Apache tests the RewriteRule pattern, and only if it matches does it work back through the conditions stacked above it. That ordering matters for %1 backreferences, which come from the last condition that matched, while $1 comes from the rule pattern.

Conditions default to AND

Stack three conditions and all three must hold. [OR] binds a condition to the one after it, so a run of conditions ending in a plain one forms a single OR group, and groups are combined with AND. Mixing the two without thinking about that grouping produces rules that fire on traffic you meant to exclude.

Flags worth knowing

FlagWhat it does
[L]Stop this pass. Apache restarts the file with the new URL.
[END]Stop rewriting entirely, restart included. Apache 2.4 and later.
[R=301]Send the browser a real redirect. The address bar changes. Without a code you get 302.
[NC]Match without regard to case.
[QSA]Keep the original query string and append the new one.
[QSD]Throw the original query string away.
[NE]Do not escape characters in the substitution. Needed for anchors and pre-encoded paths.
[F]403 Forbidden. Implies [L].
[G]410 Gone. Implies [L].
[C]Chain: if the next rule fails, the rest of the chain is skipped.
[S=n]Skip the next n rules when this one matches.
[E=VAR:val]Set an environment variable, readable later as %{ENV:VAR}.
[P]Proxy the request through mod_proxy instead of redirecting.

Rules people actually write

Force https

RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]

Behind Cloudflare or any other proxy that terminates TLS, %{HTTPS} reads off even on an https request, and this rule loops. Test %{HTTP:X-Forwarded-Proto} instead.

Strip www

RewriteEngine On
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ https://%1/$1 [R=301,L]

The %1 is the hostname captured by the condition. Getting $1 and %1 the wrong way round here sends every request to a domain that does not exist.

Front controller

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php?page=$1 [QSA,L]

Drop either condition and the rewritten /index.php matches the rule again on the restart. List /index.php in the paths box above to see the guard doing its job.

Clean URL for one page

RewriteRule ^pricing/?$ /pricing.php [L,QSA]

The /? accepts the trailing slash without a second rule and without a redirect.

What this tester does and does not know

It parses the rules and walks them exactly as Apache would, up to the ten internal redirects Apache allows before it gives up. Nothing is fetched from your site and nothing is written to disk. Your rules are not stored or logged, which is also why there is no history to come back to.

Filesystem tests are answered from the paths you list, not from any real server, so -f and -d are only as accurate as that list. -U and -F fire real subrequests on a live server and are treated as false here. RewriteMap is not simulated. Directives that shape headers, access or caching are listed in the trace and then left alone, since they do not decide which URL gets served.

Longer walkthrough of the syntax, with the parts of .htaccess that have nothing to do with rewriting: the htaccess tutorial.