2026年11月13日
2026年11月13日
RewriteCond and RewriteRule: Syntax, Backreferences and Flags
This article is a localized edition of the Japanese original. Read the Japanese original
mod_rewrite is the Apache module that rewrites incoming URLs, or redirects them to other URLs, depending on conditions. It revolves around two directives: RewriteRule decides "which URL is rewritten and how," and RewriteCond decides "under what conditions that rewrite happens."
This article covers the syntax and the rules of how they behave, then walks through common patterns with examples. Every example was checked with curl on Apache 2.4 (the official httpd 2.4 image as of November 2026).
Preparation
On Debian/Ubuntu, enable mod_rewrite and restart.
sudo a2enmod rewrite
sudo systemctl restart apache2
If you write rules in .htaccess, .htaccess settings must be allowed for that directory. Check that the <Directory> block in the server configuration has settings like the following. Without FileInfo (or All) in AllowOverride, the Rewrite directives in .htaccess can't be used, and without FollowSymLinks or SymLinksIfOwnerMatch in Options, requests return 403.
<Directory /var/www/html>
Options FollowSymLinks
AllowOverride FileInfo
Require all granted
</Directory>
Wherever you write the rules, you need RewriteEngine On first.
RewriteRule syntax
RewriteRule Pattern Substitution [flags]
- Pattern: A regular expression (PCRE) matched against the path part of the URL. The host name and the query string (everything after
?) are not included. - Substitution: The rewritten path, or a URL starting with
https://. Writing-applies only the flags without rewriting. - Flags: Written comma-separated inside square brackets, as in
[R=301,L].
The pattern differs between .htaccess and server configuration
This is where people trip up most. In server configuration such as <VirtualHost>, the string the pattern matches starts with a leading slash, as in /oldpage.html. In .htaccess (and inside <Directory>), the path up to that directory is stripped, so there is no leading slash, as in oldpage.html.
# In server configuration (VirtualHost)
RewriteRule ^/oldpage\.html$ /newpage.html [R=301,L]
# In .htaccess
RewriteRule ^oldpage\.html$ /newpage.html [R=301,L]
If you write ^/oldpage\.html$ in .htaccess, it never matches and nothing happens (in our test the request stayed a 404). When moving rules from server configuration to .htaccess or the other way around, review the start of each pattern.
RewriteCond syntax
RewriteCond TestString CondPattern [flags]
RewriteCond applies only to the single RewriteRule that immediately follows it. If you stack several RewriteCond lines, the RewriteRule right below them runs only when all of them are true (AND). They have no effect on the rule after that, so repeat the conditions for each rule that needs them.
The test string uses server variables in the form %{VARIABLE_NAME}. Commonly used ones:
| Variable | Example value |
|---|---|
%{HTTP_HOST} |
Host name of the request (example.com) |
%{HTTPS} |
on for HTTPS, otherwise off |
%{REQUEST_URI} |
The path (/blog/index.php). Does not include the query string |
%{QUERY_STRING} |
Everything after ? (id=15&cat=2) |
%{REQUEST_FILENAME} |
The corresponding path on the file system |
%{REMOTE_ADDR} |
The client's IP address |
%{HTTP:Header-Name} |
Any request header (such as %{HTTP:X-Forwarded-Proto}) |
Besides regular expressions, the condition pattern accepts the following special tests. Prefix with ! to negate.
| Test | Meaning |
|---|---|
-f |
Exists as a regular file |
-d |
Exists as a directory |
-s |
Exists as a file with a size greater than 0 |
-l |
Exists as a symbolic link |
RewriteCond has two main flags. [NC] makes the match case-insensitive, and [OR] combines the condition with the next one using OR.
Backreferences: $1 and %1
Parts captured with parentheses in a regular expression can be used in the substitution.
$1to$9: Parts captured by theRewriteRulepattern%1to%9: Parts captured by the condition pattern of the last matchedRewriteCond
# /blog/2024/05/hello-world → /articles/hello-world
RewriteRule ^blog/(\d{4})/(\d{2})/(.+)$ /articles/$3 [R=301,L]
Commonly used flags
| Flag | Meaning |
|---|---|
L |
Stop processing at this rule (see the caveat below) |
R=301 / R=302 |
Send an external redirect. R alone means 302 |
NC |
Case-insensitive match |
QSA |
Append the original query string to the query string in the substitution |
QSD |
Discard the original query string (Apache 2.4 and later) |
F |
Return 403 Forbidden |
G |
Return 410 Gone |
END |
End rewriting here, with no further .htaccess pass either (Apache 2.4 and later) |
NE |
Do not escape characters such as # and % in the substitution |
In .htaccess, even after processing stops at L, .htaccess is processed again from the top with the rewritten URL. If the rewritten URL matches the same rule again, you get loops or unintended double rewrites. To end rewriting at that point, use END.
Examples
The following rules were placed in .htaccess in this order and tested. The general rule is to put redirects first and internal rewrites (the front controller) last.
RewriteEngine On
# 1) Normalize example.com to www.example.com
RewriteCond %{HTTP_HOST} ^example\.com$ [NC]
RewriteRule ^(.*)$ https://www.example.com/$1 [R=301,L]
# 2) Redirect HTTP to HTTPS
RewriteCond %{HTTPS} off
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]
# 3) Redirect an old page to a new page
RewriteRule ^oldpage\.html$ https://www.example.com/newpage.html [R=301,L]
# 4) Redirect using a value from the query string and drop the original query
RewriteCond %{QUERY_STRING} (?:^|&)id=([0-9]+)(?:&|$)
RewriteRule ^product\.php$ https://www.example.com/products/%1/ [R=301,L,QSD]
# 5) Handle every path that doesn't exist with index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]
Test results:
| Request | Result |
|---|---|
http://example.com/foo/bar?x=1 |
301 → https://www.example.com/foo/bar?x=1 |
http://www.example.com/foo |
301 → https://www.example.com/foo |
/oldpage.html (HTTPS) |
301 → https://www.example.com/newpage.html |
/product.php?cat=2&id=15 |
301 → https://www.example.com/products/15/ (no query) |
/product.php?cat=2 |
Not rewritten (no id, so the condition doesn't match) |
/some/pretty/url |
200 (index.php responds) |
/style.css (an existing file) |
200 (the file is served as is) |
Key points for each rule:
- 1) The
RewriteRulepattern doesn't include the query string, but if the substitution has no query of its own, the original query string is carried over automatically (?x=1is kept). - 2) If a load balancer or CDN terminates HTTPS, Apache always sees
%{HTTPS}asoff. Without theX-Forwarded-Protocondition, the redirect repeats forever. If you don't use a proxy, the second condition is unnecessary. - 3) If the substitution is only a path such as
/newpage.html, whether the redirect goes tohttporhttpsdepends on the connection Apache received. Where a proxy terminates HTTPS, you can end up redirecting tohttp://, so writing a URL that starts withhttps://in redirects avoids the ambiguity. - 4) The query string can't be matched by the
RewriteRulepattern, so inspect%{QUERY_STRING}withRewriteCondand use the captured value via%1. WithoutQSD, the original?cat=2&id=15stays attached to the destination. - 5) This is the same shape WordPress and other CMSs use. Existing files and directories are excluded, and everything else is passed to
index.php.
Combining conditions with OR
This example returns 403 for the admin area and a specific path unless the request comes from a given IP range. The first two conditions are joined with [OR], and that result is ANDed with the third condition.
RewriteCond %{REQUEST_URI} ^/admin [OR]
RewriteCond %{REQUEST_URI} ^/secret
RewriteCond %{REMOTE_ADDR} !^192\.0\.2\.
RewriteRule ^ - [F]
%{REQUEST_URI} includes the leading slash even in .htaccess, so this example can be written the same way in server configuration and in .htaccess.
When you don’t need mod_rewrite
For simple redirects, mod_alias Redirect and RedirectMatch are easier to read and harder to get wrong. Apache's official documentation also recommends not using mod_rewrite when another method will do.
Redirect 301 /oldpage.html https://www.example.com/newpage.html
RedirectMatch 301 ^/blog/(\d{4})/(\d{2})/(.+)$ https://www.example.com/articles/$3
mod_rewrite earns its place when you need to branch on conditions such as the host name, the query string or whether a file exists.
Troubleshooting
Check redirects with curl
Browsers remember 301s, so even after you fix a rule they may keep going to the old destination. Look at the Location header with curl -I https://www.example.com/oldpage.html, or use R=302 while testing and switch to R=301 once the rule is final.
Log the rewriting
In Apache 2.4, RewriteLog has been removed; logging is done with LogLevel. Put it in the server configuration (a VirtualHost or similar, not .htaccess) and watch the error log.
LogLevel warn rewrite:trace3
sudo tail -f /var/log/apache2/error.log | grep rewrite
The log shows the string being matched (in .htaccess, the path without the leading slash), the result of each condition and the rewritten value. Trace-level logging is verbose and slows the server down, so revert it when you are done. How to read it in detail is also explained in Debugging Apache mod_rewrite with the debug log.
Check the configuration syntax
After changing the server configuration, check it with sudo apache2ctl configtest before reloading. Mistakes in .htaccess show up as 500 errors in the error log when the page is accessed.
Summary
- Rewrite with
RewriteRule Pattern Substitution [flags], and attach conditions to the single following rule withRewriteCond - The pattern matches only the path, and in
.htaccessthere is no leading slash $1comes from the rule's pattern;%1comes from the last matched condition- Inspect the query string with
%{QUERY_STRING}, and drop it withQSDif you don't need it - Put redirects first and internal rewrites such as a front controller last
- Simple redirects only need
Redirect; test with curl andLogLevel rewrite:trace3
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)