1

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?

5
  • 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?)
    – MrWhite
    Feb 23, 2022 at 3:17
  • @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
    – caffeine
    Feb 23, 2022 at 9:24
  • 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?)
    – MrWhite
    Feb 23, 2022 at 9:49
  • 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)
    – caffeine
    Feb 23, 2022 at 9:59
  • I've updated my answer with a complete solution based on your comments.
    – MrWhite
    Feb 23, 2022 at 18:59

2 Answers 2

1

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.)

1
  • 1
    Thanks for the extensive explanations. This definitely solved my problem and brought some very useful info on this topic.
    – caffeine
    Feb 25, 2022 at 9:28
0

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
...

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .