RailsアプリケーションでRack::Multipart::MultipartTotalPartLimitError
というエラーに遭遇し、なぜこうなるのか理解できていませんでした。
クライアントからリクエストを受けてからRailsがレスポンスを返す流れを整理してみました。
Rack::Multipart::MultipartTotalPartLimitError
Rackがマルチパートリクエスト(主にファイルアップロードや複数のフォームパラメータを含むPOSTリクエスト)を処理する際に、「受信したパート(フォームフィールドやファイルなど)の合計数」が設定された上限(デフォルト4096)を超えた場合に発生する。
multiparts(マルチパート)とは、1つのリクエストやメッセージの中で「複数のデータ(パート)」をまとめて送信できる仕組みです。
Webの世界では、特にmultipart/form-dataという形式がHTMLフォームからファイルやテキストデータを一緒に送るために使われます。
受信したパート(multipart/form-data)が4096を超えるPOSTリクエストの場合に起きるエラーのようです。
これは、悪意のあるユーザーが、何万もの非常に小さなパーツを持つリクエストをサーバーに送りつけると、サーバーはその解析に大量のCPUとメモリを消費してしまいます。これにより、サーバーがダウンしたり、他のユーザーへの応答が極端に遅くなったりする DoS攻撃(サービス妨害攻撃) につながる可能性があります。
この攻撃を防ぐために、Rackは受け付けるパーツの総数に上限(デフォルトでは4096)を設けており、それを超えるとこのエラーを発生させてリクエストを拒否するようになっています。
エラーが発生するタイミング
-
Nginx / Puma
ユーザーからのリクエスト(multipart/form-data形式のデータを含む)をそのまま受け取ります。この時点では、中身が何個のパーツに分かれているかはまだ解析していません。
↓ -
Rack Middleware Stack
リクエストはRails本体に届く前に、いくつかのミドルウェアを通過します。その中の一つに、ファイルアップロードなどを担当するRack::Multipart
があります。
↓ -
Rack::Multipart がリクエストを解析 ← ★エラーはここで発生
Rack::Multipart
は、送られてきたデータをRailsが扱いやすいようにパーツごとに分解・整理します。この分解作業の開始直後に、「パーツが全部で何個あるか」をチェックします。
この時点で、パーツ数が上限(例: 4096個)を超えていると判断されると、即座に処理を中断しRack::Multipart::MultipartTotalPartLimitError
を発生させます。
↓
【処理がここでストップするため、これ以降には進まない】
↓ -
Rails Router
↓ -
Rails Controller
レストランを例に役割の整理
コンポーネント | 役割 (レストランの例え) | 主な仕事 | |
---|---|---|---|
🤵 | Nginx | 受付・案内係 | 全リクエストの受付、静的ファイルの高速配信、Pumaへのリクエスト転送(リバースプロキシ) |
👨💼 | Puma/Unicorn | フロアマネージャー | Railsアプリケーションの管理、NginxからのリクエストをRailsに渡す |
📝 | Rack | 共通の注文票 | PumaとRailsが会話するための「ルール・規格」。コンポーネントではなく約束事。 |
👨🍳 | Rails | キッチン・シェフ | データベースとの連携、ビジネスロジックの実行、最終的なWebページ(HTML)の生成 |
リクエストの流れ
Rack::Multipart::MultipartTotalPartLimitError
への対応
フロント側でバリデーション
今回のケースだとRailsのバリデーションの前にエラーが起きるので、フロント側にバリデーションをするのが良さそうです。
パーツ総数上限(デフォルトでは4096)を超える場合にメッセージを表示したいので、3,000 ~ 4,000のパーツを含むリクエストに対してバリデーションするようにできると良さそうです。
バックエンド側でバリデーション
フロント側でバリデーションをしても悪意あるリクエストや直接APIを叩くことも可能です。
そしてシステムとしての独立性と安全性を保つために、バックエンド側にもバリデーションがあると良さそうです。
(*RackのRACK_MULTIPART_TOTAL_PART_LIMIT
は無制限にすることも可能。将来的に無制限にする対応などがあれば、悪意あるリクエストが来やすくなってしまうかも)
バリデーションをそれぞれのモデルに追加する工数が大きい場合は、まずレスキューさせておくのも手になりそうです。
バックエンド側でレスキュー
(推測)
Rackミドルウェアでエラーになっても、バックエンド側でレスキューすることでクライアントにメッセージを返すことも可能そうです。
class ApplicationController < ActionController::API
rescue_from Rack::Multipart::MultipartTotalPartLimitError, with: :render_payload_too_large
private
def render_payload_too_large
render json: {
error: "Payload Too Large",
message: "一度に送信できる項目数が上限を超えています。"
}, status: :payload_too_large
end
end
まとめ
クライアントからリクエストされ、Railsに届く前にエラーが起きていた事象について整理してみました。
現在のシステムの状況を整理して、バリデーションを設けることでクライアントが使いやすいサービスになりそうです!
誤りやより良いバリデーション、対応方法があれば教えてください 🙌