Score:1

.htaccess mod_rewrite not catching all RewriteRules

de flag

There is a PHP application with a PHP router as entry point for all the requests placed inside index.php. I am trying to write a .htaccess file to forward every request to index.php except for API requests and for design resources. So I am trying to obtain the following behavior:

  1. example.com/api/v1/* should serve api_v1.php
  2. example.com/any_path/resource.css => should serve resource.css if it exists (there are multiple extensions allowed; .css is just one example)
  3. serve index.php for anything that did not fall for the conditions above

Given that .htaccess is evaluated from top to the bottom, from particular to general conditions and that flag [L] would break the execution if anything matched, I have managed to come up with the following .htaccess:

RewriteEngine On

# Prevent 301 redirect with slash when folder exists and does not have slash appended
# This is not a security issue here since a PHP router is used and all the paths are redirected
DirectorySlash Off

#1. Rewrite for API url
RewriteRule ^api/v1/(.*)$ api_v1.php [L,NC]

#2. Rewrite to index.php except for for design/document/favicon/robots files that exists
RewriteCond %{REQUEST_URI} !.(css|js|png|jpg|jpeg|bmp|gif|ttf|eot|svg|woff|woff2|ico|webp|pdf)$
RewriteCond %{REQUEST_URI} !^(robots\.txt|favicon\.ico)$ [OR]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php [L]

#3. Rewrite enything else
RewriteRule ^(.*)$ index.php [L]

Using the code above, it seems that accessing example.com/api/v1/ does not execute api_v1.php. Instead, it would continue and execute index.php

example.com/api/v1/ would only work if I remove all conditions after line 8.

What am I doing wrong here?

kz flag
It's a little unclear exactly what you are trying to do. Should certain requests be rewritten to `index.php` even if they exist as a physical file, but not one of the file types stated? Should requests for resources (`.css`, `.jpg`, etc.) still be rewritten to `index.php` if they don't exist? (ie. do you have legitimate URLs that have a "file extension" like this?)
caffeine avatar
de flag
@MrWhite Yes, if a .css file is not found, rewrite to index.php so I can properly handle 404 error from index.php. If .css file exists serve it, otherwise rewrite to index.php
kz flag
But what about file types you've not explicitly stated? Should these be routed through `index.php` regardless of whether they exist or not? (Do you need to explicitly state a list of known file types?)
caffeine avatar
de flag
Yes, whatever is NOT in that list, whether it exists or not, it should be rewritten to index.php. The only complication here is that I also rewrite to index.php for the file names that are in the list but NOT existing on server. Basically, I try to serve directly .css files if these exists, otherwise rewrite everything to index.php (referring to conditions #2 and #3)
kz flag
I've updated my answer with a complete solution based on your comments.
Score:1
kz flag

it seems that accessing site.com/api/v1/ does not execute api_v1.php. Instead it would continue and execute index.php

Yes, because the first rule rewrites the request to api_v1.php and this ends up getting rewritten to index.php by the second rule (because php is not in the list of excluded extensions in the first condition, the second condition is also successful so the third condition is not processed - see below) during the second pass of the rewrite engine. In a directory context (ie. .htaccess) the L flag does not stop all processing, it just stops the current pass through the rewrite engine. The rewrite engine loops until the URL passes through unchanged.

You could resolve this on Apache 2.4 by using the END flag, instead of L, on the first rule to stop all processing by the rewrite engine.

For example:

RewriteRule ^api/v1/ api_v1.php [NC,END]

(I removed the trailing (.*)$ on the regex since it's not necessary here and makes the regex marginally less efficient.)

However, the fact that a request for an existing .php file is rewritten by your later rules really demonstrates that your second and third rules do not seem to be working as intended...

#2. Rewrite to index.php except for for design/document/favicon/robots files that exists
RewriteCond %{REQUEST_URI} !.(css|js|png|jpg|jpeg|bmp|gif|ttf|eot|svg|woff|woff2|ico|webp|pdf)$
RewriteCond %{REQUEST_URI} !^(robots\.txt|favicon\.ico)$ [OR]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php [L]

#3. Rewrite enything else
RewriteRule ^(.*)$ index.php [L]

If you request something.php then the first condition is successful (because .php is not included in the list). The second condition is also successful (it's not robots.txt or favicon.ico). Since the second condition is explicitly OR'd with the third condition, the third condition is not processed. All the conditions are successful and the request is rewritten to index.php (regardless of whether something.php exists or not).

If, on the other hand, you request resource.css then the first condition fails (because it has a .css extension). Since this condition is implicitly AND'd, no further conditions are processed and the rule is not triggered. The third rule then unconditionally rewrites resource.css to index.php, regardless of whether it exists or not!

So, this doesn't check that a requested resource (css, jpg, etc.) "exists" as would seem to imply by the preceding comment. The third condition (that checks the request does not map to a file) is only processed when the preceding OR'd condition fails AND the first condition is successful. In other words, the 3rd condition is only processed when you request /robots.txt - which I'm sure is not your intention.

In other words, rules #2 and #3 (despite their apparent complexity) would seem to be rewriting pretty much everything to index.php.

Complete solution

Based on your additional comments:

  • For a subset of file types, serve the resource - if it exists. If the resource does not exist then send the request to index.php.

  • For other file types, send the request to index.php, regardless of whether that resource exists or not.

  • A few requests (ie. /robots.txt and /favicon.ico) should not be sent to index.php.

Try the following instead:

# Prevent 301 redirect with slash when folder exists and does not have slash appended
# This is not a security issue here since a PHP router is used and all the paths are redirected
DirectorySlash Off

# Since "DirectorySlash Off" is set, ensure that mod_auotindex directory listings are disabled
Options -Indexes

RewriteEngine On

#1. Rewrite for API url
RewriteRule ^api/v1/ api_v1.php [NC,END]

#2. Known URLs/files are served directly
RewriteRule ^(index\.php|robots\.txt|favicon\.ico)$ - [END]

#3. Certain file types (resources) are served directly if they exist
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule \.(css|js|png|jpe?g|bmp|gif|ttf|eot|svg|woff|woff2|ico|webp|pdf)$ - [END]

#4. Rewrite everything else
RewriteRule ^ index.php [END]

The rules finish early for anything that needs to be served directly, so we only need to rewrite to index.php once in the last rule.

Note that I've included index.php in rule #2. This is an optimisation, although not strictly necessary if using the END flag (as opposed to L) on the last rule.

Optionally, you might choose to redirect any direct requests to index.php back to the root (to canonicalise the URL for search engines). This is just in case /index.php gets exposed (or guessed) to the end user.

For example, add the following after the first rule for the API:

#1.5 Redirect direct requests to "/index.php" back to "/" (root)
RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteRule ^index\.php$ / [R=301,L]

The condition that checks against the REDIRECT_STATUS environment variable is necessary to prevent a redirect loop - to avoid redirecting the rewritten URL. (Although, again, this is not strictly required if using the END flag on the last/rewrite rule.)

caffeine avatar
de flag
Thanks for the extensive explanations. This definitely solved my problem and brought some very useful info on this topic.
Score:0
ng flag

Try to place a rewrite condition before each rule and then break the rule. In other words every condition needs to have it own rule.

condition -> rule
condition -> rule
...

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.