1

I have these two rules and they appear to be working. How can I combine them into one rule

#Force hmtl extn
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}\.html -f
RewriteRule ^(.*)$ $1.html

#Force php extn
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}\.php -f
RewriteRule ^(.*)$ $1.php
3
  • 2
    I'm not sure that those rules can be combined. Why would you want to? Jan 29, 2022 at 12:59
  • Just to reduce the size of the file, and improve execution speed perhaps. Jan 29, 2022 at 22:38
  • @Rhohit, Don't worry about either of those things in this case.
    – Steve
    Jan 30, 2022 at 21:58

1 Answer 1

4

Combining rules doesn't necessarily make them any more efficient.

In this particular case, it doesn't really make sense to combine them. Whilst they are similar, they perform different checks and rewrite to different URLs.

It is technically possible to combine these two rules, but that doesn't make the rule any shorter or more efficient. In fact, it would be arguably more complex and less efficient (or the same efficiency at best). See below. *1

However, as written these two rules are not strictly correct - they can result in 500 Internal Server Errors (due to internal rewrite-loops) for certain requests. They can also be made much more efficient (which will offer far greater improvement than simply combining them).

Make current rules shorter and more efficient

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}\.html -f
RewriteRule ^(.*)$ $1.html

This rule can be improved in a number of ways:

  • There is no need to check that the request does not map to a directory (first condition) before immediately checking that the request (with a .html extension) does map to a file (second condition). These are mutually inclusive conditions. So, straight away, only one filesystem check is required; not two. Filesystem checks are relatively expensive, so always best to avoid when possible.

  • The second condition that supposedly checks that the target file exists is not necessarily testing the same URL-path that the following rule will rewrite to. This will result in a rewrite-loop (500 error) should you receive a request for /file/foo where /file.html exists (and /file is not a directory). In this scenario, the condition will be successful (/file.html exists), but it will rewrite to /file/foo.html, /file/foo.html.html, etc.

  • Assuming your extensionless URLs don't include what looks like a file extension then you can immediately exclude all URLs that already include a file extension (ie. your static resources) since you're not going to have a file like /image.jpg.html or /styles.css.html, etc.

  • You should include the L flag (or END on Apache 2.4) to prevent further (unnecessary) processing of the .htaccess file.

So, taking the above points into consideration, these two rules can be written more efficiently (and correctly) like this instead:

# Rewrite to ".html" files
RewriteCond %{DOCUMENT_ROOT}/$1.html -f
RewriteRule ^(?!.+\.\w{2,4})(.+)$ $1.html [L]

# Rewrite to ".php" files
RewriteCond %{DOCUMENT_ROOT}/$1.php -f
RewriteRule ^(?!.+\.\w{2,4})(.+)$ $1.php [L]

These two rules should be in order of "most likely" first.

The regex in the RewriteRule pattern can be simplified and made more efficient if your URLs do not contain dots at all. Then you could simply use ^([^.]+)$ instead of ^(?!.+\.\w{2,4})(.+)$, avoiding the negative lookahead.

NB: No need to backslash-escape the literal dot in the TestString (first argument) - this is an ordinary string, not a regex.

This does assume your .htaccess file is in the document root.

*1 Combining these two rules into one

As an academic exercise, you could combine these two rules into one like this:

# Rewrite to ".html" OR ".php" files
RewriteCond html (.+)
RewriteCond %{DOCUMENT_ROOT}/$1.%1 -f [OR]
RewriteCond php (.+)
RewriteCond %{DOCUMENT_ROOT}/$1.%1 -f
RewriteRule ^(?!.+\.\w{2,4})(.+)$ $1.%1 [L]

The downsides with this approach (actually, it's not that bad!):

  • It is arguably less efficient. When the .html file exists there are two filesystem checks (2nd and 4th conditions), when only one is necessary. With the separate rules there is only one. (If filesystem checks are cached internally then this is a non-issue, but I'm not sure that that is the case.)

  • There is a complexity in this method which may not be immediately obvious. (See point above)

  • Not really a downside, but it is no fewer directives than having two separate rules. That are easier to understand.

UPDATE:

How this works:

Since we need to rewrite the request to two different URLs (.html or .php) depending on which file exists (.html has priority in this example since it is checked first), we need to "remember" which file extension we need to rewrite to. This is the purpose of the 1st and 3rd conditions (RewriteCond directives). They capture html and php respectively (the second overwriting the former), which is then accessible with the %1 backreference in the RewriteRule substitution string. The trick is making sure that the backreference is not overwritten (with php) when the .html file exists...

An important point here is (as with other programming languages), the second operand in an ORd expression is not processed when the first operand is true/successful. In other words, with the above conditions, the 3rd condition that captures php is not processed when the preceding condition is true, ie. when the test for the .html file is successful (2nd condition), php is not captured in the backreference; it stays as html (1st condition).

And... only capturing groups from the last matched CondPattern are saved as backreferences. So, in the above conditions, if the 2nd condition fails (ie. the corresponding .html does not exist) then the 3rd condition captures the string php and this overrides the backreference captured in the 1st condition. On the other hand, if the 2nd condition is successful (ie. the .html file does exist) then the 3rd condition is not processed and the earlier backreference (that holds html) is not overwritten. The 2nd and 4th conditions are not regex comparisons so do not affect the captured backreferences.

So, if you request /foo and /foo.html exists then...

  1. The RewriteRule pattern successfully matches the request and captures foo in the $1 backreference.
  2. The 1st condition stores html in the %1 backreference.
  3. The 2nd condition successfully asserts that /foo.html exists. Since this is OR'd with the next condition, the 3rd condition is skipped.
  4. The 4th condition successfully asserts that /foo.html exists (again).
  5. Since the conditions are successful then the request is rewritten to foo.html.

On the other hand, if you request /bar and only /bar.php exists then...

  1. The RewriteRule pattern successfully matches the request and captures bar in the $1 backreference.
  2. The 1st condition stores html in the %1 backreference.
  3. The 2nd condition fails to assert that /bar.html exists.
  4. The 3rd condition stores php in the %1 backreference. (Essentially overwriting step#1)
  5. The 4th condition successfully asserts that /bar.php exists.
  6. Since the conditions are successful then the request is rewritten to bar.php.
1
  • I've updated my answer to include an explanation as to how the combined rule works.
    – MrWhite
    Feb 1, 2022 at 23:49

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.