4
\$\begingroup\$

I have a personal website that's used primarily for fun. I upload images, videos and text that I want to share. An HTML submission form accepts questions and string submissions from users, which uses a phpmyadmin database table for storage.

The below snippet is my current .htaccess file. https://gtmetrix.com/ notes that redirects are the largest culprit in slowing down my page loads, but I'm unsure of how to streamline them.

RewriteEngine On

#REDIRECT TO SECURE HTTPS CONNECTION
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

#FORCE WWW TO NON-WWW
RewriteCond %{HTTP_HOST} ^www.MYDOMAIN.com [NC]
RewriteRule ^(.*)$ https://MYDOMAIN.com/$1 [L,R=301]

#URL EXTENSION REMOVAL
RewriteCond %{THE_REQUEST} /([^.]+)\.html [NC]
RewriteRule ^ /%1 [NC,L,R]
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^ %{REQUEST_URI}.html [NC,L]

#HOTLINKING PROTECTION
    #NOTE: having |html| and |htm| included prevented access of the site through browser search, so i removed them.
RewriteCond %{HTTP_REFERER} !^https://(www\.)?MYDOMAIN\.com(/.*)*$ [NC]
RewriteCond %{HTTP_REFERER} !^$
RewriteRule \.(css|flv|gif|ico|jpe|jpeg|jpg|js|mp3|mp4|php|png|pdf|swf|txt)$ - [F]

#CONTENT SECURITY POLICY
<FilesMatch "\.(html|php)$">
    Header set Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data: 'unsafe-inline'; media-src 'self' data: 'unsafe-inline'; connect-src 'self';"
</FilesMatch>

#REDIRECT FOR DATE PAGE
Redirect /date /storage/date-202010

#REDIRECT FOR HOME PAGE
Redirect /home /

#CUSTOM ERROR PAGES
ErrorDocument 400 /allerror.php
ErrorDocument 401 /allerror.php
ErrorDocument 403 /allerror.php
ErrorDocument 404 /allerror.php
ErrorDocument 405 /allerror.php
ErrorDocument 408 /allerror.php
ErrorDocument 500 /allerror.php
ErrorDocument 502 /allerror.php
ErrorDocument 504 /allerror.php

#PREVENT DIRECTORY BROWSING
Options All -Indexes

#FILE CACHING
    #cache html and htm files for one day
<FilesMatch "\.(html|htm)$">
Header set Cache-Control "max-age=43200"
</FilesMatch>
    #cache css, javascript and text files for one week
<FilesMatch "\.(js|css|txt)$">
Header set Cache-Control "max-age=604800"
</FilesMatch>
    #cache flash and images for one month
<FilesMatch "\.(flv|swf|ico|gif|jpg|jpeg|mp4|png)$">
Header set Cache-Control "max-age=2592000"
</FilesMatch>
    #disable cache for script files
<FilesMatch "\.(pl|php|cgi|spl|scgi|fcgi)$">
Header unset Cache-Control
</FilesMatch>

#BLOCKS FILE TYPES FOR USERS
<FilesMatch "\.(htaccess|htpasswd|ini|log|sh|inc|bak)$">
Order Allow,Deny
Deny from all
</FilesMatch>

UPDATE

I have created a new post integrating an HSTS and many of the changes that were recommended by Mr. White. The bounty has been awarded. Please direct any further feedback to the New Post.

\$\endgroup\$
4
+50
\$\begingroup\$

https://gtmetrix.com/ notes that redirects are the largest culprit in slowing down my page loads

gtmetrix.com's "suggestion" in this regard is arguably "incorrect" (or rather, not nearly as serious as it implies), assuming you are already consistently linking to the canonical URL*1 throughout your site (and you have no other redirects in your application code). These redirects are only likely to affect a "very small fraction" of your site visitors on their very first visit.

(*1 Canonical URL being HTTPS + non-www + no .html extension.)

You have 3 external redirects in the .htaccess code you posted:

#REDIRECT TO SECURE HTTPS CONNECTION
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

#FORCE WWW TO NON-WWW
RewriteCond %{HTTP_HOST} ^www.example.com [NC]
RewriteRule ^(.*)$ https://example.com/$1 [L,R=301]

#URL EXTENSION REMOVAL
RewriteCond %{THE_REQUEST} /([^.]+)\.html [NC]
RewriteRule ^ /%1 [NC,L,R]

If you have implemented HSTS then you need to redirect from HTTP to HTTPS on the same host, before canonicalising the www subdomain - which is what you are doing above in the first rule. This is a requirement of HSTS and the "preload list". So you cannot avoid having at least 2 redirects (worst case) in this scenario.

However, if you have no intention of implementing HSTS then you can combine the first two redirects into one. Which you can do by simply reversing the order of the first two rules. For example:

#FORCE WWW TO NON-WWW
RewriteCond %{HTTP_HOST} ^www\.example\.com [NC]
RewriteRule ^ https://example.com%{REQUEST_URI} [L,R=301]

#REDIRECT TO SECURE HTTPS CONNECTION
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

The first rule, that redirects www to non-www, also redirects to HTTPS, so there is never a need to execute the 2nd redirect as well. So, there is only ever 1 redirect to canonicalise HTTPS and non-www.

I also removed the redundant capturing subpattern (ie. (.*)) in the "HTTP to HTTPS" RewriteRule pattern, since you are using the REQUEST_URI server variable instead. And changed the other "www to non-www" redirect to be consistent. Note that the REQUEST_URI server variable contains the full URL-path, including the slash prefix, whereas the captured backreference omits the slash prefix.

The above two rules could be combined into a single (marginally more complex) rule, but there is no benefit in doing so.

The rules could also be made more "generic", without having to explicitly state the canonical hostname. However, how you implement this and whether this is easily possible is dependent on whether you have other subdomains or not. But again, this serves no "benefit", other than being more copy/pastable. Generally, it is preferable to be explicit here - less prone to error.

#URL EXTENSION REMOVAL
RewriteCond %{THE_REQUEST} /([^.]+)\.html [NC]
RewriteRule ^ /%1 [NC,L,R]

You can also avoid the .html "extension removal redirect" from triggering an additional redirect by including this redirect first (before the above two canonical redirects) and redirecting directly to HTTPS and non-www (the canonical scheme+hostname) as part of the redirect.

UPDATE: This should also be a 301 (permanent) redirect, not a 302 (temporary) redirect that it is currently. A 301 redirect is cached by the browser by default, so it avoids unnecessary round trips to the server. When you don't explicitly include the status code with the R flag, it defaults to a 302.

The NC flag is also not required on the RewriteRule directive, since you are not matching anything here that is case-sensitive.

This rule to remove the .html extension probably works OK for your URLs, however, it's not necessarily correct and could possibly be made more efficient. The reason for checking against THE_REQUEST server variable, as opposed to the RewriteRule pattern or REQUEST_URI server variable is to avoid a potential redirect loop by preventing rewritten requests from being redirected. This is because THE_REQUEST does not change after the request is rewritten - it contains the first line of the HTTP request headers. However, THE_REQUEST also contains the query string, so it's possible for a legitimate request that contains .html as part of the query string to be incorrectly redirected.

For example, request example.com/?p1=foo.html&p2=bar (the homepage with a query string and URL parameters containing the value foo.html) and this will be incorrectly redirected to example.com/?p1=foo, truncating the query string.

The regex /([^.]+)\.html will also fail to match any URL that contains dots as part of the URL-path in places other than the file extension. eg. A request for /foo.bar.html would not be redirected. Although this may be perfectly OK for the URLs on your site.

To avoid these "incorrect" redirects you could capture the URL-path from the RewriteRule pattern instead and either use a simpler condition and check against THE_REQUEST (to avoid a loop) or use the REDIRECT_STATUS environment variable instead, which is always empty on direct requests.

For example:

#URL EXTENSION REMOVAL
RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteRule (.+)\.html$ https://example.com/$1 [NC,R=301,L]

This captures the URL-path before the .html file extension using the RewriteRule pattern (which naturally excludes the query string). The simple condition that checks the REDIRECT_STATUS env var prevents a redirect loop.

Bringing the above points together we have:

#URL EXTENSION REMOVAL
RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteRule (.+)\.html$ https://example.com/$1 [NC,R=301,L]

#FORCE WWW TO NON-WWW
RewriteCond %{HTTP_HOST} ^www\.example\.com [NC]
RewriteRule ^ https://example.com%{REQUEST_URI} [L,R=301]

#REDIRECT TO SECURE HTTPS CONNECTION
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

The NC flag was not required in the "URL extension removal" redirect.

This now triggers at most 1 redirect regardless of whether a request comes in for HTTP, www or includes the .html extension. However, as noted, this is at the expense of not satisfying the requirements for HSTS.

And it should be noted, that in real terms, there may not be a perceivable difference between 1, 2 or even 3 redirects here. Particularly since it will not affect the vast majority of visitors anyway.


Additional:

#REDIRECT FOR DATE PAGE
Redirect /date /storage/date-202010

#REDIRECT FOR HOME PAGE
Redirect /home /

Generally, you should avoid mixing redirects from both mod_alias (Redirect / RedirectMatch) and mod_rewrite (RewriteRule). The two modules run independently and at different times during the request, despite the apparent order of the directives in the .htaccess file. mod_rewrite runs first. So, you can get unexpected conflicts.

Note also that Redirect is prefix-matching and everything after the match is appended on to the end of the target URL. eg. /date/foo would get redirected to /storage/date-202010/foo by the first rule. These particular redirects are also 302 (temporary) redirects. It looks like they should be 301 (permanent)?

However, in this case it probably does not matter whether you use Redirect or RewriteRule, but as a general rule, if you are using mod_rewrite for some redirects, then use mod_rewrite for all redirects. For example:

#REDIRECT FOR DATE PAGE
RewriteRule ^date$ /storage/date-202010 [R=301,L]

#REDIRECT FOR HOME PAGE
RewriteRule ^home$ / [R=301,L]

#BLOCKS FILE TYPES FOR USERS
<FilesMatch "\.(htaccess|htpasswd|ini|log|sh|inc|bak)$">
Order Allow,Deny
Deny from all
</FilesMatch>

You stated in comments that you are using Apache 2.4, however Order, Allow and Deny are Apache 2.2 directives and are formerly deprecated on Apache 2.4. They still work, but for backwards compatibility only and should be updated as soon as.

Note that you need to update all instances on your system since the newer directives don't necessarily mix well.

On Apache 2.4 you would use the Require directive instead:

#BLOCKS FILE TYPES FOR USERS
<FilesMatch "\.(ht[ap]|ini|log|sh|inc|bak)$">
Require all denied
</FilesMatch>

Note that the Apache server config "should" already be blocking direct access to .htaccess and .htpasswd files, but better to be safe I guess.


ErrorDocument 500 /allerror.php

Defining the 500 ErrorDocument late in .htaccess is probably too late to catch most 500 (Internal Server Error) responses (that result from misconfigurations). There's probably not much you can do about this, but it would be preferable to define this earlier in the server config (or <VirtualHost> container) in order to be more "useful".

| improve this answer | |
\$\endgroup\$
  • 2
    \$\begingroup\$ Updated my answer to address potential issues/optimisations with the redirect that removes the ".html" extension. (There are also potential / more serious issues with the internal rewrite that appends the .html extension. I'll update my answer again later to address this.) \$\endgroup\$ – MrWhite Oct 17 at 0:29
  • \$\begingroup\$ I've created a new post integrating many of your changes and HSTS, and would appreciate any further recommendations you may have being directed to there. \$\endgroup\$ – CodeLoveGuy Oct 17 at 19:15

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.