Nginx 别名由于 try_files $uri 别名错误而中断

Nginx alias breaks due to try_files $uri alias bug

我有一个版本化的 Symfony API 实例,我想按以下方式配置它:

我尝试使用 nginx 位置和别名来解决这个问题,因为它是 Symfony,我们使用 try_files (as recommended) 在默认为 index.php 之前检查实际文件.

问题

似乎有一个 known nginx bugaliastry_files.

打破了 $uri 变量

我怎样才能绕过这个错误来达到我想要的结果?

nginx 配置文件

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

已尝试修复

根据 this hacky fix 我创建了以下内容,它确实有效但生成了一个巨大的配置文件,并不理想:

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

存在另一种解决方法,当 alias 指令与 try_files 指令结合使用时可以使用(参见 答案的示例)。您可以尝试以下配置吗?

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

临时更新(待补充说明)

OP 问了一个额外的问题:

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?

我不认为这是不正确的 Symfony 行为的原因。虽然当然可以调整此变量,但最可能的原因是 $_SERVER['REQUEST_URI'] 值不正确。使用上面的配置,它将等于 /api/v1/some/path 但很可能 Symfony 期望只是 /some/path 而不是。这是您可以尝试覆盖该变量的配置:

map $request_uri $api_ver {
    ~^/api/v([12])/?  ;
}
map $request_uri $api_route {
    ~^/api/v[12](/[^?]*)?(?:$|\?)  ;
}
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;
        }
    }
}

我认为这应该可以解决您的问题,但如果您真的想要调整 $_SERVER['DOCUMENT_URI']$_SERVER['SCRIPT_NAME']$_SERVER['PHP_SELF'],您可以向嵌套位置添加另外两行:

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

但我认为正确的 Symfony 行为不需要它。

更新 2

要防止 $_SERVER['REQUEST_URI'] 变量为空字符串,您有以下选项:

  1. 将请求从 /api/v1/api/v2 重定向到 /api/v1//api/v2/(对我来说似乎是最好的):

    map $request_uri $api_ver {
        ~^/api/v([12])/  ;
    }
    map $request_uri $api_route {
        ~^/api/v[12](/[^?]*)(?:$|\?)  ;
    }
    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. 将尾部斜杠明确添加到确切的 /api/v1/api/v2 请求中:

    map $request_uri $api_ver {
        ~^/api/v([12])/?  ;
    }
    map $request_uri $api_route {
        ~^/api/v[12](?:/([^?]*))?(?:$|\?)  /;
    }
    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;
            }
        }
    }
    

如果 $_SERVER['REQUEST_URI'] 变量值应该以 /api 字符串作为前缀,您可以尝试 fastcgi_param REQUEST_URI /api$api_route$is_args$args; 而不是 fastcgi_param REQUEST_URI $api_route$is_args$args;

如果您想调整 $_SERVER['DOCUMENT_URI']$_SERVER['SCRIPT_NAME']$_SERVER['PHP_SELF'] 变量,请添加

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

行到嵌套位置。

在“hacky fix”中,您缺少 proxy_pass 指令的结尾 /。 这是一个可以轻松测试您的实施的回购协议:https://github.com/MelwinKfr/nginx-workaround97

如果您想了解有关尾部斜线的更多信息,请参阅 this thread

如果您觉得 'hacky fix' 没问题,您可以让它更干净一些。只需将重复的配置放入其他文件即可。然后你可以使用 include 指令导入另一个文件。

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