Chuyện Gì Xảy Ra
Bạn truy cập một route như GET /users/invalid_id — có thể từ một link hỏng, gõ sai URL, hoặc lỗi phía frontend — và thay vì nhận một lỗi 404 gọn gàng, ứng dụng lại crash với:
CastError: Cast to ObjectId failed for value "invalid_id" (type string) at path "_id" for model "User"
Mongoose đã cố ép kiểu chuỗi "invalid_id" thành MongoDB ObjectId. Thao tác này thất bại vì chuỗi đó không phải giá trị hex hợp lệ 24 ký tự. Nếu không có try/catch hoặc trình xử lý lỗi toàn cục, lỗi rejection không được xử lý và khiến request bị hỏng.
Tại Sao Điều Này Xảy Ra
MongoDB ObjectId là giá trị 12 byte, thường được biểu diễn dưới dạng chuỗi hex 24 ký tự như 507f1f77bcf86cd799439011. Khi một trường trong schema được khai báo kiểu ObjectId, Mongoose tự động cố ép kiểu bất kỳ giá trị nào bạn truyền vào. Giá trị không hợp lệ? Nó ném ngay một CastError — trước khi query chạm đến MongoDB.
Các nguyên nhân thường gặp:
- Dùng
"me","current", hoặc các chuỗi tương tự làm tham số route (ví dụ:/users/me) - Truyền UUID hoặc slug vào chỗ cần ObjectId
- ID bị lỗi cấu trúc gửi từ phía frontend
- ID bị cắt ngắn khi copy-paste lúc test (ví dụ: chỉ 20 ký tự thay vì 24)
- ID dạng số từ hệ thống cũ được chuyển tiếp vào Mongoose model
Sửa Nhanh — Kiểm Tra Trước Khi Query
Kiểm tra ID trước khi nó đến tay Mongoose. Chỉ cần một lần gọi isValid() là đủ:
const mongoose = require('mongoose');
async function getUserById(req, res) {
const { id } = req.params;
if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(404).json({ error: 'User not found' });
}
const user = await User.findById(id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
}
Trả về 404 thay vì 400 là có chủ đích. Từ góc nhìn của client, tài nguyên với ID đó đơn giản là không tồn tại. Tiết lộ rằng validation thất bại không mang lại giá trị gì — và còn mở rộng thêm bề mặt tấn công.
Sửa Lâu Dài — Validation Bằng Middleware
Sao chép kiểm tra isValid() vào từng controller sẽ rất nhàm chán. Hãy đưa nó vào một middleware:
// middleware/validateObjectId.js
const mongoose = require('mongoose');
function validateObjectId(req, res, next) {
if (!mongoose.Types.ObjectId.isValid(req.params.id)) {
return res.status(404).json({ error: 'Resource not found' });
}
next();
}
module.exports = validateObjectId;
Sau đó gắn vào router của bạn:
const express = require('express');
const router = express.Router();
const validateObjectId = require('../middleware/validateObjectId');
router.get('/users/:id', validateObjectId, getUserById);
router.put('/users/:id', validateObjectId, updateUser);
router.delete('/users/:id', validateObjectId, deleteUser);
Xử Lý Trường Hợp Đặc Biệt: /users/me
Một tình huống rất phổ biến là có route /me song song với /:id. Nếu Express xử lý /:id trước, nó sẽ truyền "me" vào handler và việc ép kiểu ObjectId thất bại. Hãy sửa thứ tự route:
// Đặt các route cụ thể TRƯỚC các route có tham số
router.get('/users/me', authMiddleware, getCurrentUser); // khớp trước
router.get('/users/:id', validateObjectId, getUserById); // dự phòng
Trình Xử Lý Lỗi Toàn Cục Như Một Lưới An Toàn
Validation xử lý được hầu hết các trường hợp, nhưng vẫn có thể có lọt qua. Trình xử lý lỗi toàn cục là phòng tuyến cuối cùng — nó bắt mọi CastError đẩy lên đến đỉnh và ngăn stack trace lộ ra ngoài:
// app.js — sau tất cả các route
app.use((err, req, res, next) => {
if (err.name === 'CastError' && err.kind === 'ObjectId') {
return res.status(404).json({ error: 'Resource not found' });
}
console.error(err);
res.status(500).json({ error: 'Internal server error' });
});
Đây là biện pháp bắt lỗi cuối cùng, không phải thay thế cho validation. Hãy dùng cả hai.
async/await Trong Express Có Một Điểm Cần Lưu Ý
Nếu không có express-async-errors hoặc một hàm wrapper, các promise rejection không được xử lý bên trong async handler sẽ không bao giờ đến được middleware xử lý lỗi toàn cục. Hãy thêm try/catch thủ công, hoặc đơn giản là cài package này:
npm install express-async-errors
// Ở đầu app.js, trước các route
require('express-async-errors');
Một dòng đó sẽ vá Express để các lỗi ném ra bên trong async handler tự động được chuyển tiếp đến middleware xử lý lỗi. Không cần boilerplate wrapper nào cả.
Kiểm Tra Lại Sau Khi Sửa
Kiểm tra bằng curl hoặc HTTP client của bạn:
# Phải trả về 404, không phải 500
curl -i http://localhost:3000/users/invalid_id
# Phải trả về user hoặc 404 nếu không tìm thấy
curl -i http://localhost:3000/users/507f1f77bcf86cd799439011
Phản hồi mong đợi khi ID không hợp lệ:
HTTP/1.1 404 Not Found
{ "error": "Resource not found" }
Không có stack trace trong phản hồi, không có crash trong server log. Đó là mục tiêu cần đạt.
Thêm: Utility isValidObjectId Có Thể Tái Sử Dụng
Route handler không phải nơi duy nhất xuất hiện ObjectId. Các tầng service, background job, data pipeline — tất cả đều cần cùng một kiểm tra. Một utility nhỏ giúp giữ tính nhất quán:
// utils/isValidObjectId.js
const mongoose = require('mongoose');
const isValidObjectId = (id) => mongoose.Types.ObjectId.isValid(id);
module.exports = isValidObjectId;
// Cách dùng
const isValidObjectId = require('./utils/isValidObjectId');
if (!isValidObjectId(someId)) {
throw new Error(`Invalid ID: ${someId}`);
}

