Vercel に上げたらファイルアップロードが 413 で落ちた(実測つき)
画像をアップロードする機能を作って Vercel に上げたら、大きいファイルだけ失敗しました。
HTTP/2 413
Content-Length: 0ローカルでは通ります。本番だけ落ちます。しかもエラーメッセージが何も返ってきません。 アプリ側で用意した「アップロードに失敗しました」も出ません。
実際に測った
あるWebアプリの本番環境に、サイズを変えて投げてみました。
- 800KB → 401(未ログインなので、これが正しい応答。アプリまで届いている)
- 6MB → 413(ログイン状態に関係なく落ちる。アプリまで届いていない)
401 が返ったということは、リクエストがアプリのコードまで到達し、認証処理が動いた証拠です。 一方 6MB は 413 で、認証すら実行されていません。
つまり 413 はアプリのバグではなく、その手前で切られているということです。
原因
Vercel のサーバーレス関数には、リクエストの本体サイズに 4.5MB の上限があります。 アプリのコードが動く前に、プラットフォーム側で弾かれます。
ここが厄介なところで、アプリ側で何を書いても回避できません。
bodyParserの設定を緩めても効かない- フレームワーク側の上限を上げても効かない
- try/catch でエラーを掴もうとしても、そもそもコードが実行されていない
私が実際にやらかしていたこと
アップロード画面に「50MBまで」と書いていました。
そう書いた記憶はあります。フレームワークの設定でそう決めたつもりでいました。 けれどプラットフォームの上限のほうが小さいので、その表示は最初から嘘でした。
利用者から見ると「50MBまでと書いてあるのに、5MBの画像で失敗する。しかもエラーが出ない」 という状態です。いちばん質の悪い壊れ方です。
対処
1. まず上限の表示を実態に合わせる
これが最優先です。コードを直すより先に、嘘の表示を消す。
上限は 4.5MB ですが、そのまま書くと境界で失敗します。 リクエストにはファイル以外のデータやヘッダーも乗るためです。4MB と表示して、 サーバー側でも 4MB で弾くようにしました。
2. ブラウザ側で先に止める
サーバーまで送ってから落とすのは、待ち時間の分だけ利用者に損をさせます。 選んだ瞬間に判定して、その場で伝えます。
const MAX_UPLOAD_SIZE = 4 * 1024 * 1024;
if (file.size > MAX_UPLOAD_SIZE) {
// 送る前に止める。送ってから 413 で落ちると、
// 待たされた挙げ句にエラーの理由も分からない
setError("ファイルが大きすぎます(4MBまで)");
return;
}ブラウザ側だけで済ませないこと。 開発者ツールから直接投げれば素通りするので、 サーバー側でも同じ値で弾きます。上限の値は1か所にまとめて、両方から参照します。
3. 本当に大きいファイルを扱うなら
サーバーを経由させない形にします。ブラウザから直接ストレージへ送り、 アプリはその許可証(署名付きURL)だけを発行する構成です。 リクエストがアプリを通らないので、上限にかかりません。
⚠️ この構成はこの記事の題材のアプリでは採っていません。 4MB で足りる用途だったためです。 使っていないものを「こうすればできます」と書くのは避けます。
何を持ち帰るか
ローカルで動くことは、本番で動く証拠になりません。
特にプラットフォームの制限は、ローカルには存在しません。開発機にはリクエストサイズの 上限も実行時間の制限もないので、本番に上げて初めて出てきます。
そして今回のように、エラーが握りつぶされる形で出ることがあります。 アプリのログを見ても何も残りません。アプリまで届いていないからです。
本番で妙な失敗をしたら、まず小さいデータで同じ操作を試してください。 小さいと通って大きいと落ちるなら、原因はアプリの中にはありません。
VercelNext.jsアップロード個人開発