MongooseでCastError: Cast to ObjectId Failed for Invalid IDを修正する方法

beginner🍃 MongoDB2026-04-13| Node.js 18+、Mongoose 6〜8、MongoDB 5〜7、Express.js

Error Message

CastError: Cast to ObjectId failed for value "invalid_id" (type string) at path "_id" for model "User"
#mongodb#mongoose#objectid#castError#nodejs

何が起きたのか

GET /users/invalid_id のようなルートにアクセスしたとき — 壊れたリンク、URLの誤入力、フロントエンドのバグなどが原因で — クリーンな404レスポンスの代わりに、アプリがクラッシュします:

CastError: Cast to ObjectId failed for value "invalid_id" (type string) at path "_id" for model "User"

Mongooseは文字列 "invalid_id" をMongoDB ObjectIdにキャストしようとしました。その文字列が有効な24文字の16進数値ではないため、失敗しました。try/catchやグローバルエラーハンドラーがなければ、拒否されたPromiseは未処理のままとなり、リクエストが落ちます。

なぜこうなるのか

MongoDB ObjectIdは12バイトの値で、通常 507f1f77bcf86cd799439011 のような24文字の16進数文字列として表されます。スキーマフィールドが ObjectId 型として定義されている場合、Mongooseは渡された値を自動的にキャストしようとします。無効な値の場合は、クエリがMongoDBに到達する前に即座に CastError がスローされます。

よくある原因:

  • ルートパラメーターとして "me""current" などの文字列エイリアスを使用している(例:/users/me
  • ObjectIdが期待される場所にUUIDやスラッグを渡している
  • フロントエンドのリクエストから不正な形式のIDが送られてくる
  • テスト中にIDをコピー&ペーストして切り詰めてしまった(例:24文字ではなく20文字)
  • レガシーシステムからの数値IDがMongooseモデルに転送されている

クイックフィックス — クエリ前にバリデーションする

IDがMongooseに到達する前にチェックします。isValid() を一度呼び出すだけで十分です:

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);
}

400ではなく404を返すのは意図的です。クライアントの視点では、そのIDを持つリソースは単純に存在しません。バリデーションが失敗したことを公開しても何の価値もなく、攻撃対象を若干広げるだけです。

恒久的な対策 — ミドルウェアによるバリデーション

すべてのコントローラーに isValid() チェックをコピーするのはすぐに面倒になります。代わりにミドルウェアに切り出しましょう:

// 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;

次に、ルーターに組み込みます:

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);

特殊ケースの対処:/users/me

非常によくあるシナリオとして、/me ルートと /:id を併用するケースがあります。Expressが先に /:id を評価すると、"me" がハンドラーに渡されてObjectIdのキャストが失敗します。ルートの順序を修正しましょう:

// 特定のルートをパラメーター化されたルートの前に配置する
router.get('/users/me', authMiddleware, getCurrentUser);  // 先にマッチする
router.get('/users/:id', validateObjectId, getUserById);  // フォールバック

安全網としてのグローバルエラーハンドラー

バリデーションはほとんどのケースをカバーしますが、漏れることもあります。グローバルエラーハンドラーは最後の砦です — トップに到達した CastError をキャッチし、スタックトレースがレスポンスに漏れないようにします:

// app.js — すべてのルートの後
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' });
});

これはバリデーションの代替ではなく、最後の手段のキャッチです。両方を使いましょう。

ExpressのAsync/awaitには落とし穴がある

express-async-errors またはラッパー関数がないと、asyncハンドラー内で未処理のPromise拒否はグローバルエラーミドルウェアに届きません。手動でtry/catchを追加するか、パッケージをインストールしましょう:

npm install express-async-errors
// app.js の先頭、ルートより前
require('express-async-errors');

この1行でExpressにパッチが当たり、asyncハンドラー内でスローされたエラーが自動的にエラーミドルウェアに転送されます。ラッパーのボイラープレートは不要です。

修正の確認

curlまたはHTTPクライアントでテストします:

# 500ではなく404が返るべき
curl -i http://localhost:3000/users/invalid_id

# ユーザーが見つかれば返し、見つからなければ404
curl -i http://localhost:3000/users/507f1f77bcf86cd799439011

無効なIDに対する期待されるレスポンス:

HTTP/1.1 404 Not Found
{ "error": "Resource not found" }

レスポンスにスタックトレースなし、サーバーログにクラッシュなし。これが目標です。

ボーナス:再利用可能なisValidObjectIdユーティリティ

ObjectIdが必要なのはルートハンドラーだけではありません。サービス層、バックグラウンドジョブ、データパイプライン — すべて同じチェックが必要です。小さなユーティリティで一貫性を保ちましょう:

// utils/isValidObjectId.js
const mongoose = require('mongoose');

const isValidObjectId = (id) => mongoose.Types.ObjectId.isValid(id);

module.exports = isValidObjectId;

// 使用例
const isValidObjectId = require('./utils/isValidObjectId');

if (!isValidObjectId(someId)) {
  throw new Error(`Invalid ID: ${someId}`);
}

Related Error Notes