htaccess Redirect Tester
runs in your browserPaste your .htaccess and a list of URLs to see the whole redirect chain, which rule and line fired, and where it loops. Apache only. Nothing is fetched.
Drop a file here, or .
Drop an .htaccess or a vhost .conf
An internal rewrite changes the path and carries on in the same request; a 301 or 302 ends it and the browser starts again from the top. Apache only — read in your browser, nothing is fetched.
Rules
URLs to test
about this tool
Paste the rules out of your .htaccess, paste the URLs you are unsure about,
and see the whole chain: every hop, the status code at each one, which rule on
which line did it, and where it settles — or loops.
A rewrite and a redirect are not the same thing
This is the distinction the whole tool is built around, and the one that costs people an afternoon.
An internal rewrite changes the path and carries on inside the same
request. The browser never hears about it; the address bar does not move. That
is RewriteRule without [R].
An external redirect sends a 301 or 302 back to the browser, which then
makes a completely new request — and that request runs your rules again from
the top. That is [R], or a substitution that is an absolute URL.
Miss the second half and redirect loops look impossible. Two rules that are each obviously correct will bounce a request between them forever, because the output of one re-enters the file at line 1 and matches the other.
One thing this gets right that most testers do not: once a redirect leaves for another host, your rules stop applying. A domain migration is one hop and then somebody else's problem — not, as a naive re-run of the same file suggests, a loop.
The leading slash, and the folder
In an .htaccess, the path a RewriteRule sees has the folder the file sits
in — and the leading slash — removed. So at the document root:
RewriteRule ^/team$ /about/team [R=301,L] # never fires
RewriteRule ^team$ /about/team [R=301,L] # correct
And in /shop/.htaccess, the pattern sees old, not /shop/old. Set the
folder above and the answer changes accordingly; leave it at / and the rules
are read as a document-root file.
Move the identical file into a VirtualHost and it reverses — there the path
keeps its slash. Rules copied off a forum are usually written for one context
and pasted into the other, and the symptom is simply that nothing happens.
Redirect and RedirectMatch are unaffected. Those are mod_alias, which
always matches the full path with its slash — a difference that trips up even
people who know about the RewriteRule case.
Apache only
nginx is not supported, on purpose. Modelling it means its phase order, its
four-way location precedence — exact, then longest ^~, then regex in file
order, then longest prefix — and its variable expansion. return 301 https://$host$request_uri needs all three at once, and a half-model of it
reported that line, the most common redirect directive there is, as a loop.
Better to do one server properly.
What it evaluates, and what it cannot
RewriteCond is checked where the answer is in the URL: HTTPS, HTTP_HOST,
REQUEST_URI, QUERY_STRING, SERVER_PORT, REQUEST_SCHEME, SERVER_NAME
and THE_REQUEST. [OR] groups and [NC] are honoured, and %1
backreferences are filled in, so the canonical www-stripper works.
A test the URL cannot answer is not guessed at. %{HTTP_COOKIE}, or
-f and -d asking whether a path is a real file on disk, cannot be known
here — so the rule is shown as firing and the step names what it assumed.
That case is worth dwelling on, because it is the most common condition on the web:
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?path=$1 [QSA,L]
An earlier version of this tool read -f as a regular expression against the
URL path. /my-file contains -f, so the negation flipped and the tool
reported that the front controller does not fire — for a rule that fires on
nearly every request of every WordPress and Laravel site alive. Saying "I
cannot know this" is the only honest answer.
Directives it reads but does not act on get their own panel, and a rule that
can never fire — invalid pattern, RewriteEngine Off, a pattern refused as
unsafe — says so in the rules table rather than sitting there looking live.
A pattern that can hang a server
Some regexes take exponential time to fail. (a+)+b against a long run of
as with no b makes the engine try every way of splitting the input, and so
does (a|ab)* — two different families of the same problem.
Patterns like that are reported and skipped rather than run. Partly so this page does not freeze — a regular expression cannot be interrupted once it starts — and partly because the same pattern in your config does the same thing to your server, on a URL an attacker picks.
Nothing is fetched
The rules are matched where you pasted them. That is what lets you test a rule before it goes live, which is the moment it is worth testing — and it answers about the rules you wrote, rather than about whatever a CDN or a load balancer in front of them is doing on its own.
Once the redirects are right, robots.txt tester checks what crawlers are allowed to reach, the HTTP header analyser reads the caching and security headers the new URL sends back, and the URL parser pulls a single address apart.
questions
- Why does my RewriteRule never fire in .htaccess?
- Almost always the leading slash. Inside an .htaccess the path is matched with the folder the file sits in, and its leading slash, removed — so ^/team$ matches nothing and ^team$ is what you want. In a server config or VirtualHost the slash stays. Switch the "Rules from" control, and set the folder if the file is not at the document root, to see both readings.
- What is the difference between a rewrite and a redirect?
- A rewrite changes the path inside the same request — the browser never hears about it and the address bar does not move. A redirect sends a 301 or 302 back, and the browser makes a fresh request that runs your rules again from the top. Apache does the second with the [R] flag, or when the substitution is an absolute URL. Missing that is why two rules that look fine apart can loop together.
- Does this handle nginx?
- No, and deliberately. Getting nginx right means its phase order, its four-way location precedence and its variable expansion — return 301 https://$host$request_uri needs all three at once. A half-model of it reported that line, the commonest redirect there is, as a loop, so it was taken out rather than shipped wrong.
- Does this fetch my site to check the redirects?
- No. You paste the rules, and the matching runs in the page. That means you can test a rule before it is live, which is the moment it is worth testing, and it answers about the rules you wrote rather than whatever a CDN in front of them is doing.
- Why does Redirect move everything under the path?
- Because mod_alias Redirect matches a path prefix and appends the rest, so Redirect 301 /old-shop /shop also sends /old-shop/socks to /shop/socks. It matches complete segments only, so it leaves /old-shopping alone. RedirectMatch takes a regex instead.
- Are RewriteCond lines evaluated?
- The ones a URL can answer are: HTTPS, HTTP_HOST, REQUEST_URI, QUERY_STRING, SERVER_PORT, REQUEST_SCHEME, SERVER_NAME and THE_REQUEST. [OR] and [NC] are honoured, and %1 backreferences are filled in. A test a URL cannot answer — a cookie, a header, or -f and -d asking whether a file exists on disk — is not guessed at: the rule is shown as firing and the step names what it had to assume. That last case matters, because %{REQUEST_FILENAME} !-f is the front-controller rule every WordPress and Laravel site ships.
- Why was one of my patterns skipped?
- Either it is not valid as a regular expression, in which case Apache would refuse to start too, or it nests a repeat inside a repeat like (a+)+ or repeats an overlapping alternation like (a|ab)*. Those can take exponential time to fail, which would freeze this page — and can do the same to your server on a URL somebody picks. Both are reported rather than run.
- Should I use 301 or 302?
- 301 for a move that is permanent: search engines transfer the old URL’s standing to the new one and browsers cache it hard, which is also why a wrong 301 is painful to undo. 302 keeps the old URL as the canonical one, which is right for a temporary detour and wrong for a migration. 307 and 308 are the same pair but keep the method and body, which matters for a POST.