MERN应用部署Heroku后前端正常但API返回404问题排查
核心根因定位
结合你描述的「本地全链路功能正常、Heroku平台静态资源可正常加载、仅Axios发起的API请求返回404、Network面板可见请求路径/参数/请求头均无异常」的特征,触发问题的原因集中在以下几类:
- 路由注册优先级错误:生产环境下将前端SPA的通配符兜底路由(用于适配history模式的前端路由跳转)放在了API路由注册逻辑之前,所有进入Express服务的请求会优先被静态资源逻辑匹配拦截,API路由根本没有机会处理对应请求
- 环境分支逻辑错配:在Express入口文件中,将API路由的注册逻辑写在了
development专属的判断分支内——本地开发时NODE_ENV为development,API路由可正常加载运行;但Heroku部署时默认将NODE_ENV设置为production,这部分API注册逻辑完全不会被执行,所有API路径无匹配的处理逻辑 - 静态资源模块拦截范围过宽:Express静态资源托管配置未排除
/api路径,请求进入服务后会优先在client/build静态目录下查找对应路径的文件,找不到时直接触发404响应,跳过后续的API路由匹配 - 兜底路由文件路径拼接错误:SPA兜底逻辑中
sendFile指向的index.html路径,因为__dirname层级判断偏差在生产环境下找不到目标文件,触发Express默认的404响应,哪怕请求路径是合法的API接口路径
对应修复方案
按照优先级逐项排查调整即可:
1. 严格规范Express入口的路由注册顺序
必须遵循「通用中间件挂载 → API路由注册 → 生产环境静态资源托管 → SPA兜底路由」的固定执行顺序,参考正确的结构:
// 1. 挂载全局通用中间件 app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(passport.initialize()); app.use(fileUpload()); // 2. 全局注册所有API路由,必须放在静态资源配置之前,且不受环境分支限制 app.use('/api/user', userRouter); // 其他业务API路由以此类推 // 3. 生产环境下配置静态资源托管 if (process.env.NODE_ENV === 'production') { app.use(express.static(path.join(__dirname, 'client', 'build'))); // 4. 最后注册SPA前端路由兜底逻辑,通过正则排除所有/api开头的请求 app.get(/^\/(?!api).*/, (req, res) => { res.sendFile(path.join(__dirname, 'client', 'build', 'index.html')); }); }
注意规避典型错误写法:不要将API路由的注册逻辑放在仅开发环境执行的else分支中——这是MERN项目部署到Heroku最常见的踩坑点,错误示例参考:
// 错误写法:生产环境完全不会加载API路由 if (process.env.NODE_ENV === 'production') { app.use(express.static('client/build')); app.get('*', (req, res) => res.sendFile(...)); } else { app.use('/api/user', userRouter); }
2. 排查Axios生产环境配置
确认前端Axios的baseURL在生产环境下没有硬编码本地开发地址(比如http://localhost:5000/api),生产环境应使用相对路径(比如/api),让请求自动匹配当前部署的域名。如果你的Network面板中已经看到请求路径为Heroku域名下的合法路径,这项可直接跳过。
3. 生产环境路由加载状态校验
可以在API路由注册逻辑中加入简短日志(比如注册完用户图片接口后打印已加载/api/user/myimages路由),重新部署后查看Heroku控制台日志是否有对应输出。如果日志缺失,说明你的API路由逻辑因为环境分支判断被跳过,直接将路由注册逻辑移到所有环境判断分支的外层即可。
4. 静态资源模块补全排除规则
在Express静态资源托管配置中加入对/api路径的排除,避免静态资源模块优先拦截API请求:
app.use((req, res, next) => { // 仅非API路径的请求传递给静态资源模块处理 if (!req.path.startsWith('/api')) { return express.static(path.join(__dirname, 'client', 'build'))(req, res, next); } next(); });
内容的提问来源于stack exchange,提问作者F Alem
相关产品推荐
相关产品推荐

