Server ActionsのbodySizeLimit変更だけでは足りず、エラー境界に落ちる
※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。
結論
Next.jsのServer Actionsはリクエスト本文がbodySizeLimit(既定1MB)を超えると、actionが{error}を返す前にフレームワーク層でthrowし、app/error.tsxのグローバルエラー境界に落ちる。
結論
Next.jsのServer Actionsは、リクエスト本文がserverActions.bodySizeLimit(既定1MB)を超えると、action自身が{error}をreturnするより先にフレームワーク層でthrowし、app/error.tsxのグローバルエラー境界へ落ちる。 bodySizeLimitを引き上げるだけでは「新しい上限を超える送信」が起きた瞬間に同じ現象が再発するため、クライアントとサーバーの両方にbodySizeLimitより低い総量ガードを入れ、超過を{error}のインライン表示として処理する必要がある。
症状
配布資料を共有する管理画面のアップロードフォームは、PDFやスライドを添付してServer Action(uploadShareFile)経由でアップロードする作りになっている。1MBを超える資料をアップロードすると、フォーム内に想定していた「エラー」ではなく、アプリ全体の汎用エラー画面が表示された。
export default function Error({
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<main className="flex min-h-screen flex-col items-center justify-center bg-gray-50 px-4 text-center">
<h1 className="text-xl font-bold text-gray-900">
エラーが発生しました
</h1>
<p className="mt-2 text-sm text-gray-500">
時間をおいて再度お試しください。問題が続く場合は担当者にご連絡ください。
</p>
<button onClick={reset} ...>再読み込み</button>
</main>
);
}
このコンポーネント(app/error.tsx)はアプリ全体のerror境界で、個別のフォームのために書いたものではない。にもかかわらず、1MBを超える資料を選んだだけでこの画面に落ちていた。
原因
Server ActionのuploadShareFileは内部で{error: "..."}という値を返す設計で、呼び出し元のフォームはその値をインラインで表示するようになっている。つまりバリデーションエラーであれば本来この経路を通り、汎用エラー画面には落ちないはずだった。
原因は、Next.jsのServer Actionsに既定で設定されているbodySizeLimit(1MB)にあった。PDFやスライドといった配布資料は1MBを超えることが珍しくなく、リクエスト本文がこの上限を超えると、Next.jsのフレームワーク層がaction自体の処理が終わる前に例外をthrowする。actionのコード自身は{error}を返そうとしているのに、そこへ到達する前にフレームワークが割り込んで例外を送出するため、呼び出し元は「actionが返した値」ではなく「投げられた例外」を受け取ることになり、Reactのerror.tsx境界がそれを捕まえて汎用画面を出す。
experimental: {
serverActions: {
// Server Actions のリクエストボディ既定上限は 1MB。配布資料(PDF・スライド等)は
// 1MB を超えることが多く、既定のままだと /admin/share のアップロードが
// フレームワーク層で弾かれ、Server Action が return する前に throw して
// グローバルエラー境界(app/error.tsx)が出てしまう(= 共有ボタンでエラー画面)。
bodySizeLimit: "4.5mb",
},
},
bodySizeLimitを上げるだけでは、根本的な解決にならない。上限を4.5MBに引き上げても、4.5MBを超える送信が来れば同じ現象がまた起きる。「actionのコードが処理できる範囲」と「フレームワークが本文を受け付ける範囲」を一致させ、かつフレームワーク側の上限に達する前にアプリ側で先に検知して{error}として処理する必要がある。
直し方
まずbodySizeLimitを4.5MBに設定した。これは任意の値ではなく、VercelのFunctionボディ上限(超過すると413で拒否される)に合わせたものだ。
そのうえで、アプリ側の総量上限をbodySizeLimitより低い位置に単一ソースとして定義した。
/**
* 配布ファイルアップロードの「1回の送信」に載せられる合計サイズの上限。
* クライアント(ShareFileUploadForm)とサーバー(uploadShareFile/createBundle)の
* 両方がこの単一ソースを参照して同じ線で弾く。
*
* next.config.ts の serverActions.bodySizeLimit(4.5mb)より 512KB 低く置き、
* multipart オーバーヘッドぶんの余裕を残す。
*/
export const MAX_TOTAL_UPLOAD_BYTES = 4 * 1024 * 1024;
bodySizeLimitの4.5MBぴったりを上限にすると、multipartのオーバーヘッド分だけで超過してしまう可能性があるため、512KB低い4MBをアプリ側の上限にしている。この4MBという値をクライアントとサーバーの両方が同じ場所から参照する。
クライアント側(フォーム)では、送信前に合計サイズを計算して超過なら送信そのものをブロックする。
const totalBytes = picked.reduce((s, p) => s + p.file.size, 0);
const overLimit = totalBytes > MAX_TOTAL_UPLOAD_BYTES;
function onSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
if (picked.length === 0 || pending) return;
// 合計サイズが上限超過なら送信しない(超過を送ると Server Action が return 前に
// throw し、グローバルエラー境界=共有ボタンでエラー画面になる)。警告は下に常時表示。
if (overLimit) return;
const fd = new FormData();
for (const p of picked) fd.append("file", p.file);
// ...
dispatch(fd);
}
送信ボタンのdisabledにもoverLimitを加え、選択リストの合計サイズ表示を超過時は赤字にし、常時表示の警告文でも上限を明示するようにした。これによって、そもそも上限を超える送信自体がブラウザから送られなくなる。
ただしクライアント側のガードだけでは、フォームを経由しない直接呼び出しに対して無防備になる。サーバー側のactions.tsにも同じ4MB総量ガードを入れ、超過時は{error}をインラインで返すようにして二重防御にした。クライアント・サーバーどちらの経路でもbodySizeLimit(4.5MB)そのものには到達しないため、フレームワーク層のthrowは発生せず、超過は常に{error}のインライン表示として処理される。
再発防止
この不具合が見えにくかったのは、Server Action内の{error}返却という「正常系のエラー処理」と、bodySizeLimit超過という「フレームワーク層の例外」が、見た目のうえでは両方とも「エラーになる」という同じ結果に見えることだった。実際にはコンポーネント側の分岐が異なり、後者は{error}のインライン表示を経由せずapp/error.tsxに落ちる。原因の切り分けは、action内の{error}表示がインラインである設計を把握したうえで「汎用エラー画面が出た=action自身のreturnより手前で何かが起きている」という推論から特定した。今回のテストでは、単一ファイルの超過と複数ファイル合計の超過という2パターンについて、{error}が返り、Storage・DBのどちらにも書き込みが発生していないことを確認するケースを追加している。
よくある質問
Q1bodySizeLimitを上げるだけでは何が足りないのですか?
上限を上げても、リクエスト本文がその新しい上限を超える可能性は残ります。上限に達するとフレームワークがaction内の{error}返却より先にthrowするため、クライアント側とサーバー側の双方に、bodySizeLimitより低い総量ガードを入れて先に弾く必要があります。
Q2Vercelにデプロイする場合、bodySizeLimitはいくつまで上げられますか?
VercelのFunctionボディ上限は4.5MBで、これを超えるリクエストは413で拒否されます。bodySizeLimitを4.5MB超に設定しても意味が無く、実務上は4.5MB以下、かつそれより低い位置にアプリ側の総量ガードを置くのが安全です。
Q3エラー境界に落ちたことは、どうやって特定しましたか?
Server Action内で{error}を返す実装はコンポーネント側でインライン表示される作りだったため、代わりにapp/error.tsxの汎用画面が出た時点で、actionのreturnより先にフレームワーク層のthrowが起きていると特定できました。
確認した環境
- Next.js 16.2.7 / React 19.2.4
- 2026-07-22 に本番で確認・同日修正
この記事の根拠
- TypeScriptファイル 1〜25行目コミット d5077aa
- TypeScriptファイル 79〜100行目コミット d5077aa
- TypeScriptファイル 1〜14行目コミット d5077aa
本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。