Next.js での画像アップロードのサイズ制限
Next.js のルートハンドラーでは、アップロードを読み取る方法としてドキュメントに記載されているのが request.formData() ですが、Next.js にはそのアップロードのサイズを制限する仕組みがありません。このページでは、当社が計測した結果と、サンプルが代わりに使っている小さな readForm() 関数、そしてコードより先に大きな画像を拒否できるほかのレイヤーを紹介します。
formData() はすべてを読み込む
await request.formData() だけを行う一時的なルートハンドラーを追加し、50 MB のファイルを送信しました。レスポンスは fileSize: 50000000 付きの 200 で、その 1 回のリクエストの間に、開発サーバーのメモリ(RSS)は約 175 MB 増えました。ルートハンドラーのドキュメントは formData() について説明していますが、サイズ制限には触れていません。Pages Router の API ルートとは異なり、ルートハンドラーには bodyParser の設定が不要だと記載されています。つまり、設定する値はありません。制限はすべて自分のコードで設ける必要があります。
解析する前にバイト数を数える
lib/gallery.ts の readForm() は、申告された Content-Length が制限を超えていれば拒否し、そうでなければ本文のストリームを読み込み、制限を超えた時点で読み込みを止めます。制限内の本文だけが、Response を通じて標準のパーサーに渡されます。
// request.formData() reads the whole body into memory, however large. This reads at most
// MAX_BYTES plus room for the other fields, answers 413 above that, then parses it.
export async function readForm(request: Request): Promise<FormData> {
const limit = MAX_BYTES + 64 * 1024;
if (Number(request.headers.get("content-length")) > limit) {
throw new GalleryError(413, TOO_BIG);
}
const chunks: Uint8Array[] = [];
let size = 0;
const reader = request.body?.getReader();
while (reader) {
const { done, value } = await reader.read();
if (done) break;
size += value.length;
if (size > limit) {
await reader.cancel();
throw new GalleryError(413, TOO_BIG);
}
chunks.push(value);
}
const body = new Response(Buffer.concat(chunks), {
headers: { "content-type": request.headers.get("content-type") ?? "" },
});
try {
return await body.formData();
} catch {
throw new GalleryError(400, "Expected a multipart/form-data request.");
}
}制限は、4,000,000 バイトのファイルに、フォームのフィールドとその周りのマルチパートヘッダーのための 64 KB を加えた値です。Content-Length のチェックだけでは十分ではありません。チャンク形式のリクエストには長さがないからです。実際にそうしたリクエストを送信したところ、ストリームのカウントがそれを捕捉しました。next dev と next start のどちらでも、20 MB の本文のうちクライアントが約 4.9 MB を送信した時点で 413 が返り、クライアント側で接続エラーは発生しませんでした。reader.cancel() が読み込みを止めるため、残りがバッファーに溜まることはありません。比較すると、multer を使った Express は、20 MB すべてを読み込んでからレスポンスを返しました。
解析の後、saveImage() がファイル自体をチェックします。ちょうど 4,000,000 バイトの境界を判定するのはこのチェックです。そのサイズのファイルは保存され、4,000,001 バイトのファイルは 413 で拒否されました。
ほかのレイヤー
- ギャラリーダイアログ。エディターのオプションで
maxUploadFileSize: 4000000を指定すると、ダイアログはそれより大きなファイルを何も送信する前に拒否し、「big.jpg is 4.3 MB. The maximum allowed size is 3.8 MB.」と表示します。サーバーはこれに頼ることはできません。 - リバースプロキシ。nginx のドキュメントでは、
client_max_body_sizeの既定値は 1m とされています。既定設定の nginx の背後では、2 MB の写真は nginx 独自の 413 ページを受け取り、Next.js には届きません。 proxyファイル(以前はミドルウェアと呼ばれていたもの)。Next.js のドキュメントによると、これを使う場合、Next.js はプロキシとルートの両方が読み取れるようにリクエストの本文をバッファーに格納します。上限はexperimental.proxyClientMaxBodySizeで、既定値は 10 MB です。これを超えると、本文は上限で切り詰められ、警告がログに記録されます。サンプルには proxy ファイルがないため、この動作はテストしていません。この説明どおりなら、proxy ファイルを追加すると、10 MB を超える本文は拒否されるのではなく、途中で切れた状態でルートハンドラーに届きます。- Server Actions には独自の
serverActions.bodySizeLimitがあり、ドキュメントによると既定値は 1 MB です。この設定は Server Actions 用です。ルートハンドラーには何の効果もなく、エディターのアップロードが送られるのはルートハンドラーです。
ページ上部の図は、その順序を示しています。サンプルの 413 がユーザーに返されるようにするには、それより前のすべてのレイヤーで 4,000,000 バイトより少し大きいサイズまで許可しておく必要があります。
ブラウザーに返るもの
サンプルが拒否する場合は、すべて {"error":"The image is larger than 4,000,000 bytes."} 付きの HTTP 413 になります。ギャラリーダイアログは、アップロードが失敗すると常に独自のテキスト「Upload failed for <name>.」を表示します。そのため、ダイアログ自体のサイズチェックを有効にしている場合、ユーザーが目にするのは通常、ダイアログのメッセージのほうです。
テストする
このガイドのサンプルプロジェクトを npm run build と npm start で起動し、以下を実行します。
head -c 4500000 /dev/zero > big.jpg
curl -F "file=@big.jpg" http://localhost:5102/rte/upload
{"error":"The image is larger than 4,000,000 bytes."}ステータスは 413 です。チャンク形式の場合を試すには、長さを指定せずに本文を送信します。たとえば、fetch で ReadableStream を本文に指定します(duplex: "half")。レスポンスは同じ 413 で、アップロードが終わる前に返ってきます。
ほかのプラットフォーム向けの同じガイド
JavaScript · PHP · ASP.NET Core · MVC 5 · Web Forms