Devanshu BiswasEveryone who has edited a .htaccess file knows the feeling: you paste a RewriteRule, reload, get a...
Everyone who has edited a .htaccess file knows the feeling: you paste a RewriteRule, reload, get a redirect loop or a 404, and start guessing. The rules feel like black magic. They aren't. Every RewriteRule pattern is an ordinary regular expression, and your browser already ships a world-class regex engine in RegExp. So I built a tester that parses your rules and replays them against a URL — no server, nothing uploaded — and shows the winning rule, the captured groups, the substituted target, and whether the browser gets a redirect or a silent internal rewrite.
A .htaccess is just text. Split it on newlines, drop blank lines and # comments, and route each line by its first word — RewriteCond, RewriteRule, Redirect, RedirectMatch. Directives I don't simulate (RewriteEngine, RewriteBase, Options) are skipped so they don't pollute the trace. A RewriteCond is buffered and attached to the RewriteRule that follows it, because in Apache a condition only guards the very next rule — a blank line or another rule ends the group.
Each rule's arguments are space-separated, but a pattern can be quoted to protect literal spaces, so a tiny tokenizer pulls out double-quoted, single-quoted, or bare tokens. The bracketed [flags] tail becomes a map — some flags are bare (L, QSA, NC), some carry a value (R=301) — so I can both test and display them.
This is the part that makes the tool trustworthy instead of a mock. In a per-directory .htaccess Apache strips the leading slash before matching, so I match against path.replace(/^\//, ""). The [NC] flag maps to JavaScript's i flag. I wrap new RegExp in a try/catch so a bad pattern reports an error instead of throwing:
function matchRule(pattern, flags, path){
const subject = path.replace(/^\//, ""); // per-directory: no leading slash
let regex;
try { regex = new RegExp(pattern, flags.NC ? "i" : ""); }
catch (e) { return { error: e.message }; }
return { subject, regex, match: subject.match(regex) }; // match[0], match[1]=$1 ...
}
Then I splice the captures back into the substitution string. $1…$9 come from the rule's own pattern, %1…%9 from the last matched RewriteCond, and %{VARS} like %{HTTP_HOST} from the request context — three .replace calls turn the template into the real target.
The single most useful thing the tester reports is the kind of outcome. A Redirect/RedirectMatch is always external. A RewriteRule is an internal rewrite — the address bar never changes, the server just serves different content — unless it carries an [R] flag or its target is an absolute http(s):// URL, either of which forces a real browser redirect. [F] and [G] short-circuit to 403/410:
function classify(rule, target){
if (rule.flags.F) return "forbidden"; // 403
if (rule.flags.G) return "gone"; // 410
if (rule.type !== "rewrite") return "redirect"; // Redirect / RedirectMatch
if (rule.flags.R) return "redirect"; // [R] or [R=301]
if (/^https?:\/\//i.test(target)) return "redirect"; // absolute URL forces it
return "rewrite"; // default: browser URL unchanged
}
Now the replay: walk the rules top to bottom. Skip a rule whose conditions fail or whose pattern misses. When one fires, record the outcome — a redirect ends processing immediately, and an [L] flag stops the loop too. Everything after a stop is marked "skipped", which is exactly how a misplaced catch-all reveals itself: if your ^(.*)$ index.php sits above your specific rules, the trace shows it eating everything below.
The conditions get evaluated the same honest way — interpolate %{HTTPS} to "on"/"off", regex-test it (honouring a leading ! negation), and AND multiple conds together unless one carries [OR]. Filesystem checks (-f, -d) can't be probed from a browser, so the UI gives you toggles for what the server would find.
The demo ships presets (front controller + HTTPS, pretty URLs, old→new 301, force-www) and calls out the five classic gotchas: the leading slash the pattern never sees, order plus [L] deciding everything, rewrite ≠ redirect, RewriteCond guarding only the next rule, and Redirect being prefix-match while RedirectMatch is regex.
The lesson underneath the whole build is the one that makes .htaccess stop being scary: mod_rewrite is a regex loop with early exit, and once you can replay that loop by hand, the file is just code you can read.
Paste your rules and a URL and watch it run:
https://dev48v.infy.uk/solve/day43-htaccess-redirect-tester.html