Nginx alias breaks due to try_files $uri alias bug
Asked Answered
T

3

1

I have a versioned Symfony API instance that I want to configure in the following manner:

  • api.com/api/v1 -> /srv/api-v1/public/index.php
  • api.com/api/v2 -> /srv/api-v2/public/index.php

I've tried to approach this using nginx location and aliases, as it's Symfony we use try_files (as recommended) to check for an actual file prior to defaulting to index.php.

Problem

It seems there is a known nginx bug that breaks the $uri variable with an alias and try_files.

How can I get around this bug to achieve my desired outcome?

nginx conf

server {
    listen 443 http2;
    listen [::]:443 http2;
    server_name api.com;
    root /srv/default/public/; # default root when no version
 
    location /api/v1 {
        alias /srv/api-v1/public/;
        try_files $uri /index.php$is_args$args;
    }

    location /api/v2 {
        alias /srv/api-v2/public/;
        try_files $uri /index.php$is_args$args;
    }

    location ~ ^/index\.php(/|$) {
        include /etc/nginx/fastcgi.conf;
        fastcgi_pass unix:/run/php-fpm-php7.2.socket;
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;
        internal;
        fastcgi_read_timeout 300;
    }
}

Attempted fix

Going by this hacky fix I have created the following which does work but generates a huge config file, not ideal:

upstream v1 {
    server 127.0.0.1;
}
upstream v2 {
    server 127.0.0.1;
}
server {
    listen 443 http2;
    listen [::]:443 http2;
    server_name api.com;
 
    location /api/v1 {
        proxy_pass http://v1;
    }

    location /api/v2 {
        proxy_pass http://v2;
    }
}
server {
    server_name v1;
    root /srv/api-v1/public/;

    location / {
        try_files $uri /index.php$is_args$args;
    }

    location ~ ^/index\.php(/|$) {
        include /etc/nginx/fastcgi.conf;
        fastcgi_pass unix:/run/php-fpm-php7.2.socket;
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;
        internal;
    }
}
server {
    server_name v2;
    root /srv/api-v2/public/;

    location / {
        try_files $uri /index.php$is_args$args;
    }

    location ~ ^/index\.php(/|$) {
        include /etc/nginx/fastcgi.conf;
        fastcgi_pass unix:/run/php-fpm-php7.2.socket;
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;
        internal;
    }
}

Tamartamara answered 1/12, 2020 at 15:30 Comment(2)
You can use if and $request_filename with alias to avoid using try_files. You also need to use nested locations for the PHP URIs. See this answer.Wayfarer
Beware of using if in location context. See nginx.com/resources/wiki/start/topics/depth/ifisevilSupernatural
Y
1

There is another workaround exists which can be used when an alias directive is used in conjunction with the try_files one (see this answer for an example). Can you try the following config?

server {
    listen 443 http2;
    listen [::]:443 http2;
    server_name api.com;
    root /srv/default/public/; # default root when no version
 
    location ~ ^/api/v1(?<v1route>/.*)? {
        alias /srv/api-v1/public;
        try_files $v1route /api/v1/index.php$is_args$args;
        location ~ ^/api/v1/index\.php$ {
            internal;
            include /etc/nginx/fastcgi.conf;
            fastcgi_param SCRIPT_FILENAME /srv/api-v1/public/index.php;
            fastcgi_read_timeout 300;
            fastcgi_pass unix:/run/php-fpm-php7.2.socket;
        }
    }

    location ~ ^/api/v2(?<v2route>/.*)? {
        alias /srv/api-v2/public;
        try_files $v2route /api/v2/index.php$is_args$args;
        location ~ ^/api/v2/index\.php$ {
            internal;
            include /etc/nginx/fastcgi.conf;
            fastcgi_param SCRIPT_FILENAME /srv/api-v2/public/index.php;
            fastcgi_read_timeout 300;
            fastcgi_pass unix:/run/php-fpm-php7.2.socket;
        }
    }
}

Temporary update (to be expanded with explanation)

OP asks an additional question:

There's one slight issue with the defaults Symfony expects. The following three server variables $_SERVER['DOCUMENT_URI'], $_SERVER['SCRIPT_NAME'] and $_SERVER['PHP_SELF'] equal /api/v1/index.php but default Symfony nginx generates /index.php, is there a way to tweak that in the above?

I don't think this is the reason of incorrect Symfony behavior. While this variables of course can be tweaked, the most probable reason is incorrect $_SERVER['REQUEST_URI'] value. With the configuration above it will be equal to /api/v1/some/path but most likely Symfony expects just /some/path instead. Here is the configuration you can try to overwrite that variable:

map $request_uri $api_ver {
    ~^/api/v([12])/?  $1;
}
map $request_uri $api_route {
    ~^/api/v[12](/[^?]*)?(?:$|\?)  $1;
}
server {
    listen 443 http2;
    listen [::]:443 http2;
    server_name api.com;
    root /srv/default/public; # default root when no version

    location ~ ^/api/v[12]/? {
        alias /srv/api-v$api_ver/public;
        try_files $api_route /api/v$api_ver/index.php$is_args$args;
        location ~ ^/api/v[12]/index\.php$ {
            internal;
            include /etc/nginx/fastcgi.conf;
            fastcgi_param REQUEST_URI $api_route$is_args$args;
            fastcgi_param SCRIPT_FILENAME /srv/api-v$api_ver/public/index.php;
            fastcgi_read_timeout 300;
            fastcgi_pass unix:/run/php-fpm-php7.2.socket;
        }
    }
}

I think this should fix your issues, but if you really want to tweak $_SERVER['DOCUMENT_URI'], $_SERVER['SCRIPT_NAME'] and $_SERVER['PHP_SELF'], you can add two additional lines to the nested location:

        location ~ ^/api/v[12]/index\.php$ {
            internal;
            include /etc/nginx/fastcgi.conf;
            fastcgi_param REQUEST_URI $api_route$is_args$args;
            fastcgi_param DOCUMENT_URI /index.php;
            fastcgi_param SCRIPT_NAME /index.php;
            fastcgi_param SCRIPT_FILENAME /srv/api-v$api_ver/public/index.php;
            fastcgi_read_timeout 300;
            fastcgi_pass unix:/run/php-fpm-php7.2.socket;
        }

but I don't think it is required for correct Symfony behavior.

Update 2

To prevent $_SERVER['REQUEST_URI'] variable from being an empty string, you have the following options:

  1. Redirect the request from /api/v1 or /api/v2 to /api/v1/ or /api/v2/ (seems the best as for me):

    map $request_uri $api_ver {
        ~^/api/v([12])/  $1;
    }
    map $request_uri $api_route {
        ~^/api/v[12](/[^?]*)(?:$|\?)  $1;
    }
    server {
        ...
        location ~ ^/api/v[12]$ {
            return 301 https://$host$uri/$is_args$args;
        }
        location ~ ^/api/v[12]/ {
            alias /srv/api-v$api_ver/public;
            try_files $api_route /api/v$api_ver/index.php$is_args$args;
            location ~ ^/api/v[12]/index\.php$ {
                internal;
                include /etc/nginx/fastcgi.conf;
                fastcgi_param REQUEST_URI $api_route$is_args$args;
                fastcgi_param SCRIPT_FILENAME /srv/api-v$api_ver/public/index.php;
                fastcgi_read_timeout 300;
                fastcgi_pass unix:/run/php-fpm-php7.2.socket;
            }
        }
    }
    
  2. Explicitly add the trailing slash to the exact /api/v1 or /api/v2 requests:

    map $request_uri $api_ver {
        ~^/api/v([12])/?  $1;
    }
    map $request_uri $api_route {
        ~^/api/v[12](?:/([^?]*))?(?:$|\?)  /$1;
    }
    server {
        ...
        location ~ ^/api/v[12]/? {
            alias /srv/api-v$api_ver/public;
            try_files $api_route /api/v$api_ver/index.php$is_args$args;
            location ~ ^/api/v[12]/index\.php$ {
                internal;
                include /etc/nginx/fastcgi.conf;
                fastcgi_param REQUEST_URI $api_route$is_args$args;
                fastcgi_param SCRIPT_FILENAME /srv/api-v$api_ver/public/index.php;
                fastcgi_read_timeout 300;
                fastcgi_pass unix:/run/php-fpm-php7.2.socket;
            }
        }
    }
    

If the $_SERVER['REQUEST_URI'] variable value should be prepended with /api string, you can try fastcgi_param REQUEST_URI /api$api_route$is_args$args; instead of fastcgi_param REQUEST_URI $api_route$is_args$args;.

If you want to tweak $_SERVER['DOCUMENT_URI'], $_SERVER['SCRIPT_NAME'] and $_SERVER['PHP_SELF'] variables, add the

fastcgi_param DOCUMENT_URI /index.php;
fastcgi_param SCRIPT_NAME /index.php;

lines to the nested location.

Yeager answered 3/12, 2020 at 11:6 Comment(13)
Tested and so far this seems to work as expected, a much simpler solution. Thank you!Tamartamara
Having tested this a bit more there's one slight issue with the defaults Symfony expects. The following three server variables $_SERVER['DOCUMENT_URI'], $_SERVER['SCRIPT_NAME'] and $_SERVER['PHP_SELF'] equal /api/v1/index.php but default Symfony nginx generates /index.php, is there a way to tweak that in the above?Tamartamara
Yes, most of them $_SERVER variables are defined to their default values via the /etc/nginx/fastcgi.conf (Debian based) or /etc/nginx/fastcgi_params (RHEL based) file. You can redefine them (only after file fastcgi.conf inclusion, see this answer for more explanation). It seems strange to me that a PHP application behavior can be dependent on any other variables than REQUEST_URI and DOCUMENT_URI.Yeager
I'll write an update to the answer soon, didn't have time to that right now.Yeager
Did you have any chance to look at this? Is it just a small tweak?Tamartamara
Started to write a really big answer yesterday and loose it with a system crash :( What are you asking for is a small tweak, you need to add fastcgi_param DOCUMENT_URI /index.php; and fastcgi_param SCRIPT_NAME /index.php; lines to the both nested locations after the include /etc/nginx/fastcgi.conf; line, but I'm afraid your problems are coming from an incorrect value of REQUEST_URI variable (you need to strip /api/vN substring from it) and that tweak won't help you. Tweaking REQUEST_URI is more tricky. Meanwhile you can try this one, I'll try to give the whole answer today.Yeager
have added a large bounty to this question if there's any chance you could give a rewrite example to finish this up, thank youTamartamara
Check an update, please. I think it should fix your issues. Your bounty deserves an explanation of that configuration and on tweaking FastCGI request meta-variables in general and I'll try to write one really soon, but it will take a couple of time I didn't have right now, I'm really sorry, my current project deadline is tomorrow.Yeager
Thanks I have tried this. There seems one last issue in that REQUEST_URI' => '', is empty when we expect it to be 'REQUEST_URI' => '/api/v2',, that's what is breaking the Symfony routing.Tamartamara
REQUEST_URI It doesn't seem to be applying the /api/v1 prefix with the above.Tamartamara
The goal of the configuration I suggest is to strip /api/v1 or /api/v2 prefix from the REQUEST_URI FastCGI variable. I don't know your Symfony app architecture or what REQUEST_URI value it actually expects for the /api/v<number>/some/path request, it may be /api/v<number>/some/path or /api/some/path or even just a /some/path. The first value can be avhieved with the first answer I suggested, but I think you need either second or third one.Yeager
The one required is /api/v<number>/some/path but none of the above produce that. Even using fastcgi_param REQUEST_URI $api_route$is_args$args; this value is still ''Tamartamara
I think your suggestion is a mistake, that is exactly what the first answer does. To tweak the $_SERVER['DOCUMENT_URI'], $_SERVER['SCRIPT_NAME'] and $_SERVER['PHP_SELF'] variables use the recipe given at the end of the answer. Meanwhile try the second answer update.Yeager
M
0

In the "hacky fix" you are missing the trailing / to your proxy_pass directives. Here is a repo to easily test your implementation: https://github.com/MelwinKfr/nginx-workaround97

See this thread if you want more info about the trailing slash.

Matted answered 3/12, 2020 at 16:7 Comment(2)
The attempted fix was broken because I had double uri $uri $uri/, changing to $uri fix that but it's still a huge amount of config.Tamartamara
You're right, with the try_files directives it works without the trailing slash. Even with double $uri $uri/ as long as you finish the line with index.php. Hence I don't get what your problem is. > but it's still a huge amount of config. Agreed.Matted
E
0

If you are fine with the 'hacky fix' you can make it a little cleaner. Just put the repeated config into some other file. Then you can use include directive to import the other file.

/etc/nginx/some_other_file:

    location / {
        try_files $uri /index.php$is_args$args;
    }

    location ~ ^/index\.php(/|$) {
        include /etc/nginx/fastcgi.conf;
        fastcgi_pass unix:/run/php-fpm-php7.2.socket;
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;
        internal;
    }
upstream v1 {
    server 127.0.0.1;
}
upstream v2 {
    server 127.0.0.1;
}
server {
    listen 443 http2;
    listen [::]:443 http2;
    server_name api.com;
 
    location /api/v1 {
        proxy_pass http://v1;
    }

    location /api/v2 {
        proxy_pass http://v2;
    }
}
server {
    server_name v1;
    root /srv/api-v1/public/;
    include some_other_file;
}
server {
    server_name v2;
    root /srv/api-v2/public/;
    include some_other_file;
}
Eversion answered 15/1, 2021 at 8:38 Comment(1)
Unfortunately the hack fix didn't originally work but thanks for the tidy suggestionTamartamara

© 2022 - 2024 — McMap. All rights reserved.