.htaccess mod_rewrite too many redirects

Just to make things clear, this is NOT a duplicate question like this one as core of question is different. What I'm trying to do is redirect users who try to access a site using a language code (typically en or cz) to index.php with language variable set. The site itself can recognise only English and Czech languages settings, every other is redirected based on visitor's IP geolocation (using an external online JSON API). But the problem is, that browser keeps redirecting on and on and it returns an error that too many redirects occurred. What's wrong? Tried to seek for an error, but couldn't see any.

This is complete .htaccess file:

Options -Indexes

RewriteEngine on
RewriteRule ^[a-z]{2,3}/$ /index.php?lang=$1
RewriteRule ^blog/?(.*)$ //blog.czghost.czlh/$1 [R=301]
RewriteRule ^ftp/?(.*)$ //ftp.czghost.czlh/$1 [R=301]

Also, if user writes in index.php?lang=$1, I need to redirect him to the nice URL variant instead. I've searched online, but it seems too complicated to me :(

I'm testing sites in my WAMP server on my Windows machine, before it's ready to deploy online.


Solution 1:

You've not actually stated the URL(s) you are requesting that trigger the redirect loop?

However, from the directives you have posted, it would seem that any request of the form /blog<anything> or /ftp<anything> would result in a redirect loop.

RewriteRule ^blog/?(.*)$ //blog.czghost.czlh/$1 [R=301]
RewriteRule ^ftp/?(.*)$ //ftp.czghost.czlh/$1 [R=301]

Apache mod_rewrite does not support protocol-relative URLs in the RewriteRule substitution - they are simply seen as root-relative (ie. a URL-path relative to the document root). You presumably see the result of this redirect loop in the address bar of your browser? It will go something like this:

  • Initial request: http://example.com/blog/xyz
  • Redirects to http://example.com//blog.czghost.czlh/xyz
  • Redirects to http://example.com//blog.czghost.czlh/.czghost.czlh/xyz
  • Redirects to http://example.com//blog.czghost.czlh/.czghost.czlh/.czghost.czlh/xyz
  • etc.

You need to include the protocol. For example:

RewriteRule ^blog/?(.*)$ https://blog.czghost.czlh/$1 [R=301,L]

I've also included the L (last) flag, since you want the redirect to trigger immediately, not run the risk of being further rewritten. This should really come before the internal rewrite (that rewrites to index.php).

You will need to make sure you have cleared the browser cache, as the erroneous 301 (permanent) redirect will have been cached by the browser. It can be easier to test with 302 (temporary) redirects for this reason.

Your blog and ftp rules can be handled by a single directive, using alternation. For example:

RewriteRule ^(blog|ftp)/?(.*)$ https://$1.czghost.czlh/$2 [R=301,L]

RewriteRule ^[a-z]{2,3}/$ /index.php?lang=$1

You don't have a capturing group (ie. parenthesised subpattern) in the RewriteRule pattern, so the $1 backreference will always be empty. This should be rewritten as:

RewriteRule ^([a-z]{2,3})/$ /index.php?lang=$1 [L]

Again, I've included the L flag, in order to prevent further rewriting. Note that this expects there to be a slash suffix on the end of the URL path. So, only URLs of the form /blog/en/ (with a slash suffix) would be successfully rewritten (although maybe this is unrelated).

So, in summary, this should be rewritten something like:

RewriteRule ^(blog|ftp)/?(.*)$ https://$1.czghost.czlh/$2 [R=301,L]
RewriteRule ^([a-z]{2,3})/$ /index.php?lang=$1 [L]

Also, if user writes in index.php?lang=$1, I need to redirect him to the nice URL variant instead.

You can do this with an additional redirect before the above directives. For example:

RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteCond %{QUERY_STRING} ^lang=([a-z]{2,3})$
RewriteRule ^(index\.php)?$ /%1/? [R=301,L]

You need to use a RewriteCond directive to check the query string against the QUERY_STRING server variable (this can't be done in the RewriteRule directive). The preceding check against the REDIRECT_STATUS environment variable is to make sure we only process initial requests and not rewritten requests by the later directive that rewrites the URL to /index.php?lang=xyz. The REDIRECT_STATUS env var is empty on the initial request, but gets set to "200" (as in 200 OK - HTTP status) after the first successful rewrite.

This will match either index.php, or an empty URL path (eg. /?lang=xyz). %1 is a backreference to the last matched CondPattern (as opposed to $N that is a backreference to the RewriteRule pattern). The ? on the end of the RewriteRule substitution is necessary in order to remove the query string from the redirected URL (alternatively you can use the QSD flag on Apache 2.4+).

Again, best to test first with a 302 (temporary) redirect that won't be cached by the browser.

(From comments:) The only thing that works properly, is the /ftp/ redirect. /blog/ redirect stays with unchanged URL in a blank page

You should make sure that there is no physical directory on the filesystem with the same name, ie. /blog. And that this subdirectory doesn't have an additional .htaccess file that could be overriding this. Also, make sure that there isn't a file that shares the same basename blog. eg. blog.html or blog.php etc. If there is then you will need to make sure that MultiViews (part of mod_negotiation) is not enabled. ie. Options -Indexes -MultiViews. If MultiViews is enabled then Apache will make an internal subrequest for /blog.php (for example) before mod_rewrite is able to rewrite the URL.

Summary

Options -Indexes -MultiViews

RewriteEngine on

# Redirect direct requests for "index.php?lang=xyz" to "/xyz/"
RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteCond %{QUERY_STRING} ^lang=([a-z]{2,3})$
RewriteRule ^(index\.php)?$ /%1/? [R=301,L]

# Redirect "blog" or "ftp/<anything>" to subdomain
RewriteRule ^(blog|ftp)/?(.*)$ https://$1.czghost.czlh/$2 [R=301,L]

# Internally rewrite "/xyz/" to "/index.php?lang=xyz"
RewriteRule ^([a-z]{2,3})/$ /index.php?lang=$1 [L]