Apache – Mod_Rewrite Rules for HTTPS/HTTP with CMS Index.php

.htaccessapache-2.2mod-rewrite

With the below rewrites in htaccess, as expected, this HTTPS request is not rewritten:

https://example.com/system/anything

is not re-written to

http://example.com/system/anything

But, unexpectedly, this HTTPS request is rewritten:

https://example.com/preview/anything

is re-written to

http://example.com/index.php/preview/anything

Why is this?

A couple other facts / observations:

/system/ is an actual path on the server. But /preview/ is not an actual path–it's a URL segment that makes sense in the CMS, e.g., /index.php/preview/anything is how the CMS gets the request for the /preview/anything URL.

Other non-system URLs do rewrite properly (from HTTPS to HTTP), and properly do get passed to index.php. E.g.,

https://example.com/real

is re-written to

http://example.com/real

Here is the full block of rules:

 <IfModule mod_rewrite.c>
 RewriteEngine On
 RewriteBase /

 # Force HTTPS for System URLs
 RewriteCond %{REQUEST_URI} ^/system(.*)$ [NC]
 RewriteCond %{HTTPS} !=on
 RewriteRule ^(.*)$ "https://example.com/$1" [R=301,L]

 # Force HTTP for Other URLs, but not: system or preview
 RewriteCond %{REQUEST_URI} !^/(system|preview)/(.*)$ [NC]
 RewriteCond %{HTTPS} =on
 RewriteRule ^(.*)$ "http://example.com/$1" [R=302,L]

 RewriteCond %{REQUEST_FILENAME} !-f
 RewriteCond %{REQUEST_FILENAME} !-d
 RewriteRule ^(.*)$ /index.php/$1 [L]
 </IfModule>

Any insights into why /preview/ gets the weird treatment?


Added: Note that /preview/anything is getting a 302 redirect to /index.php/preview/anything — and THAT'S a big part of what seems so weird / unexpected. It should not be getting a redirect in the final rule, just a rewrite.

Best Answer

Are these rewrite rules in an .htaccess file? In this case the [L] flag does not do what you think it does — it stops processing of the current ruleset, but then the request is processed by Apache again, using .htaccess files appropriate for the rewritten URI, therefore your rules may be executed again. This does not happen for rules which are in the Apache configuration file (and not inside a <Directory> section) — in this case the [L] flag is handled as expected.

For your example, https://example.com/preview/anything is internally rewritten into https://example.com/index.php/preview/anything by the third rule; however, in order to process that request, Apache must read the .htaccess file again — and this time the URI matches your second rule, which returns a 302 redirect.

Apache 2.4.x supports the [END] flag which stops such rewrite loops, unlike [L]; solutions for earlier Apache versions are more complex.

If you want to make sure that Apache makes only a single pass over your rewrite rules, you may add the following rule before all others:

RewriteCond %{ENV:REDIRECT_STATUS} !=""
RewriteRule ^ - [L]

On the first pass REDIRECT_STATUS will be empty; on the second pass it will have a non-empty value (usually 200), and the rule will match and really stop further rewrites.

In case such a rule is not appropriate (e.g., in some cases you need to handle rewritten URIs on the second pass), you can set an environment variable in rules which should be really final:

RewriteRule ^(.*)$ /index.php/$1 [L,E=FINISH:1]

and add the following rule before all others:

RewriteCond %{ENV:REDIRECT_FINISH} !=""
RewriteRule ^ - [L]

Note that during the second pass Apache prepends REDIRECT_ to environment variable names which were defined during the first pass, therefore you need to set FINISH, but test REDIRECT_FINISH.

Alternatively, you may try to modify your matching conditions so that on the second pass URIs modified on the first pass will not be matched (e.g., insert (index\.php/)? into the regexp in the second rule).

Related Topic