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

基于JavaScript与Mongoose的无效_id错误处理问题排查

问题描述
  • 开发模式下:除updateTour接口外,其他接口能正常返回所有错误信息;调用updateTour时仅返回旧对象,不会触发名称过短等验证错误。
  • 生产模式下:所有接口仅返回错误状态码,updateTour接口同样存在验证错误不触发、无效ID错误处理不当的问题。
  • 需求:程序需正确处理无效_id错误,且updateTour接口能正常触发验证错误并返回对应提示。
相关代码

errorController.js

const AppError = require('./../utils/appError');

const handleCastErrorDB = err => {
  const message = `Invalid ${err.path}: ${err.value}.`;
  return new AppError(message, 400);
};

const handleDuplicateFieldsDB = err => {
  const value = err.errmsg.match(/(["'])(\\?.)*?\1/)[0];
  console.log(value);

  const message = `Duplicate field value: ${value}. Please use another value!`;
  return new AppError(message, 400);
};
const handleValidationErrorDB = err => {
  const errors = Object.values(err.errors).map(el => el.message);

  const message = `Invalid input data. ${errors.join('. ')}`;
  return new AppError(message, 400);
};

const sendErrorDev = (err, res) => {
  res.status(err.statusCode).json({
    status: err.status,
    error: err,
    message: err.message,
    stack: err.stack
  });
};

const sendErrorProd = (err, res) => {
  // Operational, trusted error: send message to client
  if (err.isOperational) {
    res.status(err.statusCode).json({
      status: err.status,
      message: err.message
    });

    // Programming or other unknown error: don't leak error details
  } else {
    // 1) Log error
    console.error('ERROR 💥', err);

    // 2) Send generic message
    res.status(500).json({
      status: 'error',
      message: 'Something went very wrong!'
    });
  }
};

module.exports = (err, req, res, next) => {
  // console.log(err.stack);

  err.statusCode = err.statusCode || 500;
  err.status = err.status || 'error';

  if (process.env.NODE_ENV === 'development') {
    sendErrorDev(err, res);
  } else if (process.env.NODE_ENV === 'production') {
    let error = { ...err };

    if (error.name === 'CastError') error = handleCastErrorDB(err);
    if (error.code === 11000) error = handleDuplicateFieldsDB(err);
    if (error._message === 'Tour validation failed')
      error = handleValidationErrorDB(err);

    sendErrorProd(error, res);
  }
};

tourController.js

const Tour = require('../models/tourModel');
const APIFeatures = require('../utils/apiFeatures');
const catchAsync = require('../utils/catchAsync');
const AppError = require('../utils/appError');

/**
 * Middleware to get the five cheapest tours in the database
 * @param {*} req
 * @param {*} res
 * @param {*} next
 */

exports.aliasTopTours = (req, res, next) => {
  req.query.limit = '5';
  req.query.sort = '-ratingsAverage,price';
  req.query.fields = 'name,price,ratingsAverage,summary,difficulty';
  next();
};

/**
 * Get all tours in the database
 * @param {*} req
 * @param {*} res
 */
exports.getAllTours = catchAsync(async (req, res, next) => {
  const features = new APIFeatures(Tour.find(), req.query)
    .filter()
    .sort()
    .limitFields()
    .paginate();

  const tours = await features.query;
  if(!tours){
    return next(new AppError('Couldn\'t find any tour in the database',404))
  }

  res.status(200).json({
    status: 'success',
    requestedAt: req.requstTime,
    results: tours.length,
    data: {
      tours,
    },
  });
});

/**
 * Create a new tour in the database
 * @param {*} req
 * @param {*} res
 */
exports.createTour = catchAsync(async (req, res, next) => {
  const newTour = await Tour.create(req.body);

  res.status(201).json({
    status: 'success',
    data: {
      tour: newTour,
    },
  });
});

/**
 * Get a single tour in the database
 * @param {*} req
 * @param {*} res
 */
exports.getTour = catchAsync(async (req, res, next) => {
   const tour = Tour.findById(req.params.id).then((tour) => {
    
    res.status(200).json({
      status: 'success',
      requestedAt: req.requstTime,
      data: {
        tour,
      },
    });
  }).catch((err) => {
    return next(new AppError('No tour found with that ID', 404))
  });
});

/**
 * Update a single tour in the database
 * @param {*} req
 * @param {*} res
 */
exports.updateTour = catchAsync(async (req, res,next) => {
  Tour.findByIdAndUpdate(req.params.id, req.body, {
    new: true,
    runValidators: true,
  }).then((tour) => {
    res.status(200).json({
      status: 'success',
      requestedAt: req.requstTime,
      data: {
        tour,
      },
    });
  }).catch((err) => {
    return next(new AppError('No tour found with that ID', 404))
  });
});

/**
 * Delete a single tour in the database
 */
exports.deleteTour = catchAsync(async (req, res,next ) => {
  Tour.findByIdAndDelete(req.params.id).then((tour) => {
    res.status(204).json({
      status: 'success',
      requestedAt: req.requstTime,
      data: {
        tour,
      },
    });
  });
});

/**
 * Calculate avg rating and avg price for all tours
 */
exports.getTourStatus = catchAsync(async (req, res,next) => {
  const stats = await Tour.aggregate([
    {
      $match: { ratingsAverage: { $gte: 4.5 } },
    },
    {
      $group: {
        _id: { $toUpper: '$difficulty' },
        numTours: { $sum: 1 },
        numRatings: { $sum: '$ratingsQuantity' },
        avgRating: { $avg: '$ratingsAverage' },
        avgPrice: { $avg: '$price' },
        minPrice: { $min: '$price' },
        maxPrice: { $max: '$price' },
      },
    },
    {
      $sort: { avgPrice: 1 },
    },
  ]);

  res.status(200).json({
    status: 'success',
    data: {
      stats,
    },
  });
});

/**
 * Show how many tours start in each month
 * @param {*} req
 * @param {*} res
 */
exports.getMonthlyPlan = catchAsync(async (req, res, next) => {
    const year = req.params.year * 1;
    const plan = await Tour.aggregate([
      {
        $unwind: '$startDates',
      },
      {
        $match: {
          startDates: {
            $gte: new Date(`2023-01-01`),
            $lte: new Date(`2023-12-31`),
          },
        },
      },
      {
        $group: {
          _id: { $month: '$startDates' },
          numTourStarts: { $sum: 1 },
          tours: { $push: '$name' },
        },
      },
      {
        $addFields: { month: '$_id' },
      },
      {
        $project: {
          _id: 0,
        },
      },
      {
        $sort: { numTourStarts: -1 },
      },
    ]);

    res.status(200).json({
      status: 'success',
      results: plan.length,
      requestedAt: req.requstTime,
      data: {
        plan,
      },
    });  
});
生产模式运行截图

生产模式运行截图

问题分析与修复方案

核心问题1:混用async/await与Promise链式调用,错误未正确传递到全局错误处理器

updateTour、getTour、deleteTour方法被包裹在catchAsync中,但内部又使用了.then()和.catch()。这种混用会导致:

  • 验证错误(如名称过短)、CastError(无效ID)等数据库错误被本地.catch()捕获,直接返回自定义404错误,无法触发全局错误处理器的格式化逻辑。
  • catchAsync无法捕获Promise链式调用中的错误,导致错误处理流程断裂。

核心问题2:生产模式下错误对象复制不完整

在errorController.js的生产模式分支中,使用let error = { ...err }复制错误对象,但展开运算符无法复制错误的原型属性(如name、_message),导致后续的错误类型判断失效。

核心问题3:拼写错误导致requestedAt字段异常

代码中多处出现req.requstTime的拼写错误,正确应为req.requestTime(假设你在中间件中设置了该字段)。


修复步骤

1. 统一使用async/await语法,移除.then()和.catch()

以updateTour为例,修改后代码:

exports.updateTour = catchAsync(async (req, res, next) => {
  const tour = await Tour.findByIdAndUpdate(req.params.id, req.body, {
    new: true,
    runValidators: true,
  });

  // 仅当未找到文档时返回404
  if (!tour) {
    return next(new AppError('No tour found with that ID', 404));
  }

  res.status(200).json({
    status: 'success',
    requestedAt: req.requestTime, // 修正拼写错误
    data: {
      tour,
    },
  });
});

同理修改getTour和deleteTour方法,确保错误被catchAsync捕获后传递到全局错误处理器。

2. 修复生产模式下错误对象的复制逻辑

在errorController.js中,替换错误对象复制方式,确保关键属性被保留:

// 原代码
let error = { ...err };

// 修改为
let error = Object.assign({}, err);
error.name = err.name;
error._message = err._message;
error.code = err.code;

3. 验证修复效果

  • 开发模式下调用updateTour传入无效数据(如过短的名称),应返回包含验证错误信息的响应。
  • 传入无效_id时,应返回Invalid _id: xxx的格式化错误信息。
  • 生产模式下,所有错误将按sendErrorProd的逻辑返回对应的友好提示,而非仅状态码。

内容的提问来源于stack exchange,提问作者Tor Bloodaxe

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 18:35:15