mod_rewrite isn't magic, it's a regex loop with early exit: a .htaccess redirect tester built on the browser's own RegExp

# webdev# apache# programming# beginners
mod_rewrite isn't magic, it's a regex loop with early exit: a .htaccess redirect tester built on the browser's own RegExpDevanshu Biswas

Everyone 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.

The whole thing is: parse the rules, then replay them

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.

The honest core: match with real RegExp

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 ...
}
Enter fullscreen mode Exit fullscreen mode

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.

Redirect or rewrite? The classification that matters most

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
}
Enter fullscreen mode Exit fullscreen mode

Run the set in order, honour [L]

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