Answer a question

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;
    }
}

Answers

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.

Logo

开发云社区提供前沿行业资讯和优质的学习知识,同时提供优质稳定、价格优惠的云主机、数据库、网络、云储存等云服务产品

更多推荐