# 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: $0 through $9 -- reference capture groups from the RewriteRule pattern
  • Condition backreferences: %0 through %9 -- reference capture groups from the last matched RewriteCond pattern

Mixing these up is one of the most common mod_rewrite bugs.

The Common Mistake

apache
# 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
apache
# 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 pattern

Correct 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

apache
# 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:

apache
# 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 RewriteRule

The %0 Backreference

%0 contains the entire string matched by the last RewriteCond pattern:

apache
RewriteCond %{REQUEST_URI} \.(css|js|png|jpg)$
RewriteRule ^ - [E=STATIC_FILE:%0]
# %0 = the entire REQUEST_URI that matched the condition

Debugging Backreference Values

Enable rewrite tracing to see what each backreference contains:

apache
LogLevel alert rewrite:trace4

Then check the log:

bash
sudo tail -f /var/log/apache2/error.log | grep rewrite

With trace4, you see the actual values of each capture group:

bash
[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] ```