nginx + fcgiwrap: how come order of fastcgi_param matters?

I'm running Debian 6.0.3 (squeeze), nginx-0.7.67, fcgiwrap-1.0-1+squeeze1. Here is the test script:

#!/usr/bin/perl
use 5.010;
use warnings;
use strict;

use Data::Dumper;

print "Content-Type: text/html\n\n";
say Dumper {map {$_ => $ENV{$_}} 'SCRIPT_NAME', 'DOCUMENT_ROOT', 'WHATEVER'};
say "$<, $>, $(, $)";

And here's the nginx configuration:

server {
    server_name   domain.com;
    root   /home/yuri/6;
    access_log   /var/log/nginx/domain.com-access.log;
    error_log   /var/log/nginx/domain.com-error.log;

    location /cgi-bin/ {
        fastcgi_pass   unix:/var/run/fcgiwrap.socket;
        fastcgi_param   DOCUMENT_ROOT   $document_root;
        fastcgi_param   DOCUMENT_ROOT   /home/yuri/7/;
        fastcgi_param   SCRIPT_NAME   $fastcgi_script_name;
        fastcgi_param   WHATEVER   1;
        fastcgi_param   WHATEVER   2;
    }

    location /1.php {
        fastcgi_pass   unix:/var/run/php5-fpm.sock;
        fastcgi_param   PHP_ADMIN_VALUE   cgi.fix_pathinfo=1;
        fastcgi_param   REQUEST_METHOD    $request_method;
        fastcgi_param   SCRIPT_FILENAME   whatever;
        fastcgi_param   SCRIPT_FILENAME   $document_root$fastcgi_script_name;
    }
}

Here's what I get in browser if going to url http://domain.com/cgi-bin/1.pl:

$VAR1 = { 'SCRIPT_NAME' => '/cgi-bin/1.pl', 'DOCUMENT_ROOT' => '/home/yuri/7/', 'WHATEVER' => '2' };

So it seems, that fcgiwrap uses the first DOCUMENT_ROOT for looking up for the script, but the script gets the last values of params. If you change the order of DOCUMENT_ROOT directives, web server returns 403. The question is... how come?

php though works as expected: the second SCRIPT_FILENAME overrides the first one.


Solution 1:

The fastcgi library just passes whatever parameters it was given into the environ pointer. getenv() as used by fcgiwrap uses whatever environment variable came first (optimization?). It is likely that PHP FPM treats this environment just like the array data type, any latter key overwrites the first.

You should not rely on the order and make sure that there is only one key. I have not looked at the FastCGI specification, whether the correct behavior is documented or not.

As for why you get the 403, I guess that there is no /home/yuri/7/1.pl file. (remember, the first parameter gets matched by fcgiwrap). Since it is not executable, fcgiwrap returns a 403.

Testing

The below patch prints the environment as given by FCGI_Accept():

diff --git a/fcgiwrap.c b/fcgiwrap.c
index e86ff9d..0dff2e6 100644
--- a/fcgiwrap.c
+++ b/fcgiwrap.c
@@ -470,6 +470,11 @@ static void inherit_environment(void)
    char * const * p;
    char *q;

+   for (p = environ; *p; p++)
+       fprintf(stderr, "ENV: %s\n", *p);
+
+   fprintf(stderr, "ENV[FOO] = %s\n", getenv("FOO"));
+
    for (p = inherited_environ; *p; p++) {
        q = strchr(*p, '=');
        if (!q) {

nginx configuration used for the test:

events {
    worker_connections 768;
}

pid pid;
error_log error_log;
http {
    server {
        access_log off;
        listen 5555;
        location / {
            fastcgi_param FOO BAR;
            fastcgi_param FOO BARRED;
            fastcgi_pass unix:/tmp/sock;
        }
    }
}

Commands to start the servers (assuming nginx.conf in the current directory):

$ nginx -p . -c nginx.conf
$ env -i ./fcgiwrap -p /dev/null -s unix:/tmp/sock

Now, run curl http://localhost:5555/ and the stderr will print:

ENV: FCGI_ROLE=RESPONDER
ENV: FOO=BAR
ENV: FOO=BARRED
ENV: HTTP_USER_AGENT=curl/7.30.0
ENV: HTTP_HOST=localhost:5555
ENV: HTTP_ACCEPT=*/*
ENV[FOO] = BAR
Cannot get script name, are DOCUMENT_ROOT and SCRIPT_NAME (or SCRIPT_FILENAME) set and is the script executable?

Aside, this is a development version. The current stable (1.1.0) does not contain the -p parameter above. For the results it does not really matter, you could as well have dropped it and received an error that no SCRIPT_NAME is given or something.