3

I have already remapped URL's from for example /sub/test/ to test.html but I also want to redirect /sub/test.html to /sub/test/ but here seems to be a clash between the rules.

.htaccess files is in the /sub directory and should only apply to /sub directory and any sub-directories under it.

Here's the .htaccess file:

<IfModule mod_rewrite.c>
Options -MultiViews -Indexes
RewriteEngine On
RewriteBase /sub

# redirect urls without trailing slash
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !index.html
RewriteCond %{REQUEST_URI} !(.*)/$
RewriteRule (.*)$ $1/ [L,R=301]

# redirect .html to url
#RewriteCond %{REQUEST_FILENAME} !-d
#RewriteCond %{REQUEST_FILENAME} -f
#RewriteRule (.*)\.html$ $1/ [L,R=301]

# remap url to a .html file
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule (.*)/$ $1.html [L]

</IfModule>

Logic is that /sub/test.html path gets redirected to /sub/test/ and that is remapped back to /sub/test.html internally.

.htaccess files is in the /sub directory and should only apply to /sub directory and any sub-directories under it.

The commented lines cause a redirect loop to /sub/test/ which is the desired url. If the only the commented lines are left there is no redirect loop so it seems that there is a conflict between the rules. What is causing the redirect loop?

Non-existent file redirect loop:

How to deal with non-existing file redirect loops? for example /web.html redirects to /web/ while non-existing /web2.html end up looking like /web.html.html.html....

  • Which version of Apache are you using? – MrWhite Mar 10 '16 at 20:51
  • 1
    @w3dk Apache 2.2.29 – DominicM Mar 11 '16 at 12:22
1

What is causing the redirect loop?

When the "last directive" executes, the rewriting process doesn't suddenly stop completely, only the current pass stops. The whole rewriting process starts again from the top! The process only stops completely when the URL passes through unchanged (or when it hits the END flag in Apache 2.4, as Ivo van der Veeken mentions in his answer).

The "last directive" is either the last directive in the file, or a directive that gets processed with the L (last) flag.

RewriteRule (.*)/$ $1.html [L]

So, when the above directive executes, the rewriting process starts again, but this time gets caught by the (commented out) directives above, which strips the .html (just added) and redirects. On the next request, the .html is added again (for the internal rewrite), but the rewriting process starts again, strips the .html and redirects, etc. etc. etc.

To break the loop you can either use the END flag (Apache 2.4+) as mentioned above, instead of L. Or, use an additional condition (RewriteCond directive) on your .html redirect that checks against THE_REQUEST. This server variable holds the value of the initial request (the actual request header sent from the client), not the rewritten request, so it will fail to match when the request is rewritten thus breaking the "loop". This works on all versions of Apache. So, try something like:

# redirect .html to url
RewriteCond %{THE_REQUEST} ^[A-Z]+\ /.+\.html\ HTTP/
#RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule (.+)\.html$ $1/ [L,R=301]

The above condition makes sure that the .html exists on the initial request only, not the rewritten request. I don't think you really need to check that it's not a directory, unless you have directories that are named <something>.html?!

THE_REQUEST looks something like:

GET /sub/test.html HTTP/1.1

Non-existent file redirect loop:

# remap url to a .html file
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule (.*)/$ $1.html [L]

Your rewrite directives append .html to the request whenever the requested file does not exist. So, does-not-exist/ gets rewritten to does-not-exist.html which gets externally redirected (by your first rule) to does-not-exist.html/ which gets rewritten to does-not-exist.html.html, etc. etc.

You can include an additional check to make sure that the rewritten file would exist before actually rewriting to it. eg. RewriteCond %{DOCUMENT_ROOT}/sub/$1.html -f - the additional complexity is because your URLs end with a slash. In context:

# remap url to a .html file
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{DOCUMENT_ROOT}/sub/$1.html -f
RewriteRule (.*)/$ $1.html [L]

EDIT: To make this more generic (to avoid having to include the /sub subdirectory), you could try changing the 3rd condition to:

RewriteCond %{REQUEST_FILENAME}.html -f

This relies on Apache internally stripping trailing slashes from the request.

make it more portable by making the RewriteBase dynamic

Providing this .htaccess file is in the same directory that you are specifying for the RewriteBase directive (which it is in this instance), then you could dynamically assign the subdirectory to an environment variable and use that in your RewriteRule substitutions.

# Removed...
#RewriteBase /sub

# Assign the accessed subdirectory to an environment variable
RewriteCond %{REQUEST_URI} ^(/[^/]+)/
RewriteRule ^ - [E=SUBDIR:%1]

# Use this environment variable as a prefix to your substitutions
:
RewriteRule (.*)$ %{ENV:SUBDIR}/$1/ [L,R=301]

Incidentally, this is only required for your external redirects. You do not need this for internal rewrites in per-directory .htaccess files, since the directory-prefix is automatically added back to relative substitutions.

That would mean that /sub directory could be directory with any name or even multiple sub-directories.

For multiple subdirectories you could change the CondPattern with something like:

RewriteCond %{REQUEST_URI} ^(/.+)/

By default the regex is greedy, so everything between the first and last slash is captured. However, this has potential to break on some URLs/servers and could possibly open you up to an XSS attack, so I would code for your specific requirement, rather than being too generic.

  • <something>.html directories are possible so I will leave it in for robustness. How does THE_REQUEST AND REQUEST_FILENAME differ exactly? I cant find much info on it. – DominicM Mar 11 '16 at 13:04
  • Also see edit, maybe you can chime in. – DominicM Mar 11 '16 at 13:10
  • 1
    The REQUEST_FILENAME is the absolute filename on the filesystem that the request maps to. Whereas THE_REQUEST is the request header (containing the URL) as sent from the client - example shown above. Crucially, REQUEST_FILENAME gets updated as the URL is rewritten, which is contributing to the redirect loop. – MrWhite Mar 11 '16 at 13:10
  • I've updated my answer with regards to your "non-existent file redirect loop" update. Incidentally, I also mentioned this in a comment on your other question. – MrWhite Mar 11 '16 at 13:33
  • You are correct, the comment was sufficient, I just missed that part. Also is there a way to insert /sub dynamically with a variable instead of hard coding it? Would make it more reusable/portable. – DominicM Mar 11 '16 at 14:25
2

First off, RewriteCond %{REQUEST_FILENAME} !index.html won't work, you'll need !"index\.html", with the quotes. Otherwise it's not a valid regex.

According to the docs, you should use the [END] flag instead of [L], to prevent further request processing (internally, your rewrite is reevaluated by the Apache redirect rules). Apparently, there's a bug with [END] that's only been fixed in 2.4.9 though, so you may have to find another way.

I'd also switch the order of the statements. Most of your links should point to the clean URL, so this should be a bit faster

# remap url to a .html file
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule (.*)/$ $1.html [END]

# redirect .html to url
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule (.*)\.html$ $1/ [L,R=301]
  • There's nothing invalid with !index.html (except that the dot should be escaped). The quotes surrounding the regex are not required here. Quoting the string/regex is only "required" in Apache config files if you have unescaped spaces - in this sense they are just a convenience, since they can always be avoided by simply escaping the space. – MrWhite Mar 10 '16 at 20:51
0

Here is the final .htaccess that works almost perfectly.

<IfModule mod_rewrite.c>
Options -MultiViews -Indexes
RewriteEngine On
#RewriteBase /sub

# Assign the accessed subdirectory to an environment variable
RewriteCond %{REQUEST_URI} ^(/[^/]+)/
RewriteRule ^ - [E=SUBDIR:%1]

ErrorDocument 404 /%{ENV:SUBDIR}/404.html

# redirect urls without trailing slash
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !index\.html
RewriteCond %{REQUEST_URI} !(.*)/$
RewriteRule (.*)$ %{ENV:SUBDIR}/$1/ [L,R=301]

# redirect index.html to sub root
RewriteCond %{THE_REQUEST} index\.html
RewriteRule ^index.html$ /%{ENV:SUBDIR}/ [L,R=301]

# redirect .html to url
RewriteCond %{THE_REQUEST} (.*)\ /%{ENV:SUBDIR}/.+\.html
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule (.*)\.html$ $1/ [L,R=301]

# remap url to a .html file
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{DOCUMENT_ROOT}/%{ENV:SUBDIR}/$1.html -f
RewriteRule (.*)/$ $1.html [L]

</IfModule>

The only change that would be nice is to make it more portable by making the RewriteBase dynamic. I am sure there is a way to dynamically get the /sub directory and use it instead of setting the RewriteBase. That would mean that /sub directory could be directory with any name or even multiple sub-directories.

  • I had already edited/added code to my answer to remove the subdirectory (/sub) from the THE_REQUEST and DOCUMENT_ROOT conditions - did these work? I've further updated my answer with regards to making the "RewriteBase dynamic" - by "dynamically" assigning the subdirectory to an environment variable. I see you've changed the order of the directives as suggested by Ivo - personally, I wouldn't do this. Performance is not an issue here. However, readability and debugging is. The natural order is external redirects before internal rewrites. – MrWhite Mar 14 '16 at 11:51
  • @w3dk I didn't see the extended edit till now. RewriteCond %{REQUEST_FILENAME}.html -f - I don't see how this would possibly make it work. Where do you set the /sub variable / ENV:SUBDIR? Within the .htaccess or apache config? I changed the order back as suggested. – DominicM Mar 14 '16 at 14:12
  • 1
    # Assign the accessed subdirectory to an environment variable - this little rule block creates an environment variable called SUBDIR. You then access the value contained in this variable using %{ENV:SUBDIR} elsewhere in the config file. The only documentation is in my answer I'm afraid, unless you're after some general info about setting environment variables with mod_rewrite using the E flag? – MrWhite Mar 15 '16 at 11:19
  • 1
    "if RewriteBase is removed" - yes, because in per-directory .htaccess files the directory-prefix (the absolute filesystem path that leads to that .htaccess file) is added back to relative (not starting with a slash or scheme) substitutions / target URLs. For internal rewrites this is correct. However, this naturally breaks external redirects (which require an absolute URL, not a filesystem path). By using this environment variable we are helping to create this absolute URL, so the RewriteBase directive is redundant. (I'm pretty sure the env var can't be used in the RewriteBase cmd?) – MrWhite Mar 15 '16 at 12:30
  • 1
    Note that the way I wrote the code, SUBDIR contains the slash prefix. ie. /sub, not sub. So you shouldn't include the slash prefix in the substitutions. To resolve your redirects (R=301) simply change RewriteRule (.*)$ $1/ [L,R=301] to RewriteRule (.*)$ %{ENV:SUBDIR}/$1/ [L,R=301]. But this is not required for internal rewrites (ie. the directive that simply ends [L]). Note that you can't use server variables (ie. env vars) in the CondPattern (2nd argument to the RewriteCond directive). See the code in my answer, ie. RewriteCond %{THE_REQUEST} ^[A-Z]+\ /.+\.html\ HTTP/. – MrWhite Mar 15 '16 at 12:38

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.