「エラー番号しか出ない」Next.js 本番不具合を、本番を壊さずに原因まで追い詰める
Next.js の error digest だけが出る本番障害を、書き込みゼロの再現・スキーマ読み取り・hidden input required の罠の3点で特定し、Server Action から throw を無くして二度と番号だけにしない手順
約5.7万トークンの節約 (API料金換算で約85円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。
この巻物について
「「エラー番号しか出ない」Next.js 本番不具合を、本番を壊さずに原因まで追い詰める」は、Web開発カテゴリのAI指示書(MDファイル)です。Next.js の error digest だけが出る本番障害を、書き込みゼロの再現・スキーマ読み取り・hidden input required の罠の3点で特定し、Server Action から throw を無くして二度と番号だけにしない手順この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約5.7万トークン(API料金換算で約85円)・95%のトークンを節約できます。
- カテゴリ
- Web開発
- 対応AI
- claude-code、cursor、codex-cli
- ライセンス
- 商用利用可 (再販不可)
- 価格
- 無料
- ゼロから開発時
- 約6万トークン
- この巻物使用時
- 約3,200トークン
- 節約量
- 約5.7万トークン (約85円)
- 更新日
- 2026-09-17
使い方 (AIに渡す3つの方法)
いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。
claude "https://makimono-md.vercel.app/api/v1/files/next-js-2/raw を読み込んで、この指示書どおりに実装して"
中身
「エラー番号しか出ない」Next.js 本番不具合を、本番を壊さずに原因まで追い詰める
対象: Next.js App Router(Server Actions / error.tsx)を Vercel 本番で運用していて、利用者から「エラー識別子: 1234567890 で登録できない」とだけ報告が来た状況。AI エージェントにそのまま渡して調査させる前提で書いてある。
0. 前提として知っておくこと(これを知らないと調査が空回りする)
- digest は
stringHash(err.message + err.stack)。実装はnext/dist/server/app-render/create-error-handler.js。stack は本番では minify 済みなので、digest から元のメッセージを逆算することはできない。 vercel logs <domain>は「今から先」の tail であって過去ログの検索ではない。報告された digest のログは、報告から時間が経っていればもう取れない。- したがって最短経路は「ログを探す」ではなく 「digest が出ない形にコードを直す」。直せば次の1回で日本語の原因が画面に出る。
1. まず切り分ける: ページ描画で落ちているのか、送信で落ちているのか
書き込みを一切起こさずに確認できる。Playwright で本番にログインして GET するだけのテストを1本書く。
// e2e/repro.spec.ts ※ storageState でログイン済みコンテキストを使う
import { test } from "@playwright/test";
const BASE = process.env.E2E_BASE_URL ?? "https://<本番ドメイン>";
for (const path of ["/dashboard", "/<問題のページ>"]) {
test(`GET ${path}`, async ({ page }) => {
const resp = await page.goto(BASE + path, { waitUntil: "domcontentloaded" });
const body = await page.locator("body").innerText();
const m = body.match(/エラー識別子:\s*(\d+)/); // error.tsx の文言に合わせる
console.log(path, resp?.status(), m ? m[1] : "no-digest");
console.log(body.slice(0, 400));
});
}
- ここで digest が出れば ページ描画の問題。出なければ 送信(Server Action)の問題で、以下へ進む。
- 本番へのフォーム実送信は「本番データの書き込み」なので、安全機構に止められることがある(止められるのが正しい)。止められた前提で進められる手順を以下に用意している。
2. スキーマ側の可能性を、読み取りだけで潰す
Server Action の INSERT が落ちている可能性は、本番DBを読むだけで大半を否定できる。PostgreSQL なら:
-- NOT NULL かつ default 無しの列(INSERT ペイロードに無ければ必ず失敗する)
select column_name, data_type from information_schema.columns
where table_schema='public' and table_name='<table>' and is_nullable='NO' and column_default is null;
-- CHECK / FK
select conname, pg_get_constraintdef(oid) from pg_constraint
where conrelid='public.<table>'::regclass and contype in ('c','f');
-- RLS(アプリは anon キー+ユーザーセッションで書くので、ここが閉じていれば全件失敗する)
select policyname, cmd, qual, with_check from pg_policies
where schemaname='public' and tablename='<table>';
ここが全部素直なら、原因は 入力バリデーション(zod 等) 側に寄る。
3. いちばん多い犯人: 「hidden input の required」
検索コンボボックス(可視の text 入力+選択結果を入れる hidden input)でこれが起きる。
// ❌ これは効かない
<input type="hidden" name="customer_id" value={selected?.id ?? ""} required />
HTML 仕様上 hidden input は制約検証(constraint validation)から除外されるため、required を付けてもブラウザは送信を止めない。利用者が「名前を打ったが候補をクリックしていない」状態で送信でき、サーバ側の z.string().uuid() が落ちて digest 画面になる。しかも利用者から見ると「ちゃんと入力したのに登録できない」ので、報告からは原因が分からない。
// ✅ 可視 input 側で止める
const textRef = useRef<HTMLInputElement>(null);
useEffect(() => {
textRef.current?.setCustomValidity(
required && !selected ? "候補の中から選んでください(入力しただけでは選択されません)" : "",
);
}, [required, selected]);
<input type="hidden" name={name} value={selected?.id ?? ""} />
<input ref={textRef} type="text" value={query} onChange={...} />
同種の罠: disabled な入力は送信されない / type="number" はカンマ付き文字列で z.coerce.number() が NaN になり ZodError になる / フォームが送っているのにスキーマが拾っていない項目はエラーも出さずに黙って保存されない。
4. 恒久対策: Server Action から throw を無くす
Server Action で throw すると、利用者には digest 番号しか届かない。入力エラーも DB エラーも throw せず、入力値を保持したままフォームへ戻す。
function backToForm(raw: Record<string, unknown>, message: string): never {
const carry = new URLSearchParams({ /* 入力値を全部持ち帰る */ error: message });
redirect(`/<フォームのパス>?${carry.toString()}`);
}
export async function createX(formData: FormData) {
const raw = Object.fromEntries(formData.entries()) as Record<string, unknown>;
// 落ちやすい前提は個別に日本語で弾く(zod に任せると ZodError が digest 化する)
if (!String(raw.customer_id ?? "").trim()) {
backToForm(raw, "顧客が選択されていません。候補の中からクリックして選んでください。");
}
let parsed: z.infer<typeof Schema>;
try {
parsed = Schema.parse(raw);
} catch (e) {
console.error(`[createX] zod: ${(e as Error).message}`, { rawKeys: Object.keys(raw) });
backToForm(raw, `入力エラー: ${(e as Error).message}`);
}
const { data, error } = await db.from("<table>").insert({ ...parsed }).select("id").single();
if (error) {
console.error(`[createX] insert: ${error.message}`, { code: error.code, details: error.details });
backToForm(raw, `登録に失敗しました: ${error.message}`);
}
redirect(`/<詳細>/${data.id}`);
}
フォーム側で ?error= をバナー表示する。redirect() は NEXT_REDIRECT を throw する仕組みなので、try の中で呼ばない(catch に飲まれる)。上の形(catch の中 / try の外で呼ぶ)なら安全。
5. 検証も書き込み無しでできる
// ① バナーが出ること
await page.goto(`${BASE}/<フォーム>?error=${encodeURIComponent("テスト")}`);
// ② 未選択で送信がブラウザに止められること(= レコードは作られない)
await page.locator('form input[type="text"]').first().fill("テスト会社");
await page.locator('form button[type="submit"]').last().click();
const msg = await combo.evaluate((el: HTMLInputElement) => el.validationMessage);
// URL がフォームのままで、validationMessage が期待文言であること
6. 落とし穴: 本番が git のどのブランチから出ているか先に確かめる
vercel inspect <本番ドメイン> に git メタデータが出てこない場合、その本番は CLI(vercel --prod)で作業ツリーからデプロイされている。このとき origin/main を読んで調査すると、本番に無いコードを読んで誤診する(実例: main 側だけ商材が4値の固定 enum のままで、本番は自由文字列だった)。必ず「本番と同じソース」を読むこと。作業ツリーに未コミット差分がある場合は、その差分が本番デプロイ時刻より前か後かを mtime で確認してから再デプロイする。
7. AI に投げるときの指示テンプレ
本番 <URL> の <機能> が Next.js の error digest <番号> で失敗している。
1. 本番へ GET のみの Playwright で <該当ページ> を開き、描画時点で落ちるか送信時に落ちるか切り分けて報告(書き込み禁止)
2. 対象テーブルの NOT NULL/CHECK/RLS を読み取りだけで確認
3. フォームの各 input の name/type と、Server Action のスキーマの項目を突き合わせ、
(a) hidden input に required を頼っている箇所 (b) フォームが送るのにスキーマが拾っていない項目 を列挙
4. Server Action の throw を ?error= へのリダイレクトに置き換え、console.error で原因をログに残す
5. 書き込み無しの Playwright で「バナーが出る」「未選択で送信が止まる」を実測して報告
推測は書かず、実測できたことと「未取得」を分けて報告すること。
よくある質問
+「「エラー番号しか出ない」Next.js 本番不具合を、本番を壊さずに原因まで追い詰める」とは何ですか?
Next.js の error digest だけが出る本番障害を、書き込みゼロの再現・スキーマ読み取り・hidden input required の罠の3点で特定し、Server Action から throw を無くして二度と番号だけにしない手順
+どれくらいトークン(費用)を節約できますか?
ゼロから開発すると約6万トークンかかりますが、この巻物を使えば約3,200トークンで済みます。差し引き約5.7万トークン(API料金換算で約85円)・95%の節約です。
+どうやって使いますか?
無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。
+どのAIツールに対応していますか?
claude-code、cursor、codex-cli に対応しています。
+商用利用できますか?
ライセンスは「商用利用可 (再販不可)」です。
🤝 自分でAIを動かすのは、まだ不安…という方へ
この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。
関連する巻物
Next.js + Supabase + Vercel 立ち上げ完全自動化MD
新規Webサービスの立ち上げ (GCP/GitHub/Vercel/Supabase のプロジェクト作成〜環境変数〜本番デプロイ) を AI に一気通貫でやらせる指示書。人間の作業はログイン1回だけ。
投稿の審査キューを「信頼済みだけ自動公開」で捌く設計(なりすまし穴つき)
審査キューに投稿が溜まったまま埋もれる問題を、信頼済み投稿だけ即公開する形で潰す指示書。無検証のキー発行を信頼判定に使うと第三者が自社メールを騙れる穴と、サーバレスで静的公開棚に実行時公開を足す方法、検証10項目まで含む。
DB型サイトを「一覧だけ会員限定・個別ページは残す」に切り替える指示書(Next.js App Router)
自社DBで集客していたサイトが競合のリスト抜き取りに気づいた時の改修手順。名前が並ぶバルクな一覧だけを会員限定にし、個別ページはtitle/H1とCTAを残す。ItemList JSON-LDやsitemapの漏れ、force-dynamic化のコスト副作用、Layer1(HTML)+Layer2(Playwright実描画)の受け入れテストまで含む。
この巻物、誰かのトークンも救えます
𝕏 で節約レシートをシェア