Next.js middleware の例外ルートが本番だけ 404 になる罠 — Vercel env 未設定の落とし穴
結論
ローカルで動く middleware 例外ルートが本番 Vercel でだけ 404 になる原因は、ほぼ確実に環境変数の未設定です。
vercel env ls production を一発叩いて、middleware が参照しているキーが production 欄に存在するか確認してください。存在しなければそれが原因です。コードは正しい。設定が抜けているだけです。
この記事の前提
筆者が個人開発している Next.js App Router + Vercel のポートフォリオアプリで、内部バッチ処理用の API ルート(/api/lab-tools/issue-finder/process-job)を Claude Code のスキルから自動実行する仕組みを構築しました。
外部からの不正アクセスを防ぐために、x-internal-key ヘッダーに一致した場合だけ通過させる middleware の例外ルートを実装しています。この設定はローカル開発環境(npm run dev)では完璧に動いていました。
スタック:
- Next.js 16 App Router
- Vercel(本番デプロイ)
- Claude Code MAX(自動化スキル経由でバッチ実行)
読者のよくある詰まり
「middleware の例外ルートを実装したのに、なぜか本番だけ 404 が返る」
このパターンで詰まった経験がある方は多いはずです。理由は単純で、ローカルの .env.local と Vercel の environment variables は別々に管理する必要があるからです。
// middleware.ts — ローカルでは完璧に動く
import { NextRequest, NextResponse } from 'next/server'
export function middleware(req: NextRequest) {
const internalKey = req.headers.get('x-internal-key')
const expectedKey = process.env.ISSUE_FINDER_INTERNAL_KEY
if (internalKey && internalKey === expectedKey) {
return NextResponse.next() // ← 内部キーが一致すれば通過
}
// 以降は通常の認証フロー
return NextResponse.next()
}
このコードは何も間違っていません。しかし process.env.ISSUE_FINDER_INTERNAL_KEY が本番 Vercel に登録されていなければ、expectedKey は undefined になります。
undefined === undefined は true なので、すべての x-internal-key ヘッダー付きリクエストが通過してしまうと思いきや——実際には middleware が認証フローへ飛ばす前に別のルートが 404 を返すケースもあります(ミドルウェアの評価順、matcher の設定によって挙動は変わります)。
今回の実録では、404 が返り続けました。
実際に起きたこと
事象
Claude Code のスキルから本番 API エンドポイントに POST を送信:
curl -X POST https://ihara-frontend.com/api/lab-tools/issue-finder/process-job \
-H "x-internal-key: $ISSUE_FINDER_INTERNAL_KEY" \
-H "Content-Type: application/json"
結果: 404 が返り続ける。
最初の仮説と検証
- middleware.ts のコードが間違っている? → ローカルで
npm run devして同じリクエストを送ると200 OK、8 件 insert 成功。コードは正しい。 - matcher の設定ミス? →
matcherを確認。/api/lab-tools/:path*を明示的に除外しているので問題なし。 - スキルのドキュメントが古い? → スキルの説明書には「本番 middleware の INTERNAL_KEY 経由で通る」と書いてあった。
原因判明
vercel env ls production | grep ISSUE_FINDER
出力: なし(空)
ISSUE_FINDER_INTERNAL_KEY が production 環境に登録されていませんでした。ローカルの .env.local には書いてあるのに、Vercel の production 環境変数への登録を忘れていたのです。
2026-05-20 朝、自動化スキルから本番 API に POST が通らず、手動で dev server を立ち上げて localhost 経由で実行することで回避しました。vercel env ls production を叩いたら ISSUE_FINDER_INTERNAL_KEY が空で、即座に原因が判明。env 登録後は本番でも正常に動作しています。
原因分析
なぜこのミスが起きるのか。構造的な理由が 2 つあります。
理由 1: ローカルと本番で環境変数は独立している
Next.js 開発時に使う .env.local は Vercel には自動的に反映されません。
| 場所 | 環境変数の管理方法 |
|---|---|
| ローカル開発 | .env.local(git には含めない) |
| Vercel production | Vercel Dashboard → Settings → Environment Variables |
| Vercel preview | 同上(production と別設定可能) |
.env.local で動作確認 → Vercel にデプロイ、という流れを繰り返していると、「ローカルで動いた → 本番でも動くはず」という思い込みが生まれます。
理由 2: middleware のエラーは静かで分かりにくい
middleware が原因で 404 を返す場合、エラーログに明示的な「middleware error」は残りません。アプリケーションの API ルートが見つからないのか、middleware が弾いているのか、外側からは区別がつきません。
Vercel のログを見ると、404 の記録はあっても「なぜ 404 なのか」の原因は記録されていません。これがデバッグを長引かせます。
判断基準: ローカルで動くのに本番で 404 になる時のチェックリスト
# Step 1: 本番の環境変数を確認
vercel env ls production
# Step 2: middleware が参照しているキーが存在するか確認
vercel env ls production | grep <キー名>
# Step 3: 存在しなければ追加
vercel env add <キー名> production
# Step 4: 再デプロイ(env 追加後は再デプロイが必要)
vercel deploy --prod
これで解決しない場合は、次を疑います:
- matcher が正しく設定されているか —
matcherの除外パターンを確認 - middleware の実行順序 — Edge Runtime での実行タイミング
- Vercel Edge Network のキャッシュ — Vercel のコントロールパネルからキャッシュを purge
dev server の hot reload エラーとの組み合わせ
余談ですが、同日に別の問題も踏みました。
本番 API が 404 を返す中で、dev server を使って回避しようとしたところ、dev server(PORT=3010)が「missing required error components, refreshing...」という HTML を返し続ける状態に陥りました。
GET /api/lab-tools/issue-finder/jobs は 200 を返すのに、POST /api/lab-tools/issue-finder/process-job だけ 404 HTML を返す状態。
解決策: 別ポートで起動していた dev server(PORT=3000)に切り替えたら問題なく動いた。
これは Next.js の middleware hot reload バグで、複数の dev server インスタンスが同じポートを争うとこういう状態になることがあります。根本解決は dev server を完全に停止してから再起動することですが、別ポートへの切り替えが最速の回避策でした。
今日やること(3 つ以内)
- 既存の内部 API を持っているなら今すぐ確認:
vercel env ls productionで必要な env が全部登録されているか照合する - スキル・ドキュメントに「本番確認手順」を追記: 「本番で動くか確認するには
vercel env lsで env を確認→本番エンドポイントに curl テスト」をチェックリストに入れる - 自動化スキルに疎通チェックを追加: スキル実行前に本番エンドポイントへの ping チェックを入れ、404 が返る場合は dev server fallback か早期失敗させる
で、どう稼ぐ?
内部 API が止まっていても、Claude Code のスキルはエラーを静かに飲み込むことがあります。今回のケースでいえば、バッチが「実行した体」で完了していても DB への insert は 0 件、というデータ欠損が無音で起きていました。
これが収益化インフラに直結する処理(Stripe の Webhook 処理、メルマガ配信履歴の記録、購入フラグの更新)だったら? 気づかないまま顧客への配信が止まり、課金だけ続く、という状況が発生します。
自動化の恩恵は「気づかなくても動いていること」ですが、逆側のリスクは「気づかなくても止まっていること」です。
月 5 万を目指す個人開発者がやるべきことは一つ: 自動化 routine に疎通チェックを入れること。 失敗時に Slack や LINE に通知するだけでいい。完璧な監視は不要です。「止まっていることに気づける」だけで、ユーザーへの影響を最小化できます。
- Supabase RLS Service Role の silent failure パターン
- AI agent が skill 規範を破った実録 — Qiita CI 14 回連続失敗と機械ガード設計
masatoman のメルマガ — 毎週月曜の朝に手紙を 1 通
masatoman.net の今週の記事 1 本を、読者目線で深掘りした手紙が毎週月曜 9:00 に届きます。「これ自分のことだ」が見つかる予告編。登録特典に「個人開発の収益化チェックリスト 15 項目」。
masatoman のメルマガ — 毎週月曜の朝に 1 通
masatoman.net で今週公開した記事の中から 1 本を、読者目線で深掘りした手紙が届きます。「自分も同じことやってる」「ここで詰まってた」が見つかる予告編。
Next Step
次に読むならこの導線です
【第12回】夜寝てる間に Claude Code が記事を書き上げる構成 — 月 ¥5K で動く全コード
Claude Codeラボ全12話の集大成。Skills/MCP/サブエージェント/Hooks/リモート運用を統合した「自走する Claude 自動化」を、月 ¥5K の実コストで動かす全構成を公開。寝てる間に競合調査・記事下書き・PR まで自動化する 6 層アーキテクチャの完成版。
【第12回】夜寝てる間に Claude Code が記事を書き上げる構成 — 月 ¥5K で動く全コード
Claude Codeラボ全12話の集大成。Skills/MCP/サブエージェント/Hooks/リモート運用を統合した「自走する Claude 自動化」を、月 ¥5K の実コストで動かす全構成を公開。寝てる間に競合調査・記事下書き・PR まで自動化する 6 層アーキテクチャの完成版。
Claude Code で 0→MVP を1日で作る全記録 — recipe-ai Build in Public
Claude Codeを使い、YouTube料理動画からレシピを自動抽出するAIアプリ「recipe-ai」を0からMVPまで1日で構築した全記録。CLAUDE.md設計、API実装、Supabase連携、Vercelデプロイ、Stripe課金導入までの工程を時系列で公開。
【第1回】Claude Code Skills 入門 — 自作スキルで開発効率を2倍にする実装ガイド
Claude Code の Skills 機能を自作する手順を、masatoman.net 周辺の自動化を Claude Code で回している立場で実コード付きで解説。1 スキル 15 分の投資で月 10 時間の作業を削減する実装ガイドです。
次の実験記録も追う
Claude Code × 個人開発の実験ログ、失敗、判断変更をまとめて追いたい人向けに、月次でLab Freeを届けます。
masatoman のメルマガ — 毎週月曜の朝に 1 通
masatoman.net で今週公開した記事の中から 1 本を、読者目線で深掘りした手紙が届きます。「自分も同じことやってる」「ここで詰まってた」が見つかる予告編。
この記事が役に立ったらシェア