记一次 Nginx 代理导致 API 500 错误的排查与修复

问题描述

前端访问 http://localhost:9527/dev-api/Login/getStaticResource 返回 500 Internal Server Error,直接访问 http://anyu-portal.test/ 正常。

技术栈

  • 前端:Vue.js + Vue CLI (devServer proxy)
  • 后端:ThinkPHP 5.x + PHP 7.4
  • 容器:Docker Compose (nginx, php-fpm)
  • 代理:Nginx 反向代理

请求链路分析

浏览器 localhost:9527/dev-api/Login/getStaticResource
    ↓ Vue devServer proxy (vue.config.js)
Nginx anyu-portal-frontend.test/api/Login/getStaticResource
    ↓ Nginx location /api/ proxy_pass (anyu-front.conf)
Nginx anyu-portal.test/Login/getStaticResource
    ↓ Nginx location ~ .php$ fastcgi_pass (anyu-portal.conf)
PHP-FPM anyu-portal/Public/index.php

配置文件检查

1. Vue devServer 代理配置

vue.config.js

proxy: {
  [process.env.VUE_APP_BASE_API]: {
    target: 'http://anyu-portal-frontend.test/api',
    changeOrigin: true,
    pathRewrite: {
      ['^' + process.env.VUE_APP_BASE_API]: ''
    }
  }
}

VUE_APP_BASE_API = /dev-api,所以:

  • 请求 /dev-api/Login/getStaticResource
  • 被转发到 http://anyu-portal-frontend.test/api/Login/getStaticResource

2. Nginx 前端站点配置

anyu-front.conf

server {
    listen 80;
    server_name anyu-frontend.test *.anyu-frontend.test;
    root "/www/anyu-portal-frontend";

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_set_header Host $http_x_forwarded_host;
        proxy_pass http://anyu-portal.test/;
    }
}

3. Nginx 后端站点配置

anyu-portal.conf

server {
    listen 80;
    server_name anyu-portal.test;
    root /www/anyu-portal/Public;

    if (!-e $request_filename) {
        rewrite ^(.*)$ /index.php?s=$1 last;
    }

    location ~ \.php$ {
        fastcgi_pass php:9000;
        include fastcgi-php.conf;
        include fastcgi_params;
    }
}

4. Docker Compose 网络配置

docker-compose.yml

services:
  nginx:
    extra_hosts:
      - "openapi:127.0.0.1"
      - "lvs:127.0.0.1"
      - "admin:127.0.0.1"
      - "portal:127.0.0.1"
    # 缺少: anyu-portal.test:127.0.0.1

排查过程

步骤 1:直接测试后端 API

从 nginx 容器内部直接访问后端:

docker exec nginx curl -v http://anyu-portal.test/Login/getStaticResource

结果:返回 200 OK,后端 API 本身正常。

步骤 2:测试代理链路

从 nginx 容器内部通过前端域名访问:

docker exec nginx curl -v http://anyu-portal-frontend.test/api/Login/getStaticResource

结果:返回 500 Internal Server Error。

步骤 3:检查 Host 头问题

直接访问时 curl 会发送 Host: anyu-portal.test,但通过 /api/ 代理时:

proxy_set_header Host $http_x_forwarded_host;

$http_x_forwarded_host 是客户端请求的 X-Forwarded-Host 头,通常为空。当 Host 头为空时,后端 ThinkPHP 的 getStaticResource 方法无法正确识别 SERVER_NAME,导致找不到合作伙伴配置。

步骤 4:检查域名解析

nginx 容器的 extra_hosts 中没有配置 anyu-portal.test,虽然之前已经添加了,但需要确认是否生效。

根因分析

问题有两个层面:

  1. Host 头为空proxy_set_header Host $http_x_forwarded_host; 设置了一个通常为空的变量,导致转发到后端时没有正确的 Host 头。

  2. 域名解析:nginx 容器内部无法解析 anyu-portal.test(虽然之前已添加到 extra_hosts)。

后端 ThinkPHP 的 getStaticResource 方法依赖 SERVER_NAME 获取合作伙伴配置:

public function getStaticResource() {
    $server_name = SERVER_NAME;  // 依赖 $_SERVER['SERVER_NAME']
    $redis = new Redis();
    $redis->connect(C("REDIS_HOST"), C("REDIS_PORT"));
    // ...
    if(!C('PARTNER')) {
        $this->setPartner($redis, $server_name);  // 根据 server_name 获取合作伙伴
    }
    // ...
}

当 Host 头为空或不正确时,SERVER_NAME 无法正确识别,导致 C('PARTNER') 为空,后续操作失败。

修复方案

修复 1:修改 Nginx 代理配置

文件services/nginx/conf.d/anyu-front.conf

# 修改前
location /api/ {
    proxy_set_header Host $http_x_forwarded_host;
    proxy_pass http://anyu-portal.test/;
}

# 修改后
location /api/ {
    proxy_set_header Host anyu-portal.test;
    proxy_pass http://anyu-portal.test/;
}

修复 2:添加域名解析

文件docker-compose.yml

services:
  nginx:
    extra_hosts:
      - "openapi:127.0.0.1"
      - "lvs:127.0.0.1"
      - "admin:127.0.0.1"
      - "portal:127.0.0.1"
      - "anyu-portal.test:127.0.0.1"  # 新增

修复 3:重载 Nginx 配置

docker exec nginx nginx -s reload

验证

docker exec nginx curl -v http://anyu-portal-frontend.test/api/Login/getStaticResource

结果:返回 200 OK,JSON 数据正常。

总结

经验教训

  1. Host 头很重要:反向代理时,proxy_set_header Host 必须设置正确的值,后端框架(如 ThinkPHP)依赖它来识别当前站点。

  2. 容器内部域名解析:Docker Compose 的 extra_hosts 只对容器内部生效,宿主机的 hosts 文件不会自动同步到容器。

  3. 排查顺序:先测试直接访问后端,再测试完整代理链路,定位问题出在哪一层。

  4. 变量陷阱$http_x_forwarded_host 是客户端请求头,不是 Nginx 内置变量,不要误以为它会自动填充。

最佳实践

location /api/ {
    proxy_set_header Host $proxy_host;  # 使用 proxy_pass 中指定的主机名
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://backend/;
}

使用 $proxy_host 可以自动获取 proxy_pass 中指定的主机名,比硬编码更灵活。