You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

VPS上Apache2代理Node.js API丢失/api前缀的问题排查

Apache反向代理丢失/api前缀导致MERN项目404问题

环境

  • 前端:Vite(React)
  • 后端:Node.js(PM2管理)
  • Web服务器:Ubuntu 20.04 上的 Apache2
  • 代理方式:Apache2 反向代理后端API

问题现象

前端发起 /api/v1/auth/login 这类请求时,后端收到的请求路径丢失了 /api 前缀,变成 //v1/auth/login,导致路由匹配失败,返回404错误。本地开发环境运行正常,仅部署到VPS后出现该问题。

相关配置

Apache虚拟主机配置

<VirtualHost *:80>
ServerAdmin webmaster@localhost
ServerName agileboard.muhammadzeeshan.dev
DocumentRoot /var/www/agileboard.muhammadzeeshan.dev/react-app/dist
ProxyRequests Off
ProxyPreserveHost On
ProxyPass "/api/v1" "http://localhost:5000/api/v1"
ProxyPassReverse "/api/v1" "http://localhost:5000/api/v1"

<Directory /var/www/agileboard.muhammadzeeshan.dev/react-app/dist>
    AllowOverride All
    Require all granted
    FallbackResource /index.html
</Directory>

ErrorLog ${APACHE_LOG_DIR}/agileboard_error.log
CustomLog ${APACHE_LOG_DIR}/agileboard_access.log combined

 RewriteEngine on
 RewriteCond %{SERVER_NAME} =www.agileboard.muhammadzeeshan.dev [OR]
 RewriteCond %{SERVER_NAME} =agileboard.muhammadzeeshan.dev
 RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>

Vite本地代理配置

export default defineConfig(({ command }) => {
   if (command === 'serve' || command === 'preview' || command === 'dev' ) {
    // development config
     return {
      plugins: [react()],
      server: {
       proxy: {
        '/api': {
          target: 'http://localhost:5000/api',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api/, ''),
        },
      },
    },
     build: {
        rollupOptions: {
          output: {
            manualChunks(id) {
              if (id.includes('node_modules')) {
                // Split each dependency into its own chunk
                return id.split('node_modules/')[1].split('/')[0];
              }
              if (id.includes('froala-editor')) {
                return 'froala-editor';
              }
            },
          },
        },
      },
  };


 } else {

   // Production config
 return {
   plugins: [react()],
   build: {
      rollupOptions: {
        output: {
          manualChunks(id) {
            if (id.includes('node_modules')) {
              // Split each dependency into its own chunk
              return id.split('node_modules/')[1].split('/')[0];
            }
            if (id.includes('froala-editor')) {
              return 'froala-editor';
            }
           },
         },
       },
     },
   };
  }
 });

Node.js API 服务配置(server.js)

import 'express-async-errors';
import express from "express";
import cors from 'cors';
import  config  from './config/default.js';
import morgan from 'morgan';
import errorHandlerMiddleware from './middleware/errorHandlerMiddleware.js';
import cookieParser from 'cookie-parser';
import { initModels } from './models/index.js';
import path from 'path';
import { fileURLToPath, pathToFileURL } from 'url';
import os from 'os';
// Get the current directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);


const app = express();

// Middleware
  app.use(cors());
  app.use(cookieParser());
  app.use(express.json());


  if(config.node_env ==="development"){
   app.use(morgan("dev"))
  }
  // Define your routes here
  import Route  from './routes/routes.js';
  app.use('/api/v1/', Route);


  // middleware to check for errors by controllers, this itself will be valid request
  app.use(errorHandlerMiddleware);

   app.use((req, res, next) => {
    res.set('Cache-Control', 'no-store');
  next();
 });

 app.get('/api/v1/api_testing', (req, res) => {
 res.json({ message: 'API working' });
 });

  app.use((req, res, next) => {
 console.log(`${req.method} ${req.url}`);
  next();
 });


  //  middleware to check for invalid request errors
  app.use('*',(req, res)=>{
   return res.status(404).json({message:"404 Resource not found"})
  });

 try{

  const server = app.listen(config.port, async () => {
  await initModels();

  const { address, port } = server.address();
  const host = address === '::' ? 'localhost' : address
  console.log('Database synchronized successfully');

  console.log(`Server running at http:// ${host} : ${port} `);
  });

}catch(err){
 console.error('Error: '+ err);
 process.exit(1);
}

前端Axios请求封装

const CustomRequest = axios.create({
 baseURL: "/api/v1"
});

PM2日志

Server running at http:// localhost : 5000
  POST //v1/auth/login
  POST //v1/auth/login
  POST //v1/auth/login
  POST //v1/auth/login
  POST //v1/auth/login
  POST //v1/auth/login
  POST //v1/auth/login
  GET //v1/api_testing
  GET //v1/api_testing
  GET //v1/api_testing
  GET //v1/api_testing
  GET //v1/api_testing
  POST //v1/auth/login
  POST //v1/auth/login
  POST //v1/auth/login
  POST //v1/auth/login

已尝试的解决步骤

  • 开启Apache debug日志,相关日志条目:
[proxy:debug] [pid 12345] proxy_util.c(1936): AH00925: initializing worker 
 http://localhost:5000/api/v1 shared
[proxy:debug] [pid 12345] proxy_util.c(1993): AH00927: initializing worker 
 http://localhost:5000/api/v1 local
  • 确认ProxyPreserveHost已启用
  • 修改过Apache的ProxyPass和ProxyPassReverse配置

补充信息

  1. 后端收到的请求路径丢失/api前缀(如//v1/auth/login而非/api/v1/auth/login)
  2. 本地环境配置逻辑与生产一致,但运行正常

问题原因与解决方案

原因分析

Apache的ProxyPass配置对路径末尾的斜杠敏感,当前配置ProxyPass "/api/v1" "http://localhost:5000/api/v1"未添加末尾斜杠,导致代理时路径拼接错误,/api前缀被意外剥离,最终后端收到的路径出现多余斜杠且丢失前缀。

另外,本地Vite代理配置中的rewrite规则仅在开发环境生效,生产环境前端直接请求/api/v1开头的路径,需要Apache完整传递该路径到后端。

解决方案

方案1:修正Apache ProxyPass配置(推荐)

将ProxyPass和ProxyPassReverse的路径添加末尾斜杠,确保路径匹配和拼接正确:

ProxyPass "/api/v1/" "http://localhost:5000/api/v1/"
ProxyPassReverse "/api/v1/" "http://localhost:5000/api/v1/"

或者,改为匹配整个/api前缀,更灵活适配所有API路径:

ProxyPass "/api/" "http://localhost:5000/api/"
ProxyPassReverse "/api/" "http://localhost:5000/api/"

方案2:调整后端路由前缀(备选)

如果不想修改Apache配置,可以将后端的路由前缀从/api/v1/改为/v1/,让后端直接匹配去掉/api后的路径:

// 替换原路由配置
app.use('/v1/', Route);
app.get('/v1/api_testing', (req, res) => {
 res.json({ message: 'API working' });
});

验证步骤

  1. 修改配置后重启Apache服务:sudo systemctl restart apache2
  2. 重启PM2管理的后端服务:pm2 restart <你的服务名>
  3. 发起API请求,查看PM2日志是否收到正确路径(如/api/v1/auth/login)

内容的提问来源于stack exchange,提问作者Muhammad Zeeshan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 15:10:55