生产环境MongoDB部署遇304未修改错误及API请求异常求助
生产环境API请求返回HTML(304错误)问题排查与解决
问题现象
- 开发环境运行正常,前端可正常获取所有数据;生产环境出现304错误,返回HTML,禁用缓存后问题仍存在
- 直接访问服务器API路由(如
https://two2062023.onrender.com/api/hotels/countByCity?cities=berlin,madrid,london)能返回预期JSON,但前端应用内无法获取 - 检查发现前端网络请求路径缺失
/api前缀,怀疑是客户端路由的通配路由导致请求被拦截
相关文件与配置
目录结构

MongoDB配置

服务端index.js
import express from "express"; import dotenv from "dotenv"; import mongoose from "mongoose"; import authRoute from "./routes/auth.js"; import usersRoute from "./routes/users.js"; import hotelsRoute from "./routes/hotels.js"; import roomsRoute from "./routes/rooms.js"; import cookieParser from "cookie-parser"; import cors from "cors"; import path from 'path'; const app = express(); dotenv.config(); const connect = async () => { try { await mongoose.connect(process.env.MONGO); console.log("Connected to mongoDB.") } catch (error) { throw error; } }; mongoose.connection.on("disconnected", () => { console.log("mongoDB disconnected!"); }); //middlewares app.use(cookieParser()) app.use(cors()) app.disable('etag') app.use(express.json()); app.use("/api/auth", authRoute); app.use("/api/users", usersRoute); app.use("/api/hotels", hotelsRoute); app.use("/api/rooms", roomsRoute); app.use((err, req, res, next) => { const errorStatus = err.status || 500; const errorMessage = err.message || "Something went wrong!"; return res.status(errorStatus).json({ success: false, status: errorStatus, message: errorMessage, stack: err.stack, }); }); const __dirname = path.resolve(); if (process.env.NODE_ENV == 'production') { app.use(express.static(path.join(__dirname, '/client/build'))) app.get('*', (req, res) => res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'))) console.log('this has just run!!!') } else { app.get('/', (req, res) => { res.send('Api is running') }) } const port = process.env.PORT || 8800; app.listen(port, () => { connect(); console.log(` server running in ${process.env.NODE_ENV}. Connected to server at port ${port}.`); console.log(` this is your ${path.resolve()} `) console.log(process.env.NODE_ENV) });
前端FeaturedProperties.jsx
import useFetch from "../../hooks/useFetch"; import "./featuredProperties.css"; const FeaturedProperties = () => { const { data, loading, error } = useFetch("/hotels?featured=true&limit=4"); console.log(data) return ( < div className = "fp" > { loading ? ( "Loading" ) : ( < > { data && Array.isArray(data).data.map((item) => ( < div className = "fpItem" key = { item._id && item._id } > < img src = { item.photos && item.photos[0] } alt = "" className = "fpImg" / > < span className = "fpName" > { item.name && item.name } < /span> < span className = "fpCity" > { item.city && item.city } < /span> < span className = "fpPrice" > Starting from $ { item.cheapestPrice && item.cheapestPrice } < /span> { item.rating && < div className = "fpRating" > < button > { item.rating && item.rating } < /button> < span > Excellent < /span> < /div>} < /div> )) } < /> ) } < /div> ); }; export default FeaturedProperties;
前端useFetch hook
import { useEffect, useState } from "react"; import axios from "axios"; const useFetch = (url) => { const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(false); useEffect(() => { const fetchData = async () => { setLoading(true); try { const res = await axios.get(url); setData(res.data); } catch (err) { setError(err); } setLoading(false); }; fetchData(); }, [url]); const reFetch = async () => { setLoading(true); try { const res = await axios.get(url); setData(res.data); } catch (err) { setError(err); } setLoading(false); }; return { data, loading, error, reFetch }; }; export default useFetch;
服务端酒店路由
import express from "express"; import { countByCity, countByType, createHotel, deleteHotel, getHotel, getHotelRooms, getHotels, updateHotel, } from "../controllers/hotel.js"; import Hotel from "../models/Hotel.js"; import { verifyAdmin } from "../utils/verifyToken.js" const router = express.Router(); //CREATE router.post("/", verifyAdmin, createHotel); //UPDATE router.put("/:id", verifyAdmin, updateHotel); //DELETE router.delete("/:id", verifyAdmin, deleteHotel); //GET router.get("/find/:id", getHotel); //GET ALL router.get("/", getHotels); router.get("/countByCity", countByCity); router.get("/countByType", countByType); router.get("/room/:id", getHotelRooms); export default router;
服务端酒店控制器
import Hotel from "../models/Hotel.js"; import Room from "../models/Room.js"; export const createHotel = async (req, res, next) => { const newHotel = new Hotel(req.body); try { const savedHotel = await newHotel.save(); res.status(200).json(savedHotel); } catch (err) { next(err); } }; export const updateHotel = async (req, res, next) => { try { const updatedHotel = await Hotel.findByIdAndUpdate( req.params.id, { $set: req.body }, { new: true } ); res.status(200).json(updatedHotel); } catch (err) { next(err); } }; export const deleteHotel = async (req, res, next) => { try { await Hotel.findByIdAndDelete(req.params.id); res.status(200).json("Hotel has been deleted."); } catch (err) { next(err); } }; export const getHotel = async (req, res, next) => { try { const hotel = await Hotel.findById(req.params.id); res.status(200).json(hotel); } catch (err) { next(err); } }; export const getHotels = async (req, res, next) => { const { min, max, ...others } = req.query; try { const hotels = await Hotel.find({ ...others, cheapestPrice: { $gt: min | 1, $lt: max || 999 }, }).limit(req.query.limit); res.status(200).json(hotels); } catch (err) { next(err); } }; export const countByCity = async (req, res, next) => { const cities = req.query.cities.split(","); try { const list = await Promise.all( cities.map((city) => { return Hotel.countDocuments({ city: city }); }) ); res.status(200).json(list); } catch (err) { next(err); } }; export const countByType = async (req, res, next) => { try { const hotelCount = await Hotel.countDocuments({ type: "hotel" }); const apartmentCount = await Hotel.countDocuments({ type: "apartment" }); const resortCount = await Hotel.countDocuments({ type: "resort" }); const villaCount = await Hotel.countDocuments({ type: "villa" }); const cabinCount = await Hotel.countDocuments({ type: "cabin" }); res.status(200).json([{ type: "hotel", count: hotelCount }, { type: "apartments", count: apartmentCount }, { type: "resorts", count: resortCount }, { type: "villas", count: villaCount }, { type: "cabins", count: cabinCount }, ]); } catch (err) { next(err); } }; export const getHotelRooms = async (req, res, next) => { try { const hotel = await Hotel.findById(req.params.id); const list = await Promise.all( hotel.rooms.map((room) => { return Room.findById(room); }) ); res.status(200).json(list) } catch (err) { next(err); } };
服务端酒店模型
import mongoose from "mongoose"; const HotelSchema = new mongoose.Schema({ name: { type: String, required: true, }, type: { type: String, required: true, }, city: { type: String, required: true, }, address: { type: String, required: true, }, distance: { type: String, required: true, }, photos: { type: [String], }, title: { type: String, required: true, }, desc: { type: String, required: true, }, rating: { type: Number, min: 0, max: 5, }, rooms: { type: [String], }, cheapestPrice: { type: Number, required: true, }, featured: { type: Boolean, default: false, }, }); export default mongoose.model("Hotel", HotelSchema)
解决建议
1. 修复前端API请求路径缺失/api前缀问题
前端所有API请求的URL都缺少/api前缀,导致请求被Express的通配路由app.get('*')捕获,返回了index.html(这是304错误且返回HTML的核心原因)。
方案一:全局配置axios基础URL
在项目入口文件(如index.js或App.js)中添加:
import axios from 'axios'; // 所有axios请求自动拼接/api前缀 axios.defaults.baseURL = '/api';
无需修改useFetch或任何组件的请求代码,所有请求都会自动补全前缀。
方案二:修改useFetch钩子添加前缀
如果不想全局配置,直接修改useFetch:
const useFetch = (url) => { const fullUrl = `/api${url}`; const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(false); useEffect(() => { const fetchData = async () => { setLoading(true); try { const res = await axios.get(fullUrl); setData(res.data); } catch (err) { setError(err); } setLoading(false); }; fetchData(); }, [fullUrl]); const reFetch = async () => { setLoading(true); try { const res = await axios.get(fullUrl); setData(res.data); } catch (err) { setError(err); } setLoading(false); }; return { data, loading, error, reFetch }; };
2. 修复FeaturedProperties中的数据处理错误
Array.isArray(data).data.map是错误写法,Array.isArray(data)返回布尔值,无法访问.data。修改为:
{data && Array.isArray(data) ? data.map((item) => ( <div className="fpItem" key={item._id}> <img src={item.photos?.[0]} alt="" className="fpImg" /> <span className="fpName">{item.name}</span> <span className="fpCity">{item.city}</span> <span className="fpPrice">Starting from ${item.cheapestPrice}</span> {item.rating && ( <div className="fpRating"> <button>{item.rating}</button> <span>Excellent</span> </div> )} </div> )) : null}
使用可选链?.简化空值判断,避免冗余的&&逻辑。
3. 强化API缓存控制(解决304问题)
在服务端API路由前添加缓存控制中间件,确保浏览器不缓存API响应:
// 放在app.use(express.json())之后,API路由之前 app.use((req, res, next) => { if (req.path.startsWith('/api')) { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); res.setHeader('Surrogate-Control', 'no-store'); } next(); });
4. 验证生产环境路由顺序
当前服务端路由顺序是正确的:先注册API路由,再处理静态文件,最后是通配路由返回index.html。确保process.env.NODE_ENV在生产环境被正确设置为production,否则通配路由不会生效。
内容的提问来源于stack exchange,提问作者stmb
相关产品推荐
相关产品推荐

