1

I have a rewrite rule that works most of the time. However, if I request a page with a subdirectory that doesn't exist, and the subdirectory matches an existing file, it causes infinite recursion.

    Options +FollowSymLinks
    AllowOverride None
    Require all granted

    RewriteEngine on

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

This works if I request:

  • test.php
  • test (it serves test.php)
  • badname.php or badname (gives 404 correctly)
  • directory/file_that_exists or directory/file_that_exists.php

All work. But if test.php exists, and I request test/test, it causes infinite recursion. The error log looks like this:

[Wed May 23 08:16:28.176564 2018] [core:debug] [pid 27054] core.c(3620): [client 0.0.0.0:57764] AH00122: redirected from r->uri = /test/test.php.php.php
[Wed May 23 08:16:28.176566 2018] [core:debug] [pid 27054] core.c(3620): [client 0.0.0.0:57764] AH00122: redirected from r->uri = /test/test.php.php
[Wed May 23 08:16:28.176568 2018] [core:debug] [pid 27054] core.c(3620): [client 0.0.0.0:57764] AH00122: redirected from r->uri = /test/test.php
[Wed May 23 08:16:28.176570 2018] [core:debug] [pid 27054] core.c(3620): [client 0.0.0.0:57764] AH00122: redirected from r->uri = /test/test

What am I doing wrong? Shouldn't it fail when it sees that test/test.php doesn't exist? (RewriteCond %{REQUEST_FILENAME}\.php -f) I get the same result if I simply append a slash, like test/.

2

Shouldn't it fail when it sees that test/test.php doesn't exist? (RewriteCond %{REQUEST_FILENAME}\.php -f) I get the same result if I simply append a slash, like test/

It would fail if that was the check being performed, but it's not.

The REQUEST_FILENAME server variable contains the filesystem path after the URL has been mapped to the filesystem. This is not necessarily the same as the URL-path that the RewriteRule pattern matches against.

For example, in your case, when requesting /test/test.php where there is no physical subdirectory called /test, the REQUEST_FILENAME server variable contains a string of the form /absolute/filesystem/path/to/test (ie. the requested URL up to the last known directory + the first path segment /test). And the URL-path matched by the RewriteRule pattern will be /test/test.php (to which you will repeatedly append .php because /test.php does exist, as checked by the preceding RewriteCond directive).

You can resolve this by checking the URL-path instead (building an absolute filesystem path to check), much like @shanew suggests. For example:

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

An optimisation, to avoid all those filesystem checks for every request would be to include a check for the .php extension (and .css, .js, etc.) first, before checking to see if the files exist:

RewriteCond %{REQUEST_URI} !\.(php|css|js|jpg|png)$
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{DOCUMENT_ROOT}/$1\.php -f
RewriteRule ^(.*)$ $1.php [L]

This avoids unnecessary checks when the requested URL already ends with .php, .css, etc.

  • I accepted this because it's the answer, but I'm still having trouble wrapping my head around why. I don't understand the part where "REQUEST_FILENAME contains a string of the form /absolute/filesystem/path/to/test (ie. the requested URL up to the last known directory + the first path segment /test)". The docs say "Depending on the value of AcceptPathInfo, the server may have only used some leading components of the REQUEST_URI to map the request to a file.". Is that why, or am I just off in the wrong direction? Regardless, I never would have figured this out, so thank you. – felwithe May 23 '18 at 19:03
1

If you're doing this in a Directory section or .htaccess file, you should be able to stop the infinite recursion by adding the END option to your RewriteRule, so that it looks like this

RewriteRule ^(.*)$ $1.php [L,END]

This stops the rewrite engine from performing any subsequent rewrite processing (whereas L just stops the current round of rewrite processing). The Apache 2.4 Rewrite Flags documenation explains in more detail.

That said, I'm not sure this gets you the behavior you're really after, since it will still change /test/test into /test/test.php despite the fact that it doesn't exist. To solve that, I think you need to be testing the entire file path rather than just the name of the file. To do that, change:

%{REQUEST_FILENAME}

into

%{DOCUMENT_ROOT}%{REQUEST_URI}

Additionally, to be safe, I'd add another RewriteCond at the top so that the rule never fires if the filename already ends in .php (putting it first means the other conditions can be skipped if it returns true):

%{REQUEST_FILENAME} "!\.php$"

  • You can't simply substitute REQUEST_URI (a document root relative URL) for REQUEST_FILENAME (an absolute filesystem path) in the file exists check. You need to convert REQUEST_URI into an absolute filesystem path before doing the filesystem check, otherwise it will never match. eg. prefix with the document root: %{DOCUMENT_ROOT}%{REQUEST_URI} – MrWhite May 23 '18 at 16:37
  • Good point (I don't normally use the -f or -d checks). I've edited my answer to reflect that (and escaped the backslash so it would actually appear in the extension check). – shanew May 24 '18 at 21:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.