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

生产环境MongoDB部署遇304未修改错误及API请求异常求助

生产环境API请求返回HTML(304错误)问题排查与解决

问题现象

  • 开发环境运行正常,前端可正常获取所有数据;生产环境出现304错误,返回HTML,禁用缓存后问题仍存在
  • 直接访问服务器API路由(如https://two2062023.onrender.com/api/hotels/countByCity?cities=berlin,madrid,london)能返回预期JSON,但前端应用内无法获取
  • 检查发现前端网络请求路径缺失/api前缀,怀疑是客户端路由的通配路由导致请求被拦截

相关文件与配置

目录结构

目录结构

MongoDB配置

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 06:54:55