mod_rewrite: No More Endless Loops!

Looking To The Change Log…

The change logs for Apache 2.0.45 and 1.3.28 contain the following notes:

Apache 2.0.45:
Prevent endless loops of internal redirects in mod_rewrite by
aborting after exceeding a limit of internal redirects. The
limit defaults to 10 and can be changed using the RewriteOptions
directive. PR 17462. [André Malo]

Apache 1.3.28:
backport from 2.x series: Prevent endless loops of internal redirects
in mod_rewrite by aborting after exceeding a limit of internal redirects.
The limit defaults to 10 and can be changed using the RewriteOptions
directive. PR 17462. [André Malo]

These two releases of the popular Web server introduce a new option for the RewriteOptions directive: MaxRedirects.

mod_rewrite, for those that don’t know, is a powerful module that allows you to handle incoming URI requests and more.

This article will explain the use of and advantages of the MaxRedirects option. Web hosting companies that offer the mod_rewrite module should take note of these reasons to upgrade to the latest Apache release. Keeping your Web server up-to-date is a must. Hosting companies that do not offer the mod_rewrite module are encouraged to enable it.

Before we proceed, you’ll need a working Apache installation with mod_rewrite installed. Please see my FAQ for installation help.

A Typical Case

Let’s start with an example.

We want to redirect all URIs to a PHP script. The redirection will be internal, so the client, for example, a browser, will not see that a redirection has occurred. To do this, we create in the document root an .htaccess file that contains these rules:

# Enable rewrite engine 
Options +FollowSymLinks
RewriteEngine On

# Redirect internally all URIs to /index.php
RewriteRule .* /index.php [L]

However, this will not work – it’ll freeze your Apache Web server. Why?

The problem is this line:

RewriteRule .* /index.php [L]

Simply put, that rule ‘says’ “Redirect all requests to /index.php.”. That may sound like what we originally set out to achieve, but, in practice, we will not get the results we expect.

The following is what happens with a request for www.example.net/path/to/:

Phase A

  • The URI is /path/to/
  • RewriteRule will rewrite the URI from path/to/ to /index.php
  • An internal redirection will be made for /index.php
  • The internal redirection will be treated as a new request, so it will be parsed again (see phase b)

Phase B

  • The previous internal redirection, /index.php, will be processed again by the rewrite engine
  • RewriteRule will rewrite index.php to /index.php
  • An internal redirection will be made for /index.php

At this point the rewrite engine will run Phase B continuously and the Apache Web server will freeze.

If it sounds complicated, don’t worry! Here’s a simplified log file that shows the process:

# phase a)  
 [] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/path/to/ -> path/to/  
 [] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'path/to/'  
 [] (2) [per-dir /www/htdocs/] rewrite path/to/ -> /index.php  
 [] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
   
 # phase b)  
 [redir#1] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
 [redir#1] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
 [redir#1] (2) [per-dir /www/htdocs/] rewrite index.php -> /index.php  
 [redir#1] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
   
 # phase b)  
 [redir#2] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
 [redir#2] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
 [redir#2] (2) [per-dir /www/htdocs/] rewrite index.php -> /index.php  
 [redir#2] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
   
 # phase b)  
 [redir#3] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
 [redir#3] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
 [redir#3] (2) [per-dir /www/htdocs/] rewrite index.php -> /index.php  
 [redir#3] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
   
 ....ad infinitum...  
   
 # phase b)  
 [redir#N] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
 [redir#N] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
 [redir#N] (2) [per-dir /www/htdocs/] rewrite index.php -> /index.php  
 [redir#N] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]

This is what happens with versions of Apache prior to the 2.0.45 and 1.3.28 releases.

Maxredirects

The following simplified log file shows what would happen if our example was run under Apache 2.0.45 or 1.3.28:

# phase a)  
[] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/path/to/ -> path/to/  
[] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'path/to/'  
[] (2) [per-dir /www/htdocs/] rewrite path/to/ -> /index.php  
[] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
 
# phase b)  
[redir#1] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
[redir#1] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
[redir#1] (2) [per-dir /www/htdocs/] rewrite index.php -> /index.php  
[redir#1] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
 
# phase b)  
[redir#2] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
[redir#2] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
[redir#2] (2) [per-dir /www/htdocs/] rewrite index.php -> /index.php  
[redir#2] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
 
...last...  
 
# phase b)  
[redir#10] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
[redir#10] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
[redir#10] (2) [per-dir /www/htdocs/] rewrite index.php -> /index.php  
[redir#10] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]

While the rewrite log may seem the same as before, it's not. The internal redirections are not continuous, as they stop after 10 attempts. The Web server will not freeze and we will receive a 500 HTTP status error (Internal Server Error).

Our error_log will look like this:

[Mon Jul 21 15:24:27 2003] [error] [client 127.0.0.1] mod_rewrite: maximum number of internal redirects reached. Assuming configuration error. Use 'RewriteOptions MaxRedirects' to increase the limit if neccessary.

This detailed error log will help us to debug our rules quickly and precisely.

Even though we use the same rules as before, our Apache Web server won't freeze any more. Isn't it magic?

RewriteOptions

So our Web server won't freeze any more... but why did the internal redirections stop after 10 redirects? This is where the RewriteOptions directive and maxredirects option come into play.

By default (i.e. if you don't declare it), maxredirects is set to:

RewriteOptions maxredirect=10

This directive will force the rewrite engine to stop after 10 internal redirections as, after that time, we're probably in an endless loop.

If the figure of 10 redirects is too high, you can change it to a lower value. For example:

RewriteOptions maxredirect=3

Our log file would then look like this:

# phase a)   
[] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/path/to/ -> path/to/  
[] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'path/to/'  
[] (2) [per-dir /www/htdocs/] rewrite path/to/ -> /index.php  
[] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
 
# phase b)  
[redir#1] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
[redir#1] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
[redir#1] (2) [per-dir /www/htdocs/] rewrite index.php -> /index.php  
[redir#1] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
 
# phase b)  
[redir#3] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
[redir#3] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
[redir#3] (2) [per-dir /www/htdocs/] rewrite index.php -> /index.php  
[redir#3] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
 
...stop

Remember that the maxredirect option helps control a deliberate mistake in the rules we created. It will not fix the mistake, but it will stop it freezing our Web server.

This is good for both hosting companies and mod_rewrite users.

Conclusions

Last but not least, what can we do to correct the rules we used in the example? We need to use this rule instead:

RewriteRule .* index.php [L]

The difference is that, in the amended rule, index.php does not begin with a slash. In the latter case mod_rewrite is clever enough to prevent endless-loops, as it sees that the original request of index.php is the same as the internal redirection to index.php, so it won't perform the redirection. This can be seen in the following log file:

# phase a)   
[] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/path/to/ -> path/to/  
[] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'path/to/'  
[] (2) [per-dir /www/htdocs/] rewrite path/to/ -> index.php  
[] (3) [per-dir /www/htdocs/] add per-dir prefix: index.php -> /www/htdocs/index.php  
[] (2) [per-dir /www/htdocs/] strip document_root prefix: /www/htdocs/index.php -> /index.php  
[] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
 
# phase b)  
[redir#1] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
[redir#1] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
[redir#1] (2) [per-dir /www/htdocs/] rewrite index.php -> index.php  
[redir#1] (3) [per-dir /www/htdocs/] add per-dir prefix: index.php -> /www/htdocs/index.php  
 
# Smart check that will avoid the endless loops  
[redir#1] (1) [per-dir /www/htdocs/] initial URL equal rewritten URL: /www/htdocs/index.php [IGNORING REWRITE]

Interesting, isn't it ?

If you have any questions regarding Apache and/or mod_rewrite please visit SitePoint's Apache forum.

Bibliography

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

No Reader comments

Comments on this post are closed.