Currently there's an issue when trying to use the more "efficient" method in nginx.

This is the most efficient method of handling front-controller type scripts:

try_files $uri $uri/ /index.php?q=$uri&$args;

However, because of a small little annoyance in Drupal it doesn't work. The legacy mod_rewrite style method works:

if (!-e $request_filename) {
   rewrite ^/(.*)$ /index.php?q=$1 last;
}

It's because Drupal acts funky:

/index.php?q=/user/20 - does not work
/index.php?q=user/20 - works

As far as I can tell there is no reason that it can't handle these requests the same. For example, putting in this [horrible] hack in /index.php makes things work with try_files:

if(substr($_GET['q'], 0, 1) == '/') { 
   $_GET['q'] = substr($_GET['q'], 1, strlen($_GET['q']));
}

Is this something that can be patched into Drupal 6.x and Drupal 7.x+?

This is the most efficient way of handling things in nginx, according to Igor. I am not well-versed enough into Drupal to suggest the best method for this (it looks like $_GET['q'] is referenced a handful of places) and this should be altered in a place that makes sense (I'm sure just looking at the function tree and finding the first place it starts parsing $_GET['q'] and try this kind of code there?)

Unless someone else has figured out a way to get this to work... I have not figured it out otherwise. nginx has no "slashless" $uri parameter. WordPress handles this every way from what I've been told and from what I've seen...

index.php?q=foo
index.php?q=/foo
index.php/foo

Anyone who knows anything about core or could change something or suggest anything have anything to add? Thanks!

Comments

mike503’s picture

Bump?

Anyone?

everyz’s picture

Why do you think your patch is a bad idea in this case? But have you measured the effect of using try_files instead of rewrite? Is there any gain in speed?

mike503’s picture

Well, it seems like a poor idea to modify the actual $_GET array (although from what I can grok, Drupal does that anyway, it takes it in, does some work, then replaces it with the altered information... IIRC)

Perhaps in the function that does that routine, if it is walking the array, if($index == 'q') { alter me! } - use ltrim($value, '/') or something.

I don't know enough about the internals of Drupal to suggest the best place for this though.

As for nginx, Igor always suggests try_files over an -f/-e/-d test. You would have to examine an strace to see how many syscalls it saves, but it also makes for a much cleaner config file too. It's just the "better way to do things" in nginx and I don't see a reason that Drupal shouldn't support it (a starting / doesn't seem to hurt, and isn't used in $_GET['q'] anyway, why not ltrim it and make it usable both ways?)

brianmercer’s picture

From my testing, this is fixed in D7. Definitely makes for a simpler nginx configuration. Depending on your needs you don't need a rewrite or try_files at all. Can do something like this:

  location / {
    include /etc/nginx/fastcgi_params;
    fastcgi_param SCRIPT_FILENAME /var/www/$host/drupal/index.php;
    fastcgi_param QUERY_STRING q=$uri&$args;
    fastcgi_pass php;
  }
mike503’s picture

Yeah, D7 it works.

Oddly enough, I moved a site back from D7 to D6, and did not change anything, and it seems to work. Curiously enough.

So possibly something changed in the last minor D6 release... dunno.

brianmercer’s picture

Weird, still doesn't work for me with D6. Still get the same 404 with

http://example.com/index.php?q=/

or

http://example.com/

If you find out what's making it work, please let us know.

mike503’s picture

I figured it out.

It works, but it doesn't honor the query string stuff.

i.e. when I do an admin flush -
http://foo.com/admin_menu/flush-cache?destination=admin%2Fbuild%2Fviews

it always wound up going back to the home page - it ignored that destination parameter.

switching back to

if (!-e $request_filename) {
 rewrite ^(.*)$ /index.php?q=$1 last;
}

works for now, as expected.

I think you can also do some methods without having to use rewrite (which honestly, nginx is shared-nothing and horizontally scalable, PHP is shared-nothing and easy to scale horizontally) I don't think it's going to be the culprit for a slow site.

As you said, this could work too, and skip the rewrite: fastcgi_param QUERY_STRING q=$uri&$args;

That is probably a better solution. :)

brianmercer’s picture

I saw your post on the nginx mailing list. It's not about try_files vs. if(-f). We have some sample configs discussed at http://groups.drupal.org/nginx and on github by omega8cc and yhager based on a thread in the Boost issue queue. They use try_files like this:

  location / {
    try_files $uri @drupal;
  }

  location @drupal {
     rewrite ^/(.*)$ /index.php?q=$1 last;
  }

  location ~ \.php$ {
    include /etc/nginx/fastcgi_params;
    fastcgi_param SCRIPT_FILENAME /var/www/$host/drupal$fastcgi_script_name;
    fastcgi_pass php;
  }

This is currently a reliable and compatible way of serving Drupal. The extra redirect thru the @drupal location is required because of Drupal's inflexibility with the query string (www.drupal.org/index.php?q=/) as you've noted. However, since it's an @ location it doesn't get tested on every static file request. As far as I know, this config works for everyone with every contrib module.

The issue seems to be fixed in D7, now in alpha release, and D7 is where all the dev energy is right now. I doubt you'll get a D6 patch at this point, since they're pretty conservative about making changes. They don't want to break things for 99.999% of users who are doing fine.

I'm like you and wanted to streamline as much as possible so I once tried to eliminate all redirects with a configuration like this:

  root /var/www/$host/public;

  location = / { # drupal doesn't like q=/
    include /etc/nginx/fastcgi_params;
    fastcgi_param SCRIPT_FILENAME /var/www/$host/drupal/index.php;
    fastcgi_param QUERY_STRING q=&$args;
    fastcgi_pass php;
  }

  location / {
    include /etc/nginx/fastcgi_params;
    fastcgi_param SCRIPT_FILENAME /var/www/$host/drupal/index.php;
    fastcgi_param QUERY_STRING q=$uri&$args;
    fastcgi_pass php;
  }

So there's an extra exact match (=) location to handle the q=/ situation, but exact match locations are a fast lookup, faster than regex locations, and since a lot of your requests are for the home page, and nginx stops scanning locations when it finds an exact match, it works fine.

And eliminating try_files $uri can be good for security. By default send everything to Drupal and then explicitly allow other files. The usual configuration with ~ \.php$ leaves the possibility of someone uploading a malicious .php file. Also having a try_files $uri location lets people view all the misc .txt, .inc, .gz, .htaccess, .svn files in every directory. So then you need something like this:

   location ~* (/\..*|settings\.php$|\.(htaccess|htpasswd|engine|inc|info|install|module|profile|pl|po|sh|.*sql|theme|tpl(\.php)?|xtmpl)$|^(Entries.*|Repository|Root|Tag|Template))$ {
        deny all;
    }

I also use two directories, one public and one for code so that most code is outside the web root, including the settings.php file. It does require a bunch of symlinks to make it work.

But then...once you add in the Boost module (the equivalent of wp-supercache for creating static cache files on disk and serving them directly through nginx), now you have to double all the Boost cache checking locations, and some quirkiness can creep in through contrib modules. I had some issues with the globalredirect module. So I don't recommend going that route.

I use a hybrid like this now (though more complex when using Boost caching):

  root /var/www/$host/public

  location / {
     rewrite ^/(.*)$ /index.php?q=$1 last;
  }

  location = /index.php {
    include /etc/nginx/fastcgi_params;
    fastcgi_param SCRIPT_FILENAME /var/www/$host/drupal/index.php;
    fastcgi_pass php;
  }

  location ~* ^.+\.(jpg|jpeg|gif|png|ico|css|js)$ {
    access_log      off;
    expires         45d;
  }

  # Allow only these specific file types to be downloaded or 
  #location ~* ^.*/files/.+\.(zip|rar|7z|pdf)$ {}

  # Allow all files in this location to be downloaded
  location ~ ^.*/files/.*$ {}

  # If there's no favicon.ico don't log an error, just send empty response
  location = /favicon.ico {
    try_files /favicon.ico =204;
  }

  location = /robots.txt {}

  # 404s must go to Drupal so imagecache can create the missing images
  location ~ /files/imagecache/ {
    access_log      off;
    expires         45d;
    try_files $uri @drupal;
  }

  location @drupal {
     rewrite ^/(.*)$ /index.php?q=$1 last;
  }

#  Disable after installation
#  location = /install.php {
#    include /etc/nginx/fastcgi_params;
#    fastcgi_param SCRIPT_FILENAME /var/www/$host/drupal/install.php;
#    fastcgi_param QUERY_STRING q=$uri&$args;
#    fastcgi_pass php;
#  }

#  Disable if using Drush for updatedb
#  location = /update.php {
#    include /etc/nginx/fastcgi_params;
#    fastcgi_param SCRIPT_FILENAME /var/www/$host/drupal/update.php;
#    fastcgi_param QUERY_STRING q=$uri&$args;
#    fastcgi_pass php;
#  }

# Disable if using Drush for cron
#  location = /cron.php {
#    include /etc/nginx/fastcgi_params;
#    fastcgi_param SCRIPT_FILENAME /var/www/$host/drupal/cron.php;
#    fastcgi_param QUERY_STRING q=$uri&$args;
#    fastcgi_pass php;
#  }

#  Disable if not using any xmlrpc services
#  location = /xmlrpc.php {
#    include /etc/nginx/fastcgi_params;
#    fastcgi_param SCRIPT_FILENAME /var/www/$host/drupal/xmlrpc.php;
#    fastcgi_param QUERY_STRING q=$uri&$args;
#    fastcgi_pass php;
#  }

I suggest sticking with the extra rewrite for D6 and wait for D7.

hedac’s picture

Hi brian
I tried your configuration.... it seems I always get 404 error in imagecache files that don't exist yet...

trying

if (!-e $request_filename) {
   rewrite ^/(.*)$ /index.php?q=$1 last;
}

and it doesn't work either... blank page...

What can I be doing wrong?
I'm using php 5.3.3 and php5-fpm and nginx/0.7.67

brianmercer’s picture

My mistake. Didn't make it generic enough. Try changing

  # 404s must go to Drupal so imagecache can create the missing images
  location ~ /files/imagecache/ {
    access_log      off;
    expires         45d;
    try_files $uri @drupal;
  }

to this:

  # 404s must go to Drupal so imagecache can create the missing images
  location ~ ^.*/files/imagecache/.+$ {
    access_log      off;
    expires         45d;
    try_files $uri @drupal;
  }

and then relocate it above

  location ~* ^.+\.(jpg|jpeg|gif|png|ico|css|js)$ {
    access_log      off;
    expires         45d;
  }

My files are always at http://mydomain.com/files/imagecache/test.jpg but the default Drupal install is either http://mydomain.com/sites/default/files/imagecache/test.jpg or http://mydomain.com/sites/mydomain.com/files/imagecache/test.jpg

I need to keep an updated example config somewhere.

hedac’s picture

sorry I was having a permissions issue in the server... because I copied the files badly... my bad... so php nginx user couldn't write to a specific imagecache folder... I rebuilt all permissions in files folder and now it works :)

Thank you! Yes.. I would love to see your boost configuration too... not using boost yet but I plan to use it.

brianmercer’s picture

Wordpress definitely has its own quirks. :)

All those .php files (e.g. wp-login.php, edit.php, etc.) require you to use location ~ ^.*\.php$ to catch them all, and then sometimes it uses directories (e.g. /wp-admin/) requiring an index index.php and try_files $uri/.

I prefer Drupal's way of just sending nearly everything as a query string to index.php.

perusio’s picture

like this:

At the http level define a map directive like this:

map $uri $no_slash_uri {
    ~^/(?<no_slash>.*)$ $no_slash;
}

and replace all ocurrences of index.php?q=$uri by index.php?q=$no_slash_uri. It should work for all drupal 6 based sites.

brianmercer’s picture

map $uri&$args $no_slash_uri {
    ~^/(?<no_slash>.*)$ $no_slash;
}

edit: fixed

ispboy’s picture

If nginx fails to start and displays the error message:

pcre_compile() failed: unrecognized character after (?< in ...

this means that the PCRE library is old and you should try the
syntax “?P<name>”.

heldercor’s picture

Why should this be set at the http level? Won't it interfere with other apps?

heldercor’s picture

Ok, I understand now that map is just setting a variable and it won't conflict where it isn't being used.