つかったこのサイトについて

Vercel に上げたらファイルアップロードが 413 で落ちた(実測つき)

画像をアップロードする機能を作って Vercel に上げたら、大きいファイルだけ失敗しました。

HTTP/2 413
Content-Length: 0

ローカルでは通ります。本番だけ落ちます。しかもエラーメッセージが何も返ってきません。 アプリ側で用意した「アップロードに失敗しました」も出ません。

実際に測った

あるWebアプリの本番環境に、サイズを変えて投げてみました。

  • 800KB401(未ログインなので、これが正しい応答。アプリまで届いている
  • 6MB413(ログイン状態に関係なく落ちる。アプリまで届いていない

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アップロード個人開発