Apache mod_rewrite: Redirects, Rewrites, and HTTPS Enforcement

mod_rewrite is one of Apache’s most powerful and most confusing modules. The syntax is dense, the flags are cryptic, and debugging is painful. But it’s also everywhere — almost every HTTPS redirect, URL rewrite, and routing rule in Apache uses it.

I’ve spent a lot of time staring at mod_rewrite configs. Here’s what I actually understand about it and how I use it.

Enabling mod_rewrite

On Debian/Ubuntu:

a2enmod rewrite
systemctl restart apache2

Verify it’s loaded:

apache2ctl -M | grep rewrite

The Basics: RewriteRule Syntax

RewriteRule pattern substitution [flags]

The pattern is a regex matching the request URI. The substitution is what to replace it with. Flags modify behavior.

A simple redirect:

RewriteRule ^/old-page$ /new-page [R=301,L]
  • ^/old-page$ — matches exactly /old-page
  • /new-page — redirects to this
  • R=301 — send a 301 (permanent) redirect
  • L — Last rule, stop processing further rules

Enabling Rewrite in Directory Config

If you’re using .htaccess, the directory needs AllowOverride All in the main Apache config:

<Directory /var/www/html>
    AllowOverride All
</Directory>

In the server config or .htaccess, start with:

RewriteEngine On

HTTPS Redirect

This is the most common use of mod_rewrite. Redirect all HTTP to HTTPS:

<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com

    RewriteEngine On
    RewriteCond %{HTTPS} off
    RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>

RewriteCond %{HTTPS} off — condition: only if the request isn’t already HTTPS. Without this, the HTTPS vhost would also match and you’d get an infinite redirect loop.

https://%{HTTP_HOST}%{REQUEST_URI} — preserves the full URL including path and query string.

A simpler alternative (Apache 2.4+) that doesn’t require mod_rewrite:

<VirtualHost *:80>
    ServerName example.com
    Redirect permanent / https://example.com/
</VirtualHost>

I prefer the Redirect approach for simple HTTPS redirects — less complexity, easier to read.

www to non-www (and vice versa)

Redirect www to non-www:

<VirtualHost *:443>
    ServerName www.example.com

    RewriteEngine On
    RewriteRule ^(.*)$ https://example.com$1 [R=301,L]
</VirtualHost>

Or with a condition on the hostname:

<VirtualHost *:443>
    ServerName example.com
    ServerAlias www.example.com

    RewriteEngine On
    RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
    RewriteRule ^(.*)$ https://%1$1 [R=301,L]

%1 — back-reference to the first RewriteCond capture group (the domain without www).

Non-www to www:

RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^(.*)$ https://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

Clean URLs (removing .php extension)

WordPress and many PHP CMSes do this. Make /about serve about.php:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ $1.php [L]

!-d — not an existing directory.
!-f — not an existing file.

So: if the request doesn’t match an existing file or directory, append .php and try that.

Front Controller Pattern

For MVC frameworks (Laravel, Symfony, CodeIgniter), route all requests through index.php:

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /

    RewriteRule ^index\.php$ - [L]

    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d

    RewriteRule . /index.php [L]
</IfModule>

This is essentially the default .htaccess for Laravel. If the file doesn’t exist (!-f) and it’s not a directory (!-d), send it to index.php. The framework handles routing from there.

Rewrite Flags Reference

The flags I actually use:

[R=301] — redirect with 301 (permanent). Use R=302 for temporary. Browser caches 301s, so use them carefully.

[L] — last rule, stop processing. Almost always include this.

[NC] — case-insensitive matching.

[QSA] — Query String Append. Appends the original query string to the substitution. Without this, rewriting /old?param=value to /new loses the ?param=value.

[NE] — No Escape. Don’t escape special characters in the output.

[P] — Proxy. Pass the request to a backend via mod_proxy. Requires mod_proxy.

[F] — Forbidden. Returns 403. Useful for blocking specific patterns.

Debugging mod_rewrite

Turn on rewrite logging (in Apache config, not .htaccess):

LogLevel alert rewrite:trace3

This is verbose — trace3 logs every rule evaluation. Use it temporarily and disable when done. The log goes to the Apache error log:

tail -f /var/log/apache2/error.log

Test whether mod_rewrite is doing what you expect:

apachectl configtest   # Syntax check
curl -v http://example.com/test   # Follow redirects
curl -IL http://example.com/test  # Show redirect chain headers only

Common Mistakes

Infinite redirect loop — often caused by HTTPS redirect applied to the HTTPS vhost too. Use RewriteCond %{HTTPS} off or separate HTTP and HTTPS vhosts.

Rules not applying — check AllowOverride All is set for the directory. Check RewriteEngine On is present. Check rule order.

Query string lost — add [QSA] flag or include %{QUERY_STRING} in the substitution explicitly.

Cached redirects — browsers cache 301 redirects. If you change a redirect, clear browser cache or test in incognito mode.

Rules applying in wrong order — mod_rewrite processes rules top to bottom, stopping at [L]. Earlier rules apply first.

ProxyPass as an Alternative

For proxying to a backend, ProxyPass is usually simpler than using RewriteRule [P]:

<VirtualHost *:443>
    ServerName api.example.com

    ProxyRequests Off
    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/
</VirtualHost>

Use mod_rewrite with the [P] flag when you need conditional logic or URL transformation. Use ProxyPass when you just want straightforward proxying.

For the SSL side of Apache configuration, the Apache SSL installation guide covers SSL certificates and virtual host setup in detail.

Scroll to Top