问题:
我有一个 MERN 堆栈项目,其后端 API 在 VPS 服务器上运行。我的设置在本地计算机上完美运行,但当我将其部署到 VPS 时,API 请求缺少 /api 前缀,导致我的网站上出现 404 Not Found 错误。
环境:
前端:Vite(React)
后端: Node.js 与 PM2 一起运行
Web 服务器: VPS 上的 Apache2 (Ubuntu 20.04)
代理: Apache2 正在处理后端 API 的反向代理。
问题: 当我从前端发送请求时,例如 /api/v1/auth/login,后端收到请求,但没有像 //v1/auth/login 这样的 api 前缀,导致路由不正确,我收到 404 Not Found错误。然而,本地一切正常。
我的 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';
}
},
},
},
},
};
}
});
我的节点 APi 的 server.js 通过 pm2 运行
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);
}
前端 React 应用程序中的 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
我尝试过的: 使用 LogLevel debug 检查 Apache 日志。这是一些相关的日志条目
[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 设置。
附加信息:
问题: 为什么 Apache2 在代理 Node.js API 请求时剥离 /api 前缀?如何确保保留 /api 前缀?
任何帮助或建议将不胜感激!
我在一位 LinkedIn 朋友的帮助下找到了解决方案。 所以问题是在这种类型的虚拟主机配置中
<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]
你需要保持这两行相同
ProxyPass "/api/v1/" "http://localhost:5000/api/v1/"
ProxyPassReverse "/api/v1/" "http://localhost:5000/api/v1/"
在主虚拟主机文件(如 someVirtualhostName.conf)及其 ssl 文件(如 someVirtualhostName-le-ssl.conf)中
我在我的 -le-ssl.conf 文件中的端口 http://localhost:5000 的末尾添加了 /api/v1/ ,它开始正常工作
还要记住,api 前缀(在我的例子中为“/api/v1”)和后端服务器 URL(在我的例子中为 http://localhost:5000)末尾的 / 非常重要
这是我的虚拟主机文件(主文件和 -le-ssl.conf 文件)中 ProxyPass 和 ProxyPassReserve 的最终外观
ProxyPass "/api/v1/" "http://localhost:5000/api/v1/"
ProxyPassReverse "/api/v1/" "http://localhost:5000/api/v1/"