Node.js での画像アップロードのサイズ制限
スマートフォンで撮ったままの写真は、想定していた制限よりも大きいことがよくあります。Node.js の構成では、最大 5 か所でそうしたファイルが拒否される可能性があり、それぞれ返すレスポンスが異なります。このページでは、リクエストが通過する順にそれらを確認し、Express のサンプルが multer の制限をきれいな JSON の 413 に変換する方法を示します。また、巨大なアップロードのうち、サーバーが結局どれだけ読み込むかを左右する multer の動作についても説明します。
各レイヤーとその順序
- ギャラリーダイアログ。エディターのオプションで
maxUploadFileSize: 4000000を指定すると、ダイアログはそれより大きなファイルを送信前に拒否します。4.5 MB のファイルでは「big.jpg is 4.3 MB. The maximum allowed size is 3.8 MB.」と表示され(ダイアログは 2 進数のメガバイトで計算します)、リクエストは送信されませんでした。これは利便性のための機能です。ダイアログを使わなくても、誰でもその URL を呼び出せます。 - リバースプロキシ(使っている場合)。nginx のドキュメントでは、
client_max_body_sizeの既定値は 1m とされています。そのため、既定設定の nginx の背後では、2 MB の写真は Node に届きません。nginx は独自の HTML の 413 ページを返し、ダイアログはこれをアップロードの失敗として報告します。 - multer の
limits(自分で設定するもの)。multer 自体のfileSize、files、fields、partsの既定値はInfinityで、メモリストレージではファイル全体がメモリに載ることになります。 - エラーミドルウェア。multer のエラーを、ブラウザーが想定する JSON に変換します。
- サンプル独自のチェック。
saveImage()内のチェックで、同じく 4,000,000 バイトです。multer の制限を引き上げた場合にのみ意味を持ちます。
サンプルのメッセージがユーザーに表示されるようにするには、それより前のすべてのレイヤーで 4,000,000 バイトより少し大きいサイズまで許可しておく必要があります。マルチパートのエンコードによって、ファイルの前後に数百バイトが加わるためです。
multer の制限
// multer is the multipart parser most Express apps already use. Memory storage keeps the
// file in req.file.buffer, so its bytes can be checked before anything touches the disk;
// the limits keep that buffer small. multer allows exactly fileSize bytes and fails with
// LIMIT_FILE_SIZE above that (turned into a 413 in the error handler below).
const parseForm = multer({
storage: multer.memoryStorage(),
limits: { fileSize: MAX_BYTES, files: 1, fields: 10, fieldSize: 1024, parts: 12 },
defParamCharset: "utf8", // file names are sent as UTF-8; multer's default is latin1
});fileSize は許可される最大のサイズであり、拒否される最初のサイズではありません。ちょうど 4,000,000 バイトのファイルを送信すると保存され、4,000,001 バイトでは拒否されました。そのほかの制限があるのは、メモリストレージがすべてを保持するためです。ファイルは 1 つ、テキストフィールドは最大 1,024 バイトのものが 10 個、パートは全部で 12 個です。multer の fieldSize の既定値は、テキストフィールド 1 つにつき 1 MB です。
MulterError から 413 へ
ファイルが fileSize を超えると、multer は MulterError を渡して next(err) を呼び出します。その code は "LIMIT_FILE_SIZE"、message は "File too large"、field は "file" です。エラーハンドラーがない場合は Express の既定の動作となり、ステータス 500 と HTML のページが返されました。NODE_ENV=production を設定していないと、このページにはスタックトレースが含まれます。サンプルのエラーミドルウェアは、このコードをチェックします。
// Express 5 sends errors thrown in async handlers here, so the routes need no try/catch.
app.use((err, req, res, next) => {
if (err instanceof GalleryError) {
return res.status(err.status).json({ error: err.message });
}
if (err instanceof multer.MulterError) {
const tooBig = err.code === "LIMIT_FILE_SIZE";
return res
.status(tooBig ? 413 : 400)
.json({ error: tooBig ? "The image is larger than 4,000,000 bytes." : "Invalid upload: " + err.message });
}
console.error(err);
res.status(500).json({ error: "Server error." });
});multer のそれ以外のエラー(2 つ目のファイルに対する LIMIT_FILE_COUNT、別のフィールド名のファイルに対する LIMIT_UNEXPECTED_FILE)は、multer のメッセージ付きの 400 になります。メッセージのテキストではなく、err.code をチェックしてください。
multer は先に残りの本文を読み込む
multer は、制限を超えた瞬間に 413 を送信するわけではありません。エラーが発生すると、リクエストの残りを読み込んで破棄してから next(err) を呼び出します。その理由は multer のソースコード内のコメントに書かれています。クライアントがまだ送信している最中に接続を閉じると、クライアント側で EPIPE エラーが発生するためです。当社で計測したところ、Content-Length なしで送信した 20 MB のアップロードでは、413 が返ってくる前にクライアントが 20,000,000 バイトすべてを送信し終えていました。制限に達した時点で読み込みを止める Next.js のサンプルは、約 4.9 MB の時点でレスポンスを返しました。
余分なバイト列は破棄されるため、メモリについては安全です。しかし、帯域幅と時間はそうではありません。1 GB のアップロードは最後まで読み込まれます。これが問題になる場合は、Node より前のリバースプロキシで大きな本文を拒否するか、multer より前のミドルウェアで req.headers["content-length"] をチェックしてください。ただし、Content-Length のチェックだけでは、長さを持たないチャンク形式のリクエストには対応できません。
テストする
このガイドのサンプルプロジェクトを npm start で起動し、制限を超えるファイルを作って送信します。
head -c 4500000 /dev/zero > big.jpg
curl -F "file=@big.jpg" http://localhost:5101/rte/upload
{"error":"The image is larger than 4,000,000 bytes."}ステータスは 413 です。このファイルはすべてゼロなので JPEG のシグネチャチェックでも失敗するはずですが、multer の制限が先に適用されます。ダイアログ自体のチェックを確認するには、ギャラリーの「Upload」ボタンで 4,000,000 バイトを超えるファイルを選びます。ダイアログはサイズに関するメッセージを表示し、何も送信しません。
ほかのプラットフォーム向けの同じガイド
Next.js · PHP · ASP.NET Core · MVC 5 · Web Forms