6

I am running a server on nginx 1.4.1 with PHP-FastCGI. Currently I have it setup so that it removes trailing slashes from my URLs and issues a 301 redirect. However, when I visit a directory that exists, I am forced into a redirect loop. My current document root looks like this:

- index.php (app)
- webgrind
    - index.php
- static 
    - css

Currently I cannot visit example.com/webgrind or any other directory. My access logs repeatedly read similar to:

GET /webgrind/ HTTP/1.1" 301 178 "-"
GET /webgrind  HTTP/1.1" 301 178 "-"

This is the server block in my nginx.conf:

server {
        listen 80;
        server_name example.com;

        location / {
            try_files $uri $uri/ /index.php?$args;
            root /var/www/example/public;
            index  index.php index.html index.htm;
        }

        rewrite ^/(.*)/$ /$1 permanent;

        location = /favicon.ico {
            access_log     off;
            log_not_found  off;
        }

        location ~ \.php$ {
            try_files $uri $uri/ /index.php?$args;
            root /var/www/example/public;
            index index.php index.html index.htm;

            fastcgi_pass   unix:/var/run/php5-fpm.sock;
            fastcgi_index  index.php;
            fastcgi_param  SCRIPT_FILENAME  /var/www/example/public$fastcgi_script_name;
            fastcgi_param  APPLICATION_ENV testing;
            fastcgi_param  PATH /usr/bin:/bin:/usr/sbin:/sbin;
            fastcgi_intercept_errors on;
            include        fastcgi_params;
        }
    }

I am aware that rewrite ^/(.*)/$ /$1 permanent; is the offending line. If I remove it and visit example.com/webgrind, a 301 is issued for me to redirect to example.com/webgrind/ since it is a directory. However, my application will now accept both trailing and non-trailing slashes (i.e. example.com/users/ and example.com/users) and this is not what I want.

Wrapping the 'if' directive around my rewrite as follows still creates a redirect loop for my directories (if is evil, apparently, but a rewrite directive in this case is considered safe):

if (!-d $request_filename) {
    rewrite ^/(.*)/$ /$1 permanent;
}

(I know that visiting webgrind/index.php would solve my problem, but I'd like to avoid costly and unprofessional redirect loops when my production directories are pushed live.)

So how can I conditionally strip trailing slashes only for resources that don't exist (my web application paths)?

UPDATE: My (unaltered) fastcgi_params config:

fastcgi_param   QUERY_STRING            $query_string;
fastcgi_param   REQUEST_METHOD          $request_method;
fastcgi_param   CONTENT_TYPE            $content_type;
fastcgi_param   CONTENT_LENGTH          $content_length;

fastcgi_param   SCRIPT_FILENAME         $request_filename;
fastcgi_param   SCRIPT_NAME             $fastcgi_script_name;
fastcgi_param   REQUEST_URI             $request_uri;
fastcgi_param   DOCUMENT_URI            $document_uri;
fastcgi_param   DOCUMENT_ROOT           $document_root;
fastcgi_param   SERVER_PROTOCOL         $server_protocol;

fastcgi_param   GATEWAY_INTERFACE       CGI/1.1;
fastcgi_param   SERVER_SOFTWARE         nginx/$nginx_version;

fastcgi_param   REMOTE_ADDR             $remote_addr;
fastcgi_param   REMOTE_PORT             $remote_port;
fastcgi_param   SERVER_ADDR             $server_addr;
fastcgi_param   SERVER_PORT             $server_port;
fastcgi_param   SERVER_NAME             $server_name;

fastcgi_param   HTTPS                   $https;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param   REDIRECT_STATUS         200;
danronmoon
  • 3,814
  • 5
  • 34
  • 56
  • A couple things: 1) I'm not sure I understand completely what you are trying to achieve in terms of behavior for removing the trailing slash - are you trying to achieve a server level redirect, where if the client sends either `foo.com/aaa` or `foo.com/aaa/` that the server will respond with a document; or are you trying to get the server to tell the browser to redirect from `foo.com/aaa/` to `foo.com/aaa`? – chue x Nov 02 '13 at 19:55
  • 2) I can't reproduce the redirect loop if I use the `if` block. You may be seeing the problem if you are using an older browser - see [Internet Explorer 9 Permanently Caches Redirects](http://agsci.psu.edu/it/how-to/topics/web/web-development/plone/other/internet-explorer-9-permanently-caches-redirects) for one example. So what that means is even with the "if" block, IE will try to redirect since it had the old rewrite cached. To solve this you have to clear your browser cache. – chue x Nov 02 '13 at 19:57
  • @chuex I am trying to get the server to tell the client to redirect from foo.com/aaa/ to foo.com/aaa with a 301 response. I tested on latest Chrome and Firefox on 2 different computers and cleared my cache and I am still getting this issue. – danronmoon Nov 02 '13 at 22:00
  • With `if` block I cannot reproduce it either... Is there some records in nginx error.log? And can be that, by any chance, your application redirects the `wrongfolder` to `wrongfolder/`(thus creating the infinite loop itself, fighting with nginx)? – rchukh Nov 03 '13 at 02:10
  • @rchukh There are no records in the nginx error log regarding the redirect loop, and I'm not surprised because 301's are normal operations. I'm not sure what you mean by `wrongfolder`... I am 100% positive these folders exist if that's what you're asking. If the directory didn't exist, my app (index.php) would take care of the routing (this is what `/index.php?$args` is doing). I feel better now that there are 2 cases of the `if` block working so maybe my production installation will fix this issue. Could you post a link to your test config? I can post my http block but not sure if it'd help – danronmoon Nov 03 '13 at 12:51
  • By `wrongfolder` I meant some folder that doesn't exist on your server - e.g. user asks for "ip:8080/wrongfolder/" and nginx(knowing that this folder doesn't exist, because of that `if`) redirects to "ip:8080/wrongfolder" which will be handled by `index.php`. What I wanted to ask, is whether the index.php redirects back to "ip:8080/wrongfolder/"...for some unknown(to me) reasons. It is quite a strange case, but I don't know what's inside index.php... – rchukh Nov 03 '13 at 13:11
  • Anyway - here's a complete [nginx.conf gist](https://gist.github.com/rchukh/7290075), which is working on my side (I replaced the `php-fpm` part with `empty_gif` to avoid any possible issues on your side related to index.php). – rchukh Nov 03 '13 at 13:12
  • @rchukh index.php is Zend Framework 1's [public/index.php](http://framework.zend.com/manual/1.12/en/zend.application.quick-start.html) built with Zend_Tool. I'll mess around a bit with the config and get back to you, thanks. – danronmoon Nov 03 '13 at 13:19

1 Answers1

9

Putting the root directive outside of the location block as a direct child of the server block fixed the issue.

server {
    listen 80;
    server_name example.com;

    # This WORKS!
    root /var/www/example/public; 

    location / {
        try_files $uri $uri/ /index.php?$args;
        index  index.php index.html index.htm;
    }

    if (!-d $request_filename) {
        rewrite ^/(.*)/$ /$1 permanent;
    }

    location = /favicon.ico {
        access_log     off;
        log_not_found  off;
    }

    location ~ \.php$ {
        try_files $uri $uri/ /index.php?$args;
        index index.php index.html index.htm;

        fastcgi_pass   unix:/var/run/php5-fpm.sock;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  /var/www/example/public$fastcgi_script_name;
        fastcgi_param  APPLICATION_ENV testing;
        fastcgi_param  PATH /usr/bin:/bin:/usr/sbin:/sbin;
        fastcgi_intercept_errors on;
        include        fastcgi_params;
    }
}

Apparently it is a pitfall that the Nginx wiki recommends to avoid.

danronmoon
  • 3,814
  • 5
  • 34
  • 56
  • Just to expand on your answer - the pitfall with having the root directive inside a location block is that other location blocks then do not have a root directive. (Unless they have their own duplicated root directives.) If the root directive is at the server level, then it applies to all location blocks. – chue x Nov 03 '13 at 15:38
  • I believe the if clause checking for a directory was the one to fix the issue… – Daniel Feb 03 '15 at 12:35