16 May
2011
In my opinion, the fastest and best way to run a PHP application is with Nginx and PHP-FPM. I've been using this setup for years. Before PHP-FPM, it was spawn-fcgi, but the principle is still the same.
When we discovered a particularly disturbing bug in our deployment of 15Seconds.me, it forced me to reconsider ever using this configuration again.
The culprit was this (very common, but wrong) little snippet of configuration logic:
# this serves static files that exist without running other rewrites
if (-f $request_filename) {
break;
}
# this sends all non-existing file or directory requests to index.php
if (!-e $request_filename) {
rewrite ^(.+)$ /index.php?$1 last;
}
All this does is route all requests except those for static resources such as images, css, raw html, and etc to index.php for processing. This allows us to easily take advantage of popstate for frontend navigation with ajaxy goodness, while maintaining graceful degredation on the backend for browsers without HTML5 support and search engine spiders.
Guess what. That method causes a very difficult to detect bug that can very easily bring you to the edge of insanity trying to hunt it down.
We do most of our ajax requests through a PHP file called, appropriately, famousapi.php. Late Friday night, Danny was stubbing out some functionality that resulted in some data being inserted into our MongoDB instance if index.php was called under the right circumstances. Before the data was inserted, a call was made to famousapi.php, and if everything looked good, the data was inserted.
We saw that the data was actually being inserted twice. Cue about 2 hours of WTF'ing, rolling back deployments, rolling forward deployments, commenting out code, and the invention of at least 3 new ways to combine 4 letter words. Finally, we got to the point where the only possible explanation was that index.php was actually being silently called (but never rendered) by Nginx. On. Every. Single. (dynamic) Call. To. The. Server. Static resource requests did not experience this problem.
This is likely a consequence of if being considered evil by Nginx. Upon discovering that this morning, I have modified the configuration chunk above to use this cute little one-liner instead.
Solution:
try_files $uri /index.php?$args;
Well, that was embarrassing. Unfortunately, there are dozens of blog posts around the internet holding up the method I posted at the top of this post as "the right way" to do this, so I'm guessing a lot of people have this bug in their deployments and don't even know it. If nothing else, this is definitely a better way to acheive the desired behavior and it's definitely worth the quick change to your config file.
If you're using this configuration, make sure that you have a favicon.ico in your root directory as well. Most browsers will request this by default and when that request 404's, your index.php will still end up being requested again - even with the above fix in place. Also, you should make sure you're not exposed to this vulnerability.