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配置
补充信息
- 后端收到的请求路径丢失
/api前缀(如//v1/auth/login而非/api/v1/auth/login) - 本地环境配置逻辑与生产一致,但运行正常
问题原因与解决方案
原因分析
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' }); });
验证步骤
- 修改配置后重启Apache服务:
sudo systemctl restart apache2 - 重启PM2管理的后端服务:
pm2 restart <你的服务名> - 发起API请求,查看PM2日志是否收到正确路径(如
/api/v1/auth/login)
内容的提问来源于stack exchange,提问作者Muhammad Zeeshan
相关产品推荐
相关产品推荐

