# Fix Apache mod_rewrite Cond Backreference Wrong Group Match
Your Apache mod_rewrite rules use RewriteCond with backreferences, but the rewritten URL contains unexpected values or the rule simply does not match. The issue is how backreferences work between RewriteCond and RewriteRule.
Understanding Backreference Types
Apache mod_rewrite has two types of backreferences:
- Rule backreferences:
$0through$9-- reference capture groups from theRewriteRulepattern - Condition backreferences:
%0through%9-- reference capture groups from the last matchedRewriteCondpattern
Mixing these up is one of the most common mod_rewrite bugs.
The Common Mistake
# WRONG: Using $1 in RewriteCond (rule backref in condition context)
RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC]
RewriteRule ^(.*)$ https://$1/$1 [R=301,L]
# The second $1 refers to the RewriteRule capture, not the RewriteCond capture# WRONG: Using %1 in RewriteRule for rule capture
RewriteCond %{REQUEST_URI} !-f
RewriteRule ^/blog/(.*)$ /index.php?post=%1 [L]
# %1 refers to the RewriteCond capture (which is empty since there are no groups)
# Should use $1 to capture from the RewriteRule patternCorrect Usage
```apache # Capture from RewriteCond: use %1, %2, etc. RewriteCond %{HTTP_HOST} ^www\.(.+) [NC] RewriteRule ^(.*)$ https://%1/$1 [R=301,L] # %1 = the domain part after www. (e.g., "example.com") # $1 = the URI path from the RewriteRule
# Capture from RewriteRule: use $1, $2, etc. RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^/products/([^/]+)/([^/]+)$ /product.php?category=$1&id=$2 [L] # $1 = the first path segment after /products/ # $2 = the second path segment ```
Complex Example: Multiple Conditions with Backreferences
# Rewrite based on multiple conditions
RewriteCond %{HTTP_HOST} ^([^.]+)\.example\.com$ [NC]
RewriteCond %{REQUEST_URI} ^/api/([^/]+)/(.+)$ [NC]
RewriteRule ^.*$ https://api.example.com/%2/%1?subdomain=%1&path=%2 [P,L]Here:
- %1 in the RewriteRule = capture from the LAST RewriteCond (([^/]+) from REQUEST_URI)
- %2 = the first capture group from the FIRST RewriteCond (the subdomain)
- The backreference numbering is per-condition, not global
Actually, this is where it gets tricky. The %N backreferences always refer to the last matched RewriteCond. To capture from multiple conditions, you need to chain them differently:
# Correct approach for capturing from multiple conditions
RewriteCond %{HTTP_HOST} ^([^.]+)\.example\.com$ [NC]
RewriteRule ^/api/([^/]+)/(.+)$ https://api.example.com/%1/$1/$2 [P,L]
# %1 = subdomain from RewriteCond
# $1 = first segment from RewriteRule
# $2 = remaining path from RewriteRuleThe %0 Backreference
%0 contains the entire string matched by the last RewriteCond pattern:
RewriteCond %{REQUEST_URI} \.(css|js|png|jpg)$
RewriteRule ^ - [E=STATIC_FILE:%0]
# %0 = the entire REQUEST_URI that matched the conditionDebugging Backreference Values
Enable rewrite tracing to see what each backreference contains:
LogLevel alert rewrite:trace4Then check the log:
sudo tail -f /var/log/apache2/error.log | grep rewriteWith trace4, you see the actual values of each capture group:
[rewrite:trace4] RewriteCond: input='api.example.com' pattern='^([^.]+)\.example\.com$' => matched, result[0]='api.example.com', result[1]='api'This shows exactly what %0 (api.example.com) and %1 (api) contain.
Common Patterns and Their Backreferences
```apache # Domain redirect: www to non-www RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]
# Force HTTPS, preserving path and query RewriteCond %{HTTPS} off RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
# Rewrite clean URLs to query parameters RewriteRule ^/user/(\d+)/(\w+)$ /profile.php?id=$1&tab=$2 [L]
# Redirect old URL structure to new RewriteCond %{REQUEST_URI} ^/blog/(\d{4})/(\d{2})/(.+)$ [NC] RewriteRule ^ /articles/%3?year=%1&month=%2 [R=301,L] ```